mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-02 08:23:00 +01:00
Add underpinnings to allow for local plaintext export.
Co-authored-by: Cody Henthorne <cody@signal.org>
This commit is contained in:
committed by
Cody Henthorne
parent
b605148ac4
commit
f2e4881026
@@ -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())
|
||||
}
|
||||
}
|
||||
109
core/util-jvm/src/test/java/org/signal/core/util/TestMessage.kt
Normal file
109
core/util-jvm/src/test/java/org/signal/core/util/TestMessage.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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\""))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user