Skip to content

Commit c421b00

Browse files
committed
feat(encoder): implement the encoder
1 parent d596d32 commit c421b00

File tree

18 files changed

+676
-16
lines changed

18 files changed

+676
-16
lines changed

api/kotlinx-serialization-bencoding.api

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,18 @@ public final class space/iseki/bencoding/BencodeCompositeDecoder$DefaultImpls {
3232
public static fun reportError (Lspace/iseki/bencoding/BencodeCompositeDecoder;Ljava/lang/String;Lkotlinx/serialization/descriptors/SerialDescriptor;I)Ljava/lang/Void;
3333
}
3434

35+
public abstract interface class space/iseki/bencoding/BencodeCompositeEncoder : kotlinx/serialization/encoding/CompositeEncoder {
36+
public abstract fun encodeBinaryStringElement (Lkotlinx/serialization/descriptors/SerialDescriptor;ILspace/iseki/bencoding/BinaryStringStrategy;Ljava/lang/String;)V
37+
public abstract fun encodeByteArrayElement (Lkotlinx/serialization/descriptors/SerialDescriptor;I[B)V
38+
public abstract fun getOptions ()Lspace/iseki/bencoding/BencodeOptions;
39+
public fun reportError (Ljava/lang/String;Lkotlinx/serialization/descriptors/SerialDescriptor;I)Ljava/lang/Void;
40+
}
41+
42+
public final class space/iseki/bencoding/BencodeCompositeEncoder$DefaultImpls {
43+
public static fun reportError (Lspace/iseki/bencoding/BencodeCompositeEncoder;Ljava/lang/String;Lkotlinx/serialization/descriptors/SerialDescriptor;I)Ljava/lang/Void;
44+
public static fun shouldEncodeElementDefault (Lspace/iseki/bencoding/BencodeCompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;I)Z
45+
}
46+
3547
public abstract interface class space/iseki/bencoding/BencodeConfigureScope {
3648
public abstract fun getBinaryStringStrategy ()Lspace/iseki/bencoding/BinaryStringStrategy;
3749
public abstract fun getDoubleStrategy ()Lspace/iseki/bencoding/FloatNumberStrategy;
@@ -67,11 +79,27 @@ public final class space/iseki/bencoding/BencodeDecoder$DefaultImpls {
6779
public final class space/iseki/bencoding/BencodeEncodeException : kotlinx/serialization/SerializationException {
6880
public static final field Companion Lspace/iseki/bencoding/BencodeEncodeException$Companion;
6981
public fun <init> (Ljava/lang/String;)V
82+
public static final fun of$kotlinx_serialization_bencoding (Ljava/lang/String;Lkotlinx/serialization/descriptors/SerialDescriptor;I)Lspace/iseki/bencoding/BencodeEncodeException;
7083
}
7184

7285
public final class space/iseki/bencoding/BencodeEncodeException$Companion {
7386
}
7487

88+
public abstract interface class space/iseki/bencoding/BencodeEncoder : kotlinx/serialization/encoding/Encoder {
89+
public abstract fun encodeBinaryString (Lspace/iseki/bencoding/BinaryStringStrategy;Ljava/lang/String;)V
90+
public abstract fun encodeByteArray ([B)V
91+
public abstract fun getOptions ()Lspace/iseki/bencoding/BencodeOptions;
92+
public fun reportError (Ljava/lang/String;)Ljava/lang/Void;
93+
}
94+
95+
public final class space/iseki/bencoding/BencodeEncoder$DefaultImpls {
96+
public static fun beginCollection (Lspace/iseki/bencoding/BencodeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;I)Lkotlinx/serialization/encoding/CompositeEncoder;
97+
public static fun encodeNotNullMark (Lspace/iseki/bencoding/BencodeEncoder;)V
98+
public static fun encodeNullableSerializableValue (Lspace/iseki/bencoding/BencodeEncoder;Lkotlinx/serialization/SerializationStrategy;Ljava/lang/Object;)V
99+
public static fun encodeSerializableValue (Lspace/iseki/bencoding/BencodeEncoder;Lkotlinx/serialization/SerializationStrategy;Ljava/lang/Object;)V
100+
public static fun reportError (Lspace/iseki/bencoding/BencodeEncoder;Ljava/lang/String;)Ljava/lang/Void;
101+
}
102+
75103
public abstract interface class space/iseki/bencoding/BencodeOptions {
76104
public abstract fun getBinaryStringStrategy ()Lspace/iseki/bencoding/BinaryStringStrategy;
77105
public abstract fun getDoubleStrategy ()Lspace/iseki/bencoding/FloatNumberStrategy;
Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,36 @@
11
package space.iseki.bencoding
22

33
internal interface BWriter {
4-
fun writeByte(b: Int)
5-
fun writeBytes(b: ByteArray)
4+
fun writeData(b: Int)
5+
fun writeData(b: ByteArray)
6+
7+
fun writeByteArray(s: ByteArray) {
8+
writeData(s.size.toString().encodeToByteArray())
9+
writeData(':'.code)
10+
writeData(s)
11+
}
12+
613
fun writeEnd() {
7-
writeByte('e'.code)
14+
writeData('e'.code)
815
}
916

1017
fun getByteArray(): ByteArray = throw UnsupportedOperationException("Not implemented")
1118

1219
fun writeDictBegin() {
13-
writeByte('d'.code)
20+
writeData('d'.code)
1421
}
1522

1623
fun writeListBegin() {
17-
writeByte('l'.code)
24+
writeData('l'.code)
1825
}
1926

2027
fun writeInt(i: Long) {
21-
writeByte('i'.code)
22-
writeBytes(i.toString().encodeToByteArray())
28+
writeData('i'.code)
29+
writeData(i.toString().encodeToByteArray())
2330
writeEnd()
2431
}
2532

2633
fun writeString(s: String) {
27-
writeBytes(s.length.toString().encodeToByteArray())
28-
writeByte(':'.code)
29-
writeBytes(s.encodeToByteArray())
34+
writeByteArray(s.encodeToByteArray())
3035
}
3136
}

src/commonMain/kotlin/space/iseki/bencoding/Bencode.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ interface Bencode : BinaryFormat {
1515
BencodeDecoder0(BytesLexer(bytes), serializersModule, options).decodeSerializableValue(deserializer)
1616

1717
override fun <T> encodeToByteArray(serializer: SerializationStrategy<T>, value: T): ByteArray {
18-
TODO("Not yet implemented")
18+
val writer = createBytesWriter()
19+
BencodeEncoder0(serializersModule, options, writer).encodeSerializableValue(serializer, value)
20+
return writer.getByteArray()
1921
}
2022

2123
override val serializersModule: SerializersModule
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package space.iseki.bencoding
2+
3+
import kotlinx.serialization.descriptors.SerialDescriptor
4+
import kotlinx.serialization.encoding.CompositeEncoder
5+
6+
interface BencodeCompositeEncoder : CompositeEncoder {
7+
val options: BencodeOptions
8+
fun encodeByteArrayElement(descriptor: SerialDescriptor, index: Int, value: ByteArray)
9+
fun encodeBinaryStringElement(
10+
descriptor: SerialDescriptor,
11+
index: Int,
12+
strategy: BinaryStringStrategy,
13+
value: String,
14+
)
15+
16+
fun reportError(message: String, descriptor: SerialDescriptor, index: Int): Nothing =
17+
throw BencodeEncodeException.of(message, descriptor, index)
18+
}
19+
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
package space.iseki.bencoding
22

33
import kotlinx.serialization.SerializationException
4+
import kotlinx.serialization.descriptors.SerialDescriptor
45

56
class BencodeEncodeException(reason: String) : SerializationException(messageOf(reason)) {
67
companion object {
78
private fun messageOf(reason: String) = "encoding failed: $reason"
9+
private fun messageOf(reason: String, descriptor: SerialDescriptor, index: Int) =
10+
"encoding failed at $descriptor[$index]: $reason"
11+
12+
@JvmStatic
13+
internal fun of(reason: String, descriptor: SerialDescriptor, index: Int) =
14+
BencodeEncodeException(messageOf(reason, descriptor, index))
815
}
916
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package space.iseki.bencoding
2+
3+
import kotlinx.serialization.ExperimentalSerializationApi
4+
import kotlinx.serialization.SerializationStrategy
5+
import kotlinx.serialization.descriptors.SerialDescriptor
6+
import kotlinx.serialization.encoding.Encoder
7+
import kotlinx.serialization.modules.SerializersModule
8+
9+
interface BencodeEncoder : Encoder {
10+
val options: BencodeOptions
11+
fun encodeByteArray(bytes: ByteArray)
12+
fun encodeBinaryString(strategy: BinaryStringStrategy, value: String)
13+
fun reportError(message: String): Nothing = throw BencodeEncodeException(message)
14+
}
15+
16+
internal interface EncoderDelegate : BencodeEncoder {
17+
val rootRef: BencodeEncoder
18+
override val serializersModule: SerializersModule
19+
get() = rootRef.serializersModule
20+
override val options: BencodeOptions
21+
get() = rootRef.options
22+
override fun encodeChar(value: Char) = encodeLong(value.code.toLong())
23+
override fun encodeByte(value: Byte) = encodeLong(value.toLong())
24+
override fun encodeShort(value: Short) = encodeLong(value.toLong())
25+
override fun encodeInt(value: Int) = encodeLong(value.toLong())
26+
override fun encodeFloat(value: Float) = options.floatStrategy.encodeFloat(value)
27+
override fun encodeDouble(value: Double) = options.doubleStrategy.encodeDouble(value)
28+
override fun encodeBoolean(value: Boolean) = reportError("boolean is not supported")
29+
30+
@OptIn(ExperimentalSerializationApi::class)
31+
override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) {
32+
encodeString(enumDescriptor.getElementName(index))
33+
}
34+
35+
override fun encodeBinaryString(strategy: BinaryStringStrategy, value: String) {
36+
options.binaryStringStrategy.encodeString(strategy, value)
37+
}
38+
39+
@ExperimentalSerializationApi
40+
override fun encodeNull() {
41+
reportError("null is not supported")
42+
}
43+
44+
override fun encodeInline(descriptor: SerialDescriptor): Encoder = this
45+
46+
override fun <T> encodeSerializableValue(serializer: SerializationStrategy<T>, value: T) {
47+
encodeValue(EncodeValue.Serialized(value, serializer))
48+
}
49+
50+
override fun encodeByteArray(bytes: ByteArray) {
51+
encodeValue(EncodeValue.Bytes(bytes))
52+
}
53+
54+
override fun encodeString(value: String) {
55+
encodeValue(EncodeValue.Text(value))
56+
}
57+
58+
override fun encodeLong(value: Long) {
59+
encodeValue(EncodeValue.Number(value))
60+
}
61+
62+
fun encodeValue(value: EncodeValue)
63+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package space.iseki.bencoding
2+
3+
import kotlinx.serialization.ExperimentalSerializationApi
4+
import kotlinx.serialization.descriptors.PrimitiveKind
5+
import kotlinx.serialization.descriptors.SerialDescriptor
6+
import kotlinx.serialization.descriptors.StructureKind
7+
import kotlinx.serialization.encoding.CompositeEncoder
8+
import kotlinx.serialization.modules.SerializersModule
9+
10+
@OptIn(ExperimentalSerializationApi::class)
11+
internal class BencodeEncoder0(
12+
override val serializersModule: SerializersModule,
13+
override val options: BencodeOptions,
14+
val writer: BWriter,
15+
) : EncoderDelegate {
16+
17+
override fun encodeValue(value: EncodeValue) {
18+
when (value) {
19+
is EncodeValue.Number -> writer.writeInt(value.v)
20+
is EncodeValue.Text -> writer.writeString(value.v)
21+
is EncodeValue.Bytes -> writer.writeByteArray(value.v)
22+
is EncodeValue.Serialized<*> -> value.doEncode(this)
23+
}
24+
}
25+
26+
private class Entry(val k: String, val v: EncodeValue)
27+
28+
internal inner class MapEncoder : CompositeEncoderDelegate {
29+
override val rootRef: BencodeEncoder
30+
get() = this@BencodeEncoder0
31+
private val values = arrayListOf<Entry>()
32+
private var key: String? = null
33+
34+
override fun encodeValueElement(descriptor: SerialDescriptor, index: Int, value: EncodeValue) {
35+
if (key == null) {
36+
// encode key
37+
when (value) {
38+
is EncodeValue.Text -> key = value.v
39+
is EncodeValue.Serialized<*> -> {
40+
if (value.serializer.descriptor.kind != PrimitiveKind.STRING) {
41+
reportError("key must be string", descriptor, index)
42+
}
43+
key = extractKeyStringFromSerializedValue(descriptor, index, value)
44+
}
45+
46+
else -> reportError("key must be string", descriptor, index)
47+
}
48+
return
49+
}
50+
val key = key ?: reportError("key hasn't encoded yet", descriptor, index)
51+
values.add(Entry(key, value))
52+
this.key = null
53+
}
54+
55+
override fun endStructure(descriptor: SerialDescriptor) {
56+
values.sortBy { it.k }
57+
for (it in values) {
58+
writer.writeString(it.k)
59+
this@BencodeEncoder0.encodeValue(it.v)
60+
}
61+
writer.writeEnd()
62+
}
63+
64+
private fun extractKeyStringFromSerializedValue(
65+
descriptor: SerialDescriptor, index: Int, value: EncodeValue.Serialized<*>
66+
): String {
67+
val encoder = MapKeyExtractorEncoder(this@BencodeEncoder0, descriptor, index)
68+
value.doEncode(encoder)
69+
return encoder.key ?: reportError("no key encoded")
70+
}
71+
}
72+
73+
internal inner class ClassEncoder : CompositeEncoderDelegate {
74+
override val rootRef: BencodeEncoder
75+
get() = this@BencodeEncoder0
76+
private val values = arrayListOf<Entry>()
77+
78+
override fun encodeValueElement(descriptor: SerialDescriptor, index: Int, value: EncodeValue) {
79+
values.add(Entry(descriptor.getElementName(index), value))
80+
}
81+
82+
override fun endStructure(descriptor: SerialDescriptor) {
83+
values.sortBy { it.k }
84+
for (it in values) {
85+
writer.writeString(it.k)
86+
this@BencodeEncoder0.encodeValue(it.v)
87+
}
88+
writer.writeEnd()
89+
}
90+
}
91+
92+
internal inner class ListEncoder : CompositeEncoderDelegate {
93+
override val rootRef: BencodeEncoder
94+
get() = this@BencodeEncoder0
95+
override val encodeToRootDirectly: Boolean
96+
get() = true
97+
98+
override fun encodeValueElement(descriptor: SerialDescriptor, index: Int, value: EncodeValue) {
99+
throw Error("unreachable")
100+
}
101+
102+
override fun endStructure(descriptor: SerialDescriptor) {
103+
writer.writeEnd()
104+
}
105+
106+
}
107+
108+
override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder {
109+
return when (val kind = descriptor.kind) {
110+
StructureKind.LIST -> ListEncoder().also { writer.writeListBegin() }
111+
StructureKind.OBJECT, StructureKind.CLASS -> ClassEncoder().also { writer.writeDictBegin() }
112+
StructureKind.MAP -> MapEncoder().also { writer.writeDictBegin() }
113+
else -> reportError("unsupported kind: $kind, in descriptor: ${descriptor.serialName}")
114+
}
115+
}
116+
117+
override val rootRef: BencodeEncoder
118+
get() = this
119+
120+
}
121+

src/commonMain/kotlin/space/iseki/bencoding/BinaryStringStrategy.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package space.iseki.bencoding
22

3+
import kotlinx.serialization.descriptors.SerialDescriptor
34
import kotlin.io.encoding.ExperimentalEncodingApi
45

56
/**
@@ -33,6 +34,28 @@ enum class BinaryStringStrategy {
3334
base64 = { kotlin.io.encoding.Base64.encode(decodeByteArray()) },
3435
)
3536

37+
context(BencodeEncoder)
38+
@OptIn(ExperimentalEncodingApi::class)
39+
internal fun encodeString(strategy: BinaryStringStrategy, value: String) {
40+
work(
41+
strategy = strategy,
42+
options = options,
43+
iso88591 = { encodeByteArray(string2BytesIso88591(value)) },
44+
base64 = { encodeByteArray(kotlin.io.encoding.Base64.decode(value)) },
45+
)
46+
}
47+
48+
context(BencodeCompositeEncoder)
49+
@OptIn(ExperimentalEncodingApi::class)
50+
internal fun encodeString(strategy: BinaryStringStrategy, descriptor: SerialDescriptor, index: Int, value: String) {
51+
work(
52+
strategy = strategy,
53+
options = options,
54+
iso88591 = { encodeByteArrayElement(descriptor, index, string2BytesIso88591(value)) },
55+
base64 = { encodeByteArrayElement(descriptor, index, kotlin.io.encoding.Base64.decode(value)) },
56+
)
57+
}
58+
3659
private inline fun <T> work(
3760
strategy: BinaryStringStrategy,
3861
options: BencodeOptions,

0 commit comments

Comments
 (0)