From 1ad1d233cd7dcbaab1421cd6136314e3d9b76c95 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 15 Aug 2023 11:13:01 -0400 Subject: [PATCH] Refactoring TLV's code --- .../amethyst/service/model/ATag.kt | 35 +++---- .../amethyst/service/nip19/Nip19.kt | 87 ++++-------------- .../amethyst/service/nip19/Tlv.kt | 92 +++++++++++++++---- .../amethyst/service/TlvIntegerTest.kt | 40 ++++++++ .../vitorpamplona/amethyst/service/TlvTest.kt | 35 ------- 5 files changed, 143 insertions(+), 146 deletions(-) create mode 100644 app/src/test/java/com/vitorpamplona/amethyst/service/TlvIntegerTest.kt delete mode 100644 app/src/test/java/com/vitorpamplona/amethyst/service/TlvTest.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ATag.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ATag.kt index 970faa9b9..9c66b8729 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ATag.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ATag.kt @@ -2,11 +2,9 @@ package com.vitorpamplona.amethyst.service.model import android.util.Log import androidx.compose.runtime.Immutable -import com.vitorpamplona.amethyst.model.hexToByteArray -import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.service.bechToBytes import com.vitorpamplona.amethyst.service.nip19.Tlv -import com.vitorpamplona.amethyst.service.nip19.toByteArray +import com.vitorpamplona.amethyst.service.nip19.TlvBuilder import com.vitorpamplona.amethyst.service.toNAddress import fr.acinq.secp256k1.Hex @@ -15,22 +13,12 @@ data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String, val rela fun toTag() = "$kind:$pubKeyHex:$dTag" fun toNAddr(): String { - val kind = kind.toByteArray() - val author = pubKeyHex.hexToByteArray() - val dTag = dTag.toByteArray(Charsets.UTF_8) - val relay = relay?.toByteArray(Charsets.UTF_8) - - var fullArray = byteArrayOf(Tlv.Type.SPECIAL.id, dTag.size.toByte()) + dTag - - if (relay != null) { - fullArray = fullArray + byteArrayOf(Tlv.Type.RELAY.id, relay.size.toByte()) + relay - } - - fullArray = fullArray + - byteArrayOf(Tlv.Type.AUTHOR.id, author.size.toByte()) + author + - byteArrayOf(Tlv.Type.KIND.id, kind.size.toByte()) + kind - - return fullArray.toNAddress() + return TlvBuilder().apply { + addString(Tlv.Type.SPECIAL, dTag) + addStringIfNotNull(Tlv.Type.RELAY, relay) + addHex(Tlv.Type.AUTHOR, pubKeyHex) + addInt(Tlv.Type.KIND, kind) + }.build().toNAddress() } companion object { @@ -63,10 +51,11 @@ data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String, val rela if (key.startsWith("naddr")) { val tlv = Tlv.parse(key.bechToBytes()) - val d = tlv.get(Tlv.Type.SPECIAL.id)?.get(0)?.toString(Charsets.UTF_8) ?: "" - val relay = tlv.get(Tlv.Type.RELAY.id)?.get(0)?.toString(Charsets.UTF_8) - val author = tlv.get(Tlv.Type.AUTHOR.id)?.get(0)?.toHexKey() - val kind = tlv.get(Tlv.Type.KIND.id)?.get(0)?.let { Tlv.toInt32(it) } + + val d = tlv.firstAsString(Tlv.Type.SPECIAL) ?: "" + val relay = tlv.firstAsString(Tlv.Type.RELAY) + val author = tlv.firstAsHex(Tlv.Type.AUTHOR) + val kind = tlv.firstAsInt(Tlv.Type.KIND) if (kind != null && author != null) { return ATag(kind, author, d, relay) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/nip19/Nip19.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/nip19/Nip19.kt index 39a5b0cb0..937e99b01 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/nip19/Nip19.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/nip19/Nip19.kt @@ -2,7 +2,6 @@ package com.vitorpamplona.amethyst.service.nip19 import android.util.Log import androidx.compose.runtime.Immutable -import com.vitorpamplona.amethyst.model.hexToByteArray import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.service.bechToBytes import com.vitorpamplona.amethyst.service.toNEvent @@ -82,13 +81,8 @@ object Nip19 { private fun nprofile(bytes: ByteArray): Return? { val tlv = Tlv.parse(bytes) - val hex = tlv.get(Tlv.Type.SPECIAL.id) - ?.get(0) - ?.toHexKey() ?: return null - - val relay = tlv.get(Tlv.Type.RELAY.id) - ?.get(0) - ?.toString(Charsets.UTF_8) + val hex = tlv.firstAsHex(Tlv.Type.SPECIAL) ?: return null + val relay = tlv.firstAsString(Tlv.Type.RELAY) return Return(Type.USER, hex, relay) } @@ -96,30 +90,16 @@ object Nip19 { private fun nevent(bytes: ByteArray): Return? { val tlv = Tlv.parse(bytes) - val hex = tlv.get(Tlv.Type.SPECIAL.id) - ?.get(0) - ?.toHexKey() ?: return null - - val relay = tlv.get(Tlv.Type.RELAY.id) - ?.get(0) - ?.toString(Charsets.UTF_8) - - val author = tlv.get(Tlv.Type.AUTHOR.id) - ?.get(0) - ?.toHexKey() - - val kind = tlv.get(Tlv.Type.KIND.id) - ?.get(0) - ?.let { Tlv.toInt32(it) } + val hex = tlv.firstAsHex(Tlv.Type.SPECIAL) ?: return null + val relay = tlv.firstAsString(Tlv.Type.RELAY) + val author = tlv.firstAsHex(Tlv.Type.AUTHOR) + val kind = tlv.firstAsInt(Tlv.Type.KIND.id) return Return(Type.EVENT, hex, relay, author, kind) } private fun nrelay(bytes: ByteArray): Return? { - val relayUrl = Tlv.parse(bytes) - .get(Tlv.Type.SPECIAL.id) - ?.get(0) - ?.toString(Charsets.UTF_8) ?: return null + val relayUrl = Tlv.parse(bytes).firstAsString(Tlv.Type.SPECIAL.id) ?: return null return Return(Type.RELAY, relayUrl) } @@ -127,53 +107,20 @@ object Nip19 { private fun naddr(bytes: ByteArray): Return? { val tlv = Tlv.parse(bytes) - val d = tlv.get(Tlv.Type.SPECIAL.id) - ?.get(0) - ?.toString(Charsets.UTF_8) ?: return null - - val relay = tlv.get(Tlv.Type.RELAY.id) - ?.get(0) - ?.toString(Charsets.UTF_8) - - val author = tlv.get(Tlv.Type.AUTHOR.id) - ?.get(0) - ?.toHexKey() - - val kind = tlv.get(Tlv.Type.KIND.id) - ?.get(0) - ?.let { Tlv.toInt32(it) } + val d = tlv.firstAsString(Tlv.Type.SPECIAL.id) ?: "" + val relay = tlv.firstAsString(Tlv.Type.RELAY.id) + val author = tlv.firstAsHex(Tlv.Type.AUTHOR.id) ?: return null + val kind = tlv.firstAsInt(Tlv.Type.KIND.id) ?: return null return Return(Type.ADDRESS, "$kind:$author:$d", relay, author, kind) } public fun createNEvent(idHex: String, author: String?, kind: Int?, relay: String?): String { - val kind = kind?.toByteArray() - val author = author?.hexToByteArray() - val idHex = idHex.hexToByteArray() - val relay = relay?.toByteArray(Charsets.UTF_8) - - var fullArray = byteArrayOf(Tlv.Type.SPECIAL.id, idHex.size.toByte()) + idHex - - if (relay != null) { - fullArray = fullArray + byteArrayOf(Tlv.Type.RELAY.id, relay.size.toByte()) + relay - } - - if (author != null) { - fullArray = fullArray + byteArrayOf(Tlv.Type.AUTHOR.id, author.size.toByte()) + author - } - - if (kind != null) { - fullArray = fullArray + byteArrayOf(Tlv.Type.KIND.id, kind.size.toByte()) + kind - } - - return fullArray.toNEvent() + return TlvBuilder().apply { + addHex(Tlv.Type.SPECIAL, idHex) + addStringIfNotNull(Tlv.Type.RELAY, relay) + addHexIfNotNull(Tlv.Type.AUTHOR, author) + addIntIfNotNull(Tlv.Type.KIND, kind) + }.build().toNEvent() } } - -fun Int.toByteArray(): ByteArray { - val bytes = ByteArray(4) - (0..3).forEach { - bytes[3 - it] = ((this ushr (8 * it)) and 0xFFFF).toByte() - } - return bytes -} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/nip19/Tlv.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/nip19/Tlv.kt index 83bcc0c5e..eca8ad968 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/nip19/Tlv.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/nip19/Tlv.kt @@ -1,9 +1,67 @@ package com.vitorpamplona.amethyst.service.nip19 +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.hexToByteArray +import com.vitorpamplona.amethyst.model.toHexKey +import java.io.ByteArrayOutputStream import java.nio.ByteBuffer import java.nio.ByteOrder -object Tlv { +class TlvBuilder() { + val outputStream = ByteArrayOutputStream() + + private fun add(type: Byte, byteArray: ByteArray) { + outputStream.write(byteArrayOf(type, byteArray.size.toByte())) + outputStream.write(byteArray) + } + + fun addString(type: Byte, string: String) = add(type, string.toByteArray(Charsets.UTF_8)) + fun addHex(type: Byte, key: HexKey) = add(type, key.hexToByteArray()) + fun addInt(type: Byte, data: Int) = add(type, data.toByteArray()) + + fun addStringIfNotNull(type: Byte, data: String?) = data?.let { addString(type, it) } + fun addHexIfNotNull(type: Byte, data: HexKey?) = data?.let { addHex(type, it) } + fun addIntIfNotNull(type: Byte, data: Int?) = data?.let { addInt(type, it) } + + fun addString(type: Tlv.Type, string: String) = addString(type.id, string) + fun addHex(type: Tlv.Type, key: HexKey) = addHex(type.id, key) + fun addInt(type: Tlv.Type, data: Int) = addInt(type.id, data) + + fun addStringIfNotNull(type: Tlv.Type, data: String?) = addStringIfNotNull(type.id, data) + fun addHexIfNotNull(type: Tlv.Type, data: HexKey?) = addHexIfNotNull(type.id, data) + fun addIntIfNotNull(type: Tlv.Type, data: Int?) = addIntIfNotNull(type.id, data) + + fun build(): ByteArray { + return outputStream.toByteArray() + } +} + +fun Int.toByteArray(): ByteArray { + val bytes = ByteArray(4) + (0..3).forEach { + bytes[3 - it] = ((this ushr (8 * it)) and 0xFFFF).toByte() + } + return bytes +} + +fun ByteArray.toInt32(): Int? { + if (size != 4) return null + return ByteBuffer.wrap(this, 0, 4).order(ByteOrder.BIG_ENDIAN).int +} + +class Tlv(val data: Map>) { + fun asInt(type: Byte) = data[type]?.mapNotNull { it.toInt32() } + fun asHex(type: Byte) = data[type]?.map { it.toHexKey().intern() } + fun asString(type: Byte) = data[type]?.map { it.toString(Charsets.UTF_8) } + + fun firstAsInt(type: Byte) = data[type]?.firstOrNull()?.toInt32() + fun firstAsHex(type: Byte) = data[type]?.firstOrNull()?.toHexKey()?.intern() + fun firstAsString(type: Byte) = data[type]?.firstOrNull()?.toString(Charsets.UTF_8) + + fun firstAsInt(type: Type) = firstAsInt(type.id) + fun firstAsHex(type: Type) = firstAsHex(type.id) + fun firstAsString(type: Type) = firstAsString(type.id) + enum class Type(val id: Byte) { SPECIAL(0), RELAY(1), @@ -11,26 +69,24 @@ object Tlv { KIND(3); } - fun toInt32(bytes: ByteArray): Int { - require(bytes.size == 4) { "length must be 4, got: ${bytes.size}" } - return ByteBuffer.wrap(bytes, 0, 4).order(ByteOrder.BIG_ENDIAN).int - } + companion object { - fun parse(data: ByteArray): Map> { - val result = mutableMapOf>() - var rest = data - while (rest.isNotEmpty()) { - val t = rest[0] - val l = rest[1].toUByte().toInt() - val v = rest.sliceArray(IntRange(2, (2 + l) - 1)) - rest = rest.sliceArray(IntRange(2 + l, rest.size - 1)) - if (v.size < l) continue + fun parse(data: ByteArray): Tlv { + val result = mutableMapOf>() + var rest = data + while (rest.isNotEmpty()) { + val t = rest[0] + val l = rest[1].toUByte().toInt() + val v = rest.sliceArray(IntRange(2, (2 + l) - 1)) + rest = rest.sliceArray(IntRange(2 + l, rest.size - 1)) + if (v.size < l) continue - if (!result.containsKey(t)) { - result[t] = mutableListOf() + if (!result.containsKey(t)) { + result[t] = mutableListOf() + } + result[t]?.add(v) } - result[t]?.add(v) + return Tlv(result) } - return result } } diff --git a/app/src/test/java/com/vitorpamplona/amethyst/service/TlvIntegerTest.kt b/app/src/test/java/com/vitorpamplona/amethyst/service/TlvIntegerTest.kt new file mode 100644 index 000000000..cf847a33d --- /dev/null +++ b/app/src/test/java/com/vitorpamplona/amethyst/service/TlvIntegerTest.kt @@ -0,0 +1,40 @@ +package com.vitorpamplona.amethyst.service + +import com.vitorpamplona.amethyst.service.nip19.toByteArray +import com.vitorpamplona.amethyst.service.nip19.toInt32 +import org.junit.Assert +import org.junit.Assert.assertEquals +import org.junit.Test + +class TlvIntegerTest { + fun to_int_32_length_smaller_than_4() { + Assert.assertNull(byteArrayOfInts(1, 2, 3).toInt32()) + } + + fun to_int_32_length_bigger_than_4() { + Assert.assertNull(byteArrayOfInts(1, 2, 3, 4, 5).toInt32()) + } + + @Test() + fun to_int_32_length_4() { + val actual = byteArrayOfInts(1, 2, 3, 4).toInt32() + + assertEquals(16909060, actual) + } + + @Test() + fun backAndForth() { + assertEquals(234, 234.toByteArray().toInt32()) + assertEquals(1, 1.toByteArray().toInt32()) + assertEquals(0, 0.toByteArray().toInt32()) + assertEquals(1000, 1000.toByteArray().toInt32()) + + assertEquals(-234, (-234).toByteArray().toInt32()) + assertEquals(-1, (-1).toByteArray().toInt32()) + assertEquals(-0, (-0).toByteArray().toInt32()) + assertEquals(-1000, (-1000).toByteArray().toInt32()) + } + + private fun byteArrayOfInts(vararg ints: Int) = + ByteArray(ints.size) { pos -> ints[pos].toByte() } +} diff --git a/app/src/test/java/com/vitorpamplona/amethyst/service/TlvTest.kt b/app/src/test/java/com/vitorpamplona/amethyst/service/TlvTest.kt deleted file mode 100644 index 9f9087036..000000000 --- a/app/src/test/java/com/vitorpamplona/amethyst/service/TlvTest.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.vitorpamplona.amethyst.service - -import com.vitorpamplona.amethyst.service.nip19.Tlv -import org.junit.Assert -import org.junit.Ignore -import org.junit.Test - -class TlvTest { - - @Test(expected = IllegalArgumentException::class) - fun to_int_32_length_smaller_than_4() { - Tlv.toInt32(byteArrayOfInts(1, 2, 3)) - } - - @Test(expected = IllegalArgumentException::class) - fun to_int_32_length_bigger_than_4() { - Tlv.toInt32(byteArrayOfInts(1, 2, 3, 4, 5)) - } - - @Test() - fun to_int_32_length_4() { - val actual = Tlv.toInt32(byteArrayOfInts(1, 2, 3, 4)) - - Assert.assertEquals(16909060, actual) - } - - @Ignore("Test not implemented yet") - @Test() - fun parse_TLV() { - // TODO: I don't know how to test this (?) - } - - private fun byteArrayOfInts(vararg ints: Int) = - ByteArray(ints.size) { pos -> ints[pos].toByte() } -}