Skip to content

Commit a764f71

Browse files
committed
refactor: replace checksum with transform interface for payload processing in EncodedFormat
1 parent c5d235d commit a764f71

File tree

3 files changed

+66
-79
lines changed

3 files changed

+66
-79
lines changed

src/commonMain/kotlin/com/eignex/kencode/EncodedFormat.kt

Lines changed: 32 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@ import kotlinx.serialization.modules.SerializersModule
77
* Holds the configuration for an [EncodedFormat] instance.
88
*
99
* @property codec The ASCII-safe byte codec used to turn raw bytes into text (e.g., Base62, Base64).
10-
* @property checksum An optional checksum appended to the binary payload and verified upon decoding.
10+
* @property transform An optional [PayloadTransform] applied after serialization and before encoding.
11+
* Common uses: integrity checks via [Checksum.asTransform], encryption, or error-correcting codes.
1112
* @property binaryFormat The underlying binary serialization format used before encoding to text.
1213
* @property compactZeros When true, leading zero bytes are stripped before encoding and restored on decode.
1314
* A varint prefix encodes the count, costing 1 byte for up to 127 stripped bytes.
1415
*/
1516
data class EncodedConfiguration(
1617
val codec: ByteEncoding = Base62,
17-
val checksum: Checksum? = null,
18+
val transform: PayloadTransform? = null,
1819
val binaryFormat: BinaryFormat = PackedFormat.Default,
1920
val compactZeros: Boolean = false,
2021
)
@@ -23,16 +24,16 @@ data class EncodedConfiguration(
2324
* Text `StringFormat` that produces short, predictable string tokens by composing:
2425
*
2526
* 1. A binary format (e.g. [PackedFormat], `ProtoBuf`).
26-
* 2. An optional checksum.
27+
* 2. An optional [PayloadTransform] (checksum, encryption, ECC, …).
2728
* 3. An ASCII-safe byte encoding (e.g. [Base62], [Base64], [Base36], [Base85]).
2829
*
2930
* Typical use:
30-
* - `encodeToString`: serialize -> (optionally) append checksum -> encode bytes to text.
31-
* - `decodeFromString`: decode text to bytes -> (optionally) verify checksum -> deserialize.
31+
* - `encodeToString`: serialize -> transform.encode -> (optionally) compact zeros -> encode bytes to text.
32+
* - `decodeFromString`: decode text to bytes -> (optionally) restore zeros -> transform.decode -> deserialize.
3233
*
3334
* Use the [EncodedFormat] builder function to create a customized instance.
3435
*
35-
* @property configuration The active configuration dictating the codec, checksum, and binary format.
36+
* @property configuration The active configuration dictating the codec, transform, and binary format.
3637
*/
3738
@OptIn(ExperimentalSerializationApi::class)
3839
open class EncodedFormat(
@@ -44,72 +45,45 @@ open class EncodedFormat(
4445
*/
4546
constructor(
4647
codec: ByteEncoding = Base62,
47-
checksum: Checksum? = null,
48+
transform: PayloadTransform? = null,
4849
binaryFormat: BinaryFormat = PackedFormat,
4950
compactZeros: Boolean = true,
50-
) : this(EncodedConfiguration(codec, checksum, binaryFormat, compactZeros))
51+
) : this(EncodedConfiguration(codec, transform, binaryFormat, compactZeros))
5152

5253
/**
5354
* Delegates to the underlying [BinaryFormat]'s serializers module.
5455
*/
5556
override val serializersModule: SerializersModule get() = configuration.binaryFormat.serializersModule
5657

5758
/**
58-
* Default format: `PackedFormat` + `Base62` without a checksum.
59+
* Default format: `PackedFormat` + `Base62` without a transform.
5960
*/
6061
companion object Default : EncodedFormat()
6162

6263
/**
63-
* Serializes [value] with the configured binary format, optionally appends a checksum,
64+
* Serializes [value] with the configured binary format, applies the transform,
6465
* and encodes the resulting byte array using the text codec.
6566
*/
66-
override fun <T> encodeToString(
67-
serializer: SerializationStrategy<T>, value: T
68-
): String {
69-
val bytes =
70-
configuration.binaryFormat.encodeToByteArray(serializer, value)
71-
val checked = if (configuration.checksum != null) {
72-
bytes + configuration.checksum.digest(bytes)
73-
} else bytes
74-
val payload = if (configuration.compactZeros) compactZerosEncode(checked) else checked
67+
override fun <T> encodeToString(serializer: SerializationStrategy<T>, value: T): String {
68+
val bytes = configuration.binaryFormat.encodeToByteArray(serializer, value)
69+
val transformed = configuration.transform?.encode(bytes) ?: bytes
70+
val payload = if (configuration.compactZeros) compactZerosEncode(transformed) else transformed
7571
return configuration.codec.encode(payload)
7672
}
7773

7874
/**
79-
* Decodes [string] using the text codec, optionally verifies and strips the checksum,
80-
* then deserializes the remaining bytes with the configured binary format.
75+
* Decodes [string] using the text codec, applies the inverse transform,
76+
* then deserializes with the configured binary format.
8177
*
82-
* @throws IllegalArgumentException if a checksum is configured and the verification fails.
78+
* @throws IllegalArgumentException if the transform's decode step fails (e.g. checksum mismatch).
8379
*/
84-
override fun <T> decodeFromString(
85-
deserializer: DeserializationStrategy<T>, string: String
86-
): T {
80+
override fun <T> decodeFromString(deserializer: DeserializationStrategy<T>, string: String): T {
8781
val input = configuration.codec.decode(string)
8882
val raw = if (configuration.compactZeros) compactZerosDecode(input) else input
89-
val bytes = if (configuration.checksum != null) {
90-
require(raw.size >= configuration.checksum.size) {
91-
"Input too short to contain checksum: expected at least ${configuration.checksum.size} bytes but got ${raw.size}."
92-
}
93-
val bytes =
94-
raw.sliceArray(0..<raw.size - configuration.checksum.size)
95-
val actual =
96-
raw.sliceArray(raw.size - configuration.checksum.size..<raw.size)
97-
val expected = configuration.checksum.digest(bytes)
98-
require(actual.contentEquals(expected)) {
99-
"Checksum mismatch."
100-
}
101-
bytes
102-
} else raw
103-
return configuration.binaryFormat.decodeFromByteArray(
104-
deserializer,
105-
bytes
106-
)
83+
val bytes = configuration.transform?.decode(raw) ?: raw
84+
return configuration.binaryFormat.decodeFromByteArray(deserializer, bytes)
10785
}
10886

109-
/**
110-
* Strips leading zero bytes from [bytes], prepends a varint encoding of the
111-
* count, and returns the result.
112-
*/
11387
private fun compactZerosEncode(bytes: ByteArray): ByteArray {
11488
var k = 0
11589
while (k < bytes.size && bytes[k] == 0.toByte()) k++
@@ -120,10 +94,6 @@ open class EncodedFormat(
12094
return result
12195
}
12296

123-
/**
124-
* Reads a varint prefix written by [compactZerosEncode] and restores the leading
125-
* zero bytes, returning the original byte array.
126-
*/
12797
private fun compactZerosDecode(bytes: ByteArray): ByteArray {
12898
require(bytes.isNotEmpty()) { "Compact payload cannot be empty." }
12999
val (k, prefixLen) = varintDecode(bytes)
@@ -146,35 +116,23 @@ open class EncodedFormat(
146116
* Builder for configuring [EncodedFormat] instances.
147117
*/
148118
class EncodedFormatBuilder {
149-
/**
150-
* The ASCII-safe byte codec used to turn raw bytes into text. Defaults to [Base62].
151-
*/
152119
var codec: ByteEncoding = Base62
153-
154-
/**
155-
* An optional checksum appended to the binary payload. Defaults to `null`.
156-
*/
157-
var checksum: Checksum? = null
158-
159-
/**
160-
* The underlying binary serialization format. Defaults to [PackedFormat.Default].
161-
*/
120+
var transform: PayloadTransform? = null
162121
var binaryFormat: BinaryFormat = PackedFormat.Default
163-
164-
/**
165-
* When true, leading zero bytes are stripped before encoding and restored on decode.
166-
* Defaults to `true`.
167-
*/
168122
var compactZeros: Boolean = true
123+
124+
var checksum: Checksum?
125+
get() = null
126+
set(value) { transform = value?.asTransform() }
169127
}
170128

171129
/**
172130
* Creates a customized [EncodedFormat] instance.
173131
*
174132
* ```
175133
* val format = EncodedFormat {
176-
* codec = Base36
177-
* checksum = Crc16
134+
* codec = Base36
135+
* transform = Crc16.asTransform()
178136
* }
179137
* ```
180138
*
@@ -187,18 +145,15 @@ fun EncodedFormat(
187145
): EncodedFormat {
188146
val builder = EncodedFormatBuilder().apply {
189147
codec = from.configuration.codec
190-
checksum = from.configuration.checksum
148+
transform = from.configuration.transform
191149
binaryFormat = from.configuration.binaryFormat
192150
compactZeros = from.configuration.compactZeros
193151
}
194152
builder.builderAction()
195-
196-
val newConfig = EncodedConfiguration(
153+
return EncodedFormat(EncodedConfiguration(
197154
builder.codec,
198-
builder.checksum,
155+
builder.transform,
199156
builder.binaryFormat,
200157
builder.compactZeros,
201-
)
202-
203-
return EncodedFormat(newConfig)
158+
))
204159
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.eignex.kencode
2+
3+
/**
4+
* Transforms a binary payload before base-encoding and after base-decoding.
5+
*
6+
* Implementations can perform integrity checks (checksums), authentication,
7+
* encryption, or error-correcting codes — anything that maps bytes to bytes.
8+
*
9+
* Transforms must be inverse of each other: `decode(encode(data)).contentEquals(data)`.
10+
*/
11+
interface PayloadTransform {
12+
fun encode(data: ByteArray): ByteArray
13+
fun decode(data: ByteArray): ByteArray
14+
}
15+
16+
/**
17+
* Wraps this [Checksum] as a [PayloadTransform] that appends the digest on encode
18+
* and strips and verifies it on decode.
19+
*/
20+
fun Checksum.asTransform(): PayloadTransform = object : PayloadTransform {
21+
override fun encode(data: ByteArray): ByteArray = data + digest(data)
22+
23+
override fun decode(data: ByteArray): ByteArray {
24+
require(data.size >= size) {
25+
"Input too short to contain checksum: expected at least $size bytes but got ${data.size}."
26+
}
27+
val payload = data.copyOfRange(0, data.size - size)
28+
val actual = data.copyOfRange(data.size - size, data.size)
29+
require(actual.contentEquals(digest(payload))) { "Checksum mismatch." }
30+
return payload
31+
}
32+
}

src/commonTest/kotlin/com/eignex/kencode/EncodedFormatTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class EncodedFormatTest {
1010
data class Payload(val x: Int, val s: String)
1111

1212
private val formatNoChecksum: StringFormat = EncodedFormat(Base62, null)
13-
private val formatWithChecksum: StringFormat = EncodedFormat(Base62, Crc16)
13+
private val formatWithChecksum: StringFormat = EncodedFormat(Base62, Crc16.asTransform())
1414

1515
@Test
1616
fun `roundtrip without checksum`() {
@@ -103,7 +103,7 @@ class EncodedFormatTest {
103103

104104
val customFormat = EncodedFormat {
105105
codec = Base85
106-
checksum = Crc32
106+
transform = Crc32.asTransform()
107107
binaryFormat = PackedFormat.Default
108108
}
109109

0 commit comments

Comments
 (0)