Add underpinnings to allow for local plaintext export.

Co-authored-by: Cody Henthorne <cody@signal.org>
This commit is contained in:
Alex Hart
2026-03-25 16:44:26 -03:00
committed by Cody Henthorne
parent b605148ac4
commit f2e4881026
15 changed files with 859 additions and 42 deletions

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.core.util
import com.squareup.wire.Message
import com.squareup.wire.WireEnum
import com.squareup.wire.WireField
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import okio.ByteString
fun Message<*, *>.toJson(): String {
return Json.encodeToString(JsonElement.serializer(), toJsonElement())
}
private fun Message<*, *>.toJsonElement(): JsonObject {
val map = mutableMapOf<String, JsonElement>()
for (field in this.javaClass.declaredFields) {
field.getAnnotation(WireField::class.java) ?: continue
field.isAccessible = true
val value = field.get(this)
map[field.name] = valueToJsonElement(value)
}
return JsonObject(map)
}
private fun valueToJsonElement(value: Any?): JsonElement {
return when (value) {
null -> JsonNull
is Message<*, *> -> value.toJsonElement()
is WireEnum -> JsonPrimitive((value as Enum<*>).name)
is ByteString -> JsonPrimitive(value.base64())
is String -> JsonPrimitive(value)
is Boolean -> JsonPrimitive(value)
is Int -> JsonPrimitive(value)
is Long -> JsonPrimitive(value)
is Float -> JsonPrimitive(value)
is Double -> JsonPrimitive(value)
is List<*> -> JsonArray(value.map { valueToJsonElement(it) })
else -> JsonPrimitive(value.toString())
}
}

View File

@@ -0,0 +1,109 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.core.util
import com.squareup.wire.EnumAdapter
import com.squareup.wire.FieldEncoding
import com.squareup.wire.Message
import com.squareup.wire.ProtoAdapter
import com.squareup.wire.ProtoReader
import com.squareup.wire.ProtoWriter
import com.squareup.wire.ReverseProtoWriter
import com.squareup.wire.Syntax
import com.squareup.wire.WireEnum
import com.squareup.wire.WireField
import okio.ByteString
import kotlin.jvm.JvmField
class TestMessage(
@field:WireField(tag = 1, adapter = "com.squareup.wire.ProtoAdapter#STRING")
@JvmField
val name: String = "",
@field:WireField(tag = 2, adapter = "com.squareup.wire.ProtoAdapter#INT64")
@JvmField
val id: Long = 0L,
@field:WireField(tag = 3, adapter = "com.squareup.wire.ProtoAdapter#BYTES")
@JvmField
val data: ByteString = ByteString.EMPTY,
@field:WireField(tag = 4, adapter = "org.signal.core.util.TestMessage${'$'}TestEnum#ADAPTER")
@JvmField
val status: TestEnum? = null,
@field:WireField(tag = 5, adapter = "org.signal.core.util.TestMessage${'$'}Nested#ADAPTER")
@JvmField
val nested: Nested? = null,
@field:WireField(tag = 6, adapter = "com.squareup.wire.ProtoAdapter#STRING", label = WireField.Label.REPEATED)
@JvmField
val tags: List<String> = emptyList(),
unknownFields: ByteString = ByteString.EMPTY
) : Message<TestMessage, Nothing>(ADAPTER, unknownFields) {
override fun newBuilder(): Nothing = throw UnsupportedOperationException()
enum class TestEnum(override val value: Int) : WireEnum {
FIRST(0),
SECOND(1);
companion object {
@JvmField
val ADAPTER: ProtoAdapter<TestEnum> = object : EnumAdapter<TestEnum>(TestEnum::class, Syntax.PROTO_3, FIRST) {
override fun fromValue(value: Int): TestEnum? = entries.find { it.value == value }
}
}
}
class Nested(
@field:WireField(tag = 1, adapter = "com.squareup.wire.ProtoAdapter#STRING")
@JvmField
val label: String = "",
unknownFields: ByteString = ByteString.EMPTY
) : Message<Nested, Nothing>(ADAPTER, unknownFields) {
override fun newBuilder(): Nothing = throw UnsupportedOperationException()
companion object {
@JvmField
val ADAPTER: ProtoAdapter<Nested> = object : ProtoAdapter<Nested>(
FieldEncoding.LENGTH_DELIMITED,
Nested::class,
null,
Syntax.PROTO_3,
null,
null
) {
override fun encodedSize(value: Nested): Int = 0
override fun encode(writer: ProtoWriter, value: Nested) = Unit
override fun encode(writer: ReverseProtoWriter, value: Nested) = Unit
override fun decode(reader: ProtoReader): Nested = Nested()
override fun redact(value: Nested): Nested = value
}
}
}
companion object {
@JvmField
val ADAPTER: ProtoAdapter<TestMessage> = object : ProtoAdapter<TestMessage>(
FieldEncoding.LENGTH_DELIMITED,
TestMessage::class,
null,
Syntax.PROTO_3,
null,
null
) {
override fun encodedSize(value: TestMessage): Int = 0
override fun encode(writer: ProtoWriter, value: TestMessage) = Unit
override fun encode(writer: ReverseProtoWriter, value: TestMessage) = Unit
override fun decode(reader: ProtoReader): TestMessage = TestMessage()
override fun redact(value: TestMessage): TestMessage = value
}
}
}

