Refactoring TLV's code

This commit is contained in:
Vitor Pamplona
2023-08-15 11:13:01 -04:00
parent 52687a48df
commit 1ad1d233cd
5 changed files with 143 additions and 146 deletions

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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<Byte, List<ByteArray>>) {
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<Byte, List<ByteArray>> {
val result = mutableMapOf<Byte, MutableList<ByteArray>>()
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<Byte, MutableList<ByteArray>>()
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
}
}

View File

@@ -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() }
}

View File

@@ -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() }
}