mirror of
https://github.com/mollyim/monero-wallet-sdk.git
synced 2024-12-27 00:19:28 -05:00
lib: add base58 decoder
This commit is contained in:
parent
41050d28ed
commit
36be1d209a
70
lib/android/src/main/kotlin/im/molly/monero/util/Base58.kt
Normal file
70
lib/android/src/main/kotlin/im/molly/monero/util/Base58.kt
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
package im.molly.monero.util
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
|
||||||
|
const val ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
||||||
|
|
||||||
|
object Decoder {
|
||||||
|
private val decodingTable = IntArray(128) { ALPHABET.indexOf(it.toChar()) }
|
||||||
|
|
||||||
|
private val blockSizes = listOf(0, -1, 1, 2, -1, 3, 4, 5, -1, 6, 7, 8)
|
||||||
|
|
||||||
|
fun decode(input: ByteArray): ByteBuffer {
|
||||||
|
val size = input.size
|
||||||
|
val needed = 8 * (size / 11) + findOutputBlockSize(size % 11)
|
||||||
|
val out = ByteBuffer.allocate(needed)
|
||||||
|
var pos = 0
|
||||||
|
while (pos + 11 <= size) {
|
||||||
|
decodeBlock(input, pos, 11, out)
|
||||||
|
pos += 11
|
||||||
|
}
|
||||||
|
val remain = size - pos
|
||||||
|
if (remain > 0) {
|
||||||
|
decodeBlock(input, pos, remain, out)
|
||||||
|
}
|
||||||
|
out.flip()
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun decodeBlock(block: ByteArray, offset: Int, len: Int, out: ByteBuffer) {
|
||||||
|
val blockSize = findOutputBlockSize(len)
|
||||||
|
val newOutPos = out.position() + blockSize
|
||||||
|
|
||||||
|
var num = 0uL
|
||||||
|
var base = 1uL
|
||||||
|
var zeroes = 0
|
||||||
|
|
||||||
|
for (i in (offset + len - 1) downTo offset) {
|
||||||
|
val c = block[i].toInt()
|
||||||
|
val digit = decodingTable.getOrElse(c) { -1 }.toULong()
|
||||||
|
require(digit >= 0uL) { "Invalid symbol" }
|
||||||
|
if (digit == 0uL) {
|
||||||
|
zeroes++
|
||||||
|
} else {
|
||||||
|
while (zeroes > 0) {
|
||||||
|
base *= 58u
|
||||||
|
zeroes--
|
||||||
|
}
|
||||||
|
val prod = digit * base
|
||||||
|
val lastNum = num
|
||||||
|
num += prod
|
||||||
|
require((prod / base == digit) && (num > lastNum)) { "Overflow" }
|
||||||
|
base *= 58u // Never overflows, 58^10 < 2^64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (j in 1..blockSize) {
|
||||||
|
out.put(newOutPos - j, num.toByte())
|
||||||
|
num = num shr 8
|
||||||
|
}
|
||||||
|
require(num == 0uL) { "Overflow" }
|
||||||
|
out.position(newOutPos)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findOutputBlockSize(blockSize: Int): Int = blockSizes[blockSize].also {
|
||||||
|
require(it >= 0) { "Invalid block size" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.decodeBase58(): ByteArray =
|
||||||
|
Decoder.decode(this.toByteArray(Charset.defaultCharset())).array()
|
168
lib/android/src/test/kotlin/im/molly/monero/util/Base58Test.kt
Normal file
168
lib/android/src/test/kotlin/im/molly/monero/util/Base58Test.kt
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
package im.molly.monero.util
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class Base58Test {
|
||||||
|
|
||||||
|
// Test cases from monero unit_tests/base58.cpp
|
||||||
|
|
||||||
|
private val base58ToHex = mapOf(
|
||||||
|
// 2-bytes block
|
||||||
|
"11" to "00",
|
||||||
|
"1z" to "39",
|
||||||
|
"5Q" to "FF",
|
||||||
|
// 3-bytes block
|
||||||
|
"111" to "0000",
|
||||||
|
"11z" to "0039",
|
||||||
|
"15R" to "0100",
|
||||||
|
"LUv" to "FFFF",
|
||||||
|
// 5-bytes block
|
||||||
|
"11111" to "000000",
|
||||||
|
"1111z" to "000039",
|
||||||
|
"11LUw" to "010000",
|
||||||
|
"2UzHL" to "FFFFFF",
|
||||||
|
// 6-bytes block
|
||||||
|
"11111z" to "00000039",
|
||||||
|
"7YXq9G" to "FFFFFFFF",
|
||||||
|
// 7-bytes block
|
||||||
|
"111111z" to "0000000039",
|
||||||
|
"VtB5VXc" to "FFFFFFFFFF",
|
||||||
|
// 9-bytes block
|
||||||
|
"11111111z" to "000000000039",
|
||||||
|
"3CUsUpv9t" to "FFFFFFFFFFFF",
|
||||||
|
// 10-bytes block
|
||||||
|
"111111111z" to "00000000000039",
|
||||||
|
"Ahg1opVcGW" to "FFFFFFFFFFFFFF",
|
||||||
|
// 11-bytes block
|
||||||
|
"1111111111z" to "0000000000000039",
|
||||||
|
"jpXCZedGfVQ" to "FFFFFFFFFFFFFFFF",
|
||||||
|
"11111111111" to "0000000000000000",
|
||||||
|
"11111111112" to "0000000000000001",
|
||||||
|
"11111111119" to "0000000000000008",
|
||||||
|
"1111111111A" to "0000000000000009",
|
||||||
|
"11111111121" to "000000000000003A",
|
||||||
|
"1Ahg1opVcGW" to "00FFFFFFFFFFFFFF",
|
||||||
|
"22222222222" to "06156013762879F7",
|
||||||
|
"1z111111111" to "05E022BA374B2A00",
|
||||||
|
// Multiple blocks
|
||||||
|
"1111111111111" to "000000000000000000",
|
||||||
|
"11111111111111" to "00000000000000000000",
|
||||||
|
"1111111111111111" to "0000000000000000000000",
|
||||||
|
"11111111111111111" to "000000000000000000000000",
|
||||||
|
"111111111111111111" to "00000000000000000000000000",
|
||||||
|
"11111111111111111111" to "0000000000000000000000000000",
|
||||||
|
"111111111111111111111" to "000000000000000000000000000000",
|
||||||
|
"1111111111111111111111" to "00000000000000000000000000000000",
|
||||||
|
"jpXCZedGfVQ5Q" to "FFFFFFFFFFFFFFFFFF",
|
||||||
|
"jpXCZedGfVQLUv" to "FFFFFFFFFFFFFFFFFFFF",
|
||||||
|
"jpXCZedGfVQ2UzHL" to "FFFFFFFFFFFFFFFFFFFFFF",
|
||||||
|
"jpXCZedGfVQ7YXq9G" to "FFFFFFFFFFFFFFFFFFFFFFFF",
|
||||||
|
"22222222222VtB5VXc" to "06156013762879F7FFFFFFFFFF",
|
||||||
|
"jpXCZedGfVQVtB5VXc" to "FFFFFFFFFFFFFFFFFFFFFFFFFF",
|
||||||
|
"jpXCZedGfVQ3CUsUpv9t" to "FFFFFFFFFFFFFFFFFFFFFFFFFFFF",
|
||||||
|
"jpXCZedGfVQAhg1opVcGW" to "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
|
||||||
|
"jpXCZedGfVQjpXCZedGfVQ" to "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
|
||||||
|
)
|
||||||
|
|
||||||
|
private val overflows = listOf(
|
||||||
|
"5R",
|
||||||
|
"zz",
|
||||||
|
"LUw",
|
||||||
|
"zzz",
|
||||||
|
"2UzHM",
|
||||||
|
"zzzzz",
|
||||||
|
"7YXq9H",
|
||||||
|
"zzzzzz",
|
||||||
|
"VtB5VXd",
|
||||||
|
"zzzzzzz",
|
||||||
|
"3CUsUpv9u",
|
||||||
|
"zzzzzzzzz",
|
||||||
|
"Ahg1opVcGX",
|
||||||
|
"zzzzzzzzzz",
|
||||||
|
"jpXCZedGfVR",
|
||||||
|
"zzzzzzzzzzz",
|
||||||
|
"123456789AB5R",
|
||||||
|
"123456789ABzz",
|
||||||
|
"123456789ABLUw",
|
||||||
|
"123456789ABzzz",
|
||||||
|
"123456789AB2UzHM",
|
||||||
|
"123456789ABzzzzz",
|
||||||
|
"123456789AB7YXq9H",
|
||||||
|
"123456789ABzzzzzz",
|
||||||
|
"123456789ABVtB5VXd",
|
||||||
|
"123456789ABzzzzzzz",
|
||||||
|
"123456789AB3CUsUpv9u",
|
||||||
|
"123456789ABzzzzzzzzz",
|
||||||
|
"123456789ABAhg1opVcGX",
|
||||||
|
"123456789ABzzzzzzzzzz",
|
||||||
|
"123456789ABjpXCZedGfVR",
|
||||||
|
"123456789ABzzzzzzzzzzz",
|
||||||
|
"zzzzzzzzzzz11",
|
||||||
|
)
|
||||||
|
|
||||||
|
private val invalidSymbols = listOf(
|
||||||
|
"10",
|
||||||
|
"11I",
|
||||||
|
"11O11",
|
||||||
|
"11l111",
|
||||||
|
"11_11111111",
|
||||||
|
"1101111111111",
|
||||||
|
"11I11111111111111",
|
||||||
|
"11O1111111111111111111",
|
||||||
|
"1111111111110",
|
||||||
|
"111111111111l1111",
|
||||||
|
"111111111111_111111111",
|
||||||
|
)
|
||||||
|
|
||||||
|
private val invalidLengths = listOf(
|
||||||
|
"1",
|
||||||
|
"z",
|
||||||
|
"1111",
|
||||||
|
"zzzz",
|
||||||
|
"11111111",
|
||||||
|
"zzzzzzzz",
|
||||||
|
"123456789AB1",
|
||||||
|
"123456789ABz",
|
||||||
|
"123456789AB1111",
|
||||||
|
"123456789ABzzzz",
|
||||||
|
"123456789AB11111111",
|
||||||
|
"123456789ABzzzzzzzz",
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `decode valid base58 strings`() {
|
||||||
|
base58ToHex.forEach { (input, expected) ->
|
||||||
|
assertThat(input.decodeBase58()).isEqualTo(expected.parseHex())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `empty string decodes to zero length`() {
|
||||||
|
assertThat("".decodeBase58()).hasLength(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `error on overflows`() {
|
||||||
|
overflows.forEach {
|
||||||
|
val thrown =
|
||||||
|
Assert.assertThrows(IllegalArgumentException::class.java) { it.decodeBase58() }
|
||||||
|
assertThat(thrown.message).ignoringCase().contains("overflow")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `error decoding invalid lengths`() {
|
||||||
|
invalidLengths.forEach {
|
||||||
|
Assert.assertThrows(IllegalArgumentException::class.java) { it.decodeBase58() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `error decoding invalid symbols`() {
|
||||||
|
invalidSymbols.forEach {
|
||||||
|
Assert.assertThrows(IllegalArgumentException::class.java) { it.decodeBase58() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user