View File

@@ -0,0 +1,121 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.core.util
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long
import okio.ByteString.Companion.encodeUtf8
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class WireProtoJsonTest {
@Test
fun `basic string and int64 fields serialize correctly`() {
val message = TestMessage(name = "alice", id = 42L)
val json = message.toJson()
val obj = Json.parseToJsonElement(json).jsonObject
assertEquals("alice", obj["name"]!!.jsonPrimitive.content)
assertEquals(42L, obj["id"]!!.jsonPrimitive.long)
}
@Test
fun `ByteString field serializes as base64`() {
val bytes = "hello".encodeUtf8()
val message = TestMessage(data = bytes)
val json = message.toJson()
val obj = Json.parseToJsonElement(json).jsonObject
assertEquals(bytes.base64(), obj["data"]!!.jsonPrimitive.content)
}
@Test
fun `enum field serializes as name`() {
val message = TestMessage(status = TestMessage.TestEnum.SECOND)
val json = message.toJson()
val obj = Json.parseToJsonElement(json).jsonObject
assertEquals("SECOND", obj["status"]!!.jsonPrimitive.content)
}
@Test
fun `nested message serializes as JSON object`() {
val message = TestMessage(nested = TestMessage.Nested(label = "inner"))
val json = message.toJson()
val obj = Json.parseToJsonElement(json).jsonObject
val nested = obj["nested"]!!.jsonObject
assertEquals("inner", nested["label"]!!.jsonPrimitive.content)
}
@Test
fun `repeated field serializes as JSON array`() {
val message = TestMessage(tags = listOf("a", "b", "c"))
val json = message.toJson()
val obj = Json.parseToJsonElement(json).jsonObject
val tags = obj["tags"]!!.jsonArray
assertEquals(3, tags.size)
assertEquals("a", tags[0].jsonPrimitive.content)
assertEquals("b", tags[1].jsonPrimitive.content)
assertEquals("c", tags[2].jsonPrimitive.content)
}
@Test
fun `default values produce sensible output`() {
val message = TestMessage()
val json = message.toJson()
val obj = Json.parseToJsonElement(json).jsonObject
assertEquals("", obj["name"]!!.jsonPrimitive.content)
assertEquals(0L, obj["id"]!!.jsonPrimitive.long)
}
@Test
fun `null optional fields serialize as null`() {
val message = TestMessage()
val json = message.toJson()
val obj = Json.parseToJsonElement(json).jsonObject
assertTrue(obj["nested"] is JsonNull)
assertTrue(obj["status"] is JsonNull)
}
@Test
fun `round trip produces valid parseable JSON`() {
val message = TestMessage(
name = "test",
id = 100L,
data = "bytes".encodeUtf8(),
status = TestMessage.TestEnum.FIRST,
nested = TestMessage.Nested(label = "deep"),
tags = listOf("x", "y")
)
val json = message.toJson()
val obj = Json.parseToJsonElement(json).jsonObject
assertEquals("test", obj["name"]!!.jsonPrimitive.content)
assertEquals(100L, obj["id"]!!.jsonPrimitive.long)
assertEquals("deep", obj["nested"]!!.jsonObject["label"]!!.jsonPrimitive.content)
assertEquals(2, obj["tags"]!!.jsonArray.size)
}
@Test
fun `unknownFields and adapter are excluded from output`() {
val message = TestMessage(name = "test")
val json = message.toJson()
assertFalse(json.contains("unknownFields"))
assertFalse(json.contains("\"adapter\""))
}
}