add legacy (ur1.0) encoding and decoding

This commit is contained in:
Craig Raw 2020-11-16 13:23:51 +02:00
parent a263d00e03
commit 71300aa97d
6 changed files with 556 additions and 0 deletions

View file

@ -0,0 +1,171 @@
package com.sparrowwallet.hummingbird;
/*
* Copyright 2018 Coinomi Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class BC32 {
/**
* The BC32 character set for encoding.
*/
private static final String CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
/**
* Find the polynomial with value coefficients mod the generator as 30-bit.
*/
private static int polymod(final byte[] values) {
int c = 1;
for (byte v_i : values) {
int c0 = (c >>> 25) & 0xff;
c = ((c & 0x1ffffff) << 5) ^ (v_i & 0xff);
if ((c0 & 1) != 0) c ^= 0x3b6a57b2;
if ((c0 & 2) != 0) c ^= 0x26508e6d;
if ((c0 & 4) != 0) c ^= 0x1ea119fa;
if ((c0 & 8) != 0) c ^= 0x3d4233dd;
if ((c0 & 16) != 0) c ^= 0x2a1462b3;
}
return c;
}
/**
* Verify a checksum.
*/
private static boolean verifyChecksum(final byte[] values) {
byte[] combined = new byte[1 + values.length];
System.arraycopy(new byte[]{0}, 0, combined, 0, 1);
System.arraycopy(values, 0, combined, 1, values.length);
return polymod(combined) == 0x3fffffff;
}
/**
* Create a checksum.
*/
private static byte[] createChecksum(final byte[] values) {
byte[] enc = new byte[1 + values.length + 6];
System.arraycopy(values, 0, enc, 1, values.length);
int mod = polymod(enc) ^ 0x3fffffff;
byte[] result = new byte[6];
for (int i = 0; i < 6; ++i) {
result[i] = (byte) ((mod >>> (5 * (5 - i))) & 31);
}
return result;
}
/**
* Encode a BC32 string.
*/
public static String encode(final byte[] values) {
List<Byte> boxedList = new ArrayList<>(values.length);
for(byte value : values) {
boxedList.add(value);
}
byte[] data = convertBits(boxedList, 8, 5, true);
byte[] checksum = createChecksum(data);
byte[] combined = new byte[data.length + checksum.length];
System.arraycopy(data, 0, combined, 0, data.length);
System.arraycopy(checksum, 0, combined, data.length, checksum.length);
StringBuilder sb = new StringBuilder(combined.length);
for (byte b : combined) {
sb.append(CHARSET.charAt(b));
}
return sb.toString();
}
/**
* Decode a BC32 string.
*/
public static byte[] decode(final String str) {
boolean lower = false, upper = false;
if (str.length() < 6)
throw new BC32DecoderException.InvalidDataLength("Input too short: " + str.length());
for (int i = 0; i < str.length(); ++i) {
char c = str.charAt(i);
if (c < 33 || c > 126) throw new BC32DecoderException.InvalidCharacter(c, i);
if (c >= 'a' && c <= 'z') {
if (upper)
throw new BC32DecoderException.InvalidCharacter(c, i);
lower = true;
}
if (c >= 'A' && c <= 'Z') {
if (lower)
throw new BC32DecoderException.InvalidCharacter(c, i);
upper = true;
}
}
final int dataPartLength = str.length();
byte[] values = new byte[dataPartLength];
for (int i = 0; i < dataPartLength; ++i) {
char c = str.charAt(i);
if(CHARSET.indexOf(c) == -1) {
throw new IllegalArgumentException("BC32 characters out of range");
}
values[i] = (byte) CHARSET.indexOf(c);
}
if (!verifyChecksum(values)) throw new BC32DecoderException.InvalidChecksum();
byte[] data = Arrays.copyOfRange(values, 0, values.length - 6);
List<Byte> valueList = new ArrayList<>();
for (byte b : data) {
valueList.add(b);
}
return convertBits(valueList, 5, 8, false);
}
private static byte[] convertBits(List<Byte> data, int fromBits, int toBits, boolean pad) {
int acc = 0;
int bits = 0;
int maxv = (1 << toBits) - 1;
List<Byte> ret = new ArrayList<>();
for (Byte value : data) {
short b = (short) (value & 0xff);
if ((b >> fromBits) > 0) {
throw new IllegalArgumentException("Illegal bytes for bc32 encoding");
}
acc = (acc << fromBits) | b;
bits += fromBits;
while (bits >= toBits) {
bits -= toBits;
ret.add((byte) ((acc >> bits) & maxv));
}
}
if (pad && (bits > 0)) {
ret.add((byte) ((acc << (toBits - bits)) & maxv));
} else if (bits >= fromBits || (byte) (((acc << (toBits - bits)) & maxv)) != 0) {
throw new IllegalArgumentException("Illegal bytes for bc32 encoding");
}
Object[] boxedArray = ret.toArray();
int len = boxedArray.length;
byte[] array = new byte[len];
for (int i = 0; i < len; i++) {
array[i] = ((Number)boxedArray[i]).byteValue();
}
return array;
}
}

View file

@ -0,0 +1,32 @@
package com.sparrowwallet.hummingbird;
public class BC32DecoderException extends IllegalArgumentException {
public BC32DecoderException(String message) {
super(message);
}
public static class InvalidCharacter extends BC32DecoderException {
public final char character;
public final int position;
public InvalidCharacter(char character, int position) {
super("Invalid character '" + character + "' at position " + position);
this.character = character;
this.position = position;
}
}
public static class InvalidDataLength extends BC32DecoderException {
public InvalidDataLength(String message) {
super(message);
}
}
public static class InvalidChecksum extends BC32DecoderException {
public InvalidChecksum() {
super("Checksum does not validate");
}
}
}

View file

@ -0,0 +1,146 @@
package com.sparrowwallet.hummingbird;
import java.security.MessageDigest;
import java.util.*;
public class LegacyURDecoder {
private final Set<String> fragments = new LinkedHashSet<>();
public void receivePart(String fragment) {
fragments.add(fragment);
}
public boolean isComplete() {
if(fragments.isEmpty()) {
return false;
}
String fragment = fragments.iterator().next();
String[] components = fragment.split("/");
if(components.length > 3) {
int[] sequence = checkAndGetSequence(components[1]);
int total = sequence[1];
return total == fragments.size();
}
return true;
}
public static boolean isLegacyURFragment(String fragment) {
String[] components = fragment.split("/");
//Multi-part legacy encoding may include digest which adds an extra component
if(components.length > 3) {
return true;
}
//Last component is always fragment payload in both legacy and current
String payload = components[components.length-1];
//BC32 will never contain the following characters
if(payload.indexOf('b') > -1 || payload.indexOf('i') > -1 || payload.indexOf('o') > -1) {
return false;
}
//Bytewords and BC32 strings can usually be distinguished because the latter includes digits as permissible characters
return (payload.matches(".*\\d.*"));
}
public UR decode() throws UR.InvalidTypeException {
return decode(fragments.toArray(new String[0]));
}
public static UR decode(String[] fragments) throws UR.InvalidTypeException {
int length = fragments.length;
if(length == 1){
return handleFragment(fragments[0]);
}
else {
return handleFragments(fragments);
}
}
private static UR handleFragment(String fragment) throws UR.InvalidTypeException {
String[] components = fragment.split("/");
switch(components.length) {
case 2 -> {
return new UR(components[0], BC32.decode(components[1]));
}
case 3 -> {
String digest = components[1];
String data = components[2];
checkDigest(data, digest);
return new UR(components[0], BC32.decode(data));
}
case 4 -> {
checkAndGetSequence(components[1]);
String digest = components[2];
String data = components[3];
checkDigest(digest, fragment);
return new UR(components[0], BC32.decode(data));
}
default -> throw new IllegalArgumentException("Invalid number of fragments: expected 2 / 3 / 4 but got " + components.length);
}
}
private static UR handleFragments(String[] fragments) throws UR.InvalidTypeException {
int length = fragments.length;
String[] parts = new String[length];
Arrays.fill(parts, "");
String type = null;
String digest = null;
for(String fragment : fragments) {
String[] components = fragment.split("/");
if(components.length < 4) {
throw new IllegalArgumentException(String.format("Invalid fragment: %s, insufficient number of components (%d)", fragment, components.length));
}
if(type != null && !type.equals(components[0])) {
throw new IllegalArgumentException(String.format("Invalid fragment: %s, checksum changed %s, %s", fragment, type, components[0]));
}
type = components[0];
int[] sequence = checkAndGetSequence(components[1]);
int index = sequence[0];
int total = sequence[1];
if(total != length) {
throw new IllegalArgumentException(String.format("Invalid fragment: %s, total %d not equals to fragments length %d", fragment, total, length));
}
if(digest != null && !digest.equals(components[2])) {
throw new IllegalArgumentException(String.format("Invalid fragment: %s, checksum changed %s, %s", fragment, digest, components[2]));
}
digest = components[2];
if(parts[index - 1].length() > 0) {
throw new IllegalArgumentException(String.format("Invalid fragment: %s, index %d has already been set", fragment, index));
}
parts[index - 1] = components[3];
}
String payload = Arrays.stream(parts).reduce((cur, acc) -> cur+acc).orElse("");
checkDigest(payload, digest);
return new UR(type, BC32.decode(payload));
}
private static void checkDigest(String payload, String digest) {
MessageDigest sha256Digest = LegacyUREncoder.newDigest();
sha256Digest.update(BC32.decode(payload));
byte[] calculatedChecksum = sha256Digest.digest();
byte[] checksum = BC32.decode(digest);
if(!Arrays.equals(calculatedChecksum, checksum)) {
throw new IllegalArgumentException("Invalid digest: " + digest + " for payload: " + payload);
}
}
public static int[] checkAndGetSequence(String payload) {
String[] pieces = payload.toLowerCase().split("of");
if(pieces.length != 2) {
throw new IllegalArgumentException("Invalid sequence: " + payload);
}
int index = Integer.parseInt(pieces[0]);
int total = Integer.parseInt(pieces[1]);
if(index < 1 || index > total) {
throw new IllegalArgumentException("Invalid sequence: " + payload);
}
return new int[]{index, total};
}
}

View file

@ -0,0 +1,84 @@
package com.sparrowwallet.hummingbird;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.stream.IntStream;
public class LegacyUREncoder {
public static final int DEFAULT_FRAGMENT_LENGTH = 200;
private final UR ur;
private final int fragmentLen;
public LegacyUREncoder(UR ur) {
this(ur, DEFAULT_FRAGMENT_LENGTH);
}
public LegacyUREncoder(UR ur, int fragmentLen) {
this.ur = ur;
this.fragmentLen = fragmentLen;
}
public String[] encode() {
String encoded = BC32.encode(ur.getCborBytes());
MessageDigest sha256Digest = newDigest();
sha256Digest.update(ur.getCborBytes());
byte[] checksum = sha256Digest.digest();
String bc32Checksum = BC32.encode(checksum);
String[] fragments = splitData(encoded, fragmentLen);
return composeHeadersToFragments(fragments, bc32Checksum);
}
private String[] splitData(String s, int fragmentLen) {
int count = (int)Math.ceil(s.length() / (float)fragmentLen);
int partLength = (int)Math.ceil(s.length() / (float)count);
String[] fragments = new String[count];
for(int i = 0; i < count; i++) {
fragments[i] = s.substring(partLength * i, Math.min(partLength * (i + 1), s.length()));
}
return fragments;
}
private String[] composeHeadersToFragments(String[] fragments, String checksum) {
int length = fragments.length;
if(length <= 1) {
return Arrays.stream(fragments).map(this::composeUR).toArray(String[]::new);
} else {
return IntStream.range(0, length)
.mapToObj(i -> composeHeadersToFragment(fragments[i], checksum, i, length))
.toArray(String[]::new);
}
}
public static MessageDigest newDigest() {
try {
return MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e); // Can't happen.
}
}
public String composeHeadersToFragment(String fragment, String checksum, int index, int total) {
return composeUR(composeSequencing(composeChecksum(fragment, checksum), index, total));
}
private String composeChecksum(String payload, String checksum) {
return String.format("%s/%s", checksum, payload);
}
private String composeSequencing(String payload, int index, int total) {
return String.format("%dof%d/%s", index + 1, total, payload);
}
private String composeUR(String payload) {
return composeUR(payload, ur.getType());
}
private String composeUR(String payload, String type) {
return String.format("ur:%s/%s", type, payload);
}
}

View file

@ -0,0 +1,20 @@
package com.sparrowwallet.hummingbird;
import org.junit.Assert;
import org.junit.Test;
import java.nio.charset.StandardCharsets;
public class BC32Test {
@Test
public void testDecode() {
byte[] decode = BC32.decode("fpjkcmr0ypmk7unvvsh4ra4j");
Assert.assertEquals("Hello world", new String(decode));
}
@Test
public void testEncode() {
String s = BC32.encode("Hello world".getBytes(StandardCharsets.UTF_8));
Assert.assertEquals("fpjkcmr0ypmk7unvvsh4ra4j", s);
}
}

View file

@ -7,6 +7,8 @@ import org.junit.Assert;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@ -17,6 +19,7 @@ public class URTest {
UR ur = makeMessageUR(50, "Wolf");
String encoded = UREncoder.encode(ur);
Assert.assertEquals("ur:bytes/hdeymejtswhhylkepmykhhtsytsnoyoyaxaedsuttydmmhhpktpmsrjtgwdpfnsboxgwlbaawzuefywkdplrsrjynbvygabwjldapfcsdwkbrkch", encoded);
Assert.assertFalse(LegacyURDecoder.isLegacyURFragment(encoded));
}
@Test
@ -47,6 +50,7 @@ public class URTest {
"ur:bytes/20-9/lpbbascfadaxcywenbpljkhdcayapmrleeleaxpasfrtrdkncffwjyjzgyetdmlewtkpktgllepfrltataztksmhkbot"
};
Assert.assertArrayEquals("", expectedParts, parts.toArray());
parts.forEach(part -> Assert.assertFalse(LegacyURDecoder.isLegacyURFragment(part)));
}
@Test
@ -58,6 +62,7 @@ public class URTest {
do {
String part = urEncoder.nextPart();
Assert.assertFalse(LegacyURDecoder.isLegacyURFragment(part));
urDecoder.receivePart(part);
} while(urDecoder.getResult() == null);
@ -103,4 +108,102 @@ public class URTest {
String encoded = UREncoder.encode(ur);
Assert.assertEquals("ur:bytes/gdaebycpeofygoiyktlonlpkrksfutwyzmwmfyeozs", encoded);
}
@Test
public void testLegacyEncode() throws UR.InvalidTypeException, UR.InvalidCBORException {
String random = "7e4a61385e2550981b4b5633ab178eb077a30505fbd53f107ec1081e7cf0ca3c0dc0bfea5b8bfb5e6ffc91afd104c3aa756210b5dbc5118fd12c87ee04269815ba6a9968a0d0d3b7a9b631382a36bc70ab626d5670b4b48ff843f4d9a15631aa67c7aaf0ac6ce7e3bff03b2c9643e3375e47493c4e0f8635329d66fdec41b10ce74dcbf25fc15d829e7830c325643a98561f441b40a02e8353493e6afc16192fe99d90d8ca65539af77ddeaccc8943a37563a9ba83675bd5d4da7c60c9a172cf6940cbf0ec8fe04175a629932e3512c5d2aaea3cca3246f40a21ffdc33c3987dc7b880351230eb3759fe3c7dc7b2d3a20a95996ff0b7a0dba834f96beb64c14e3426fb051a936ba41569ab99c0066a6d9c0777a49e49e6cbad24d722a4c7da112432679264b9adc0a8cff9dd1fe0ee9ee2747f6a68537c389a7303a1af23c534ee6392bc17b04cf0fbce7689e66b673a440c04a9454005b0c76664639113458eb7d0902eff04d11138ce2a8ee16a9cd7c8926514efa9bd83ae7a4c139835f0fe0f68c628e0645c8524c30dfc314e825a7aa13224d98e2f7a9d12183a999bb1f28549c99a9072d99c05c24e0c84848c4fc147a094ab7b69e9cbea86952fccf15500fbb234ffe6ee6e6ded515c8016cb017ba36fb931ef276cec4ed22c1aed1495d2df3b3ce66c03f5b9ffa8434bf0e8fb149de94e050b3da178df1f76c00a366cb2801fabdf1a1e90cd3cd45ecb7a930a40b151455f76b726d552f31c21324992da257ff8bde2923dfd5d0d6b87233fae215ffacbecd96249099e7e3427d533db56cdb09c7475b4ce3314e33f43953a7370866cc11d85f00b71b15510b46c4b4fa490c660ddfeda0ceb1b8265995f7071c155ad1b57465fdc0fa81a73f9f19ac4872029d5844c1838f732e803043673e26cbc5b51297a324ff00a2d2d4222bad556b93d27c8e376e3ff8f9d37b3073410708ebb3d4dd7473d27212310b71a3c33a5c8f87f44824640e7f8970f4eda9364195c87a91172b8f085f1773641dde1ce21938746234055bc971ce2325f814e3eec60f781dd4faf52afd5be4a6b38656f7e9739f724cb7ccd4e4d01e802add3dc7b83191f894b3ee0ed752ee514d5ec55";
UR ur = UR.fromBytes(TestUtils.hexToBytes(random));
LegacyUREncoder legacyUREncoder = new LegacyUREncoder(ur);
String[] result = legacyUREncoder.encode();
Assert.assertArrayEquals(result, new String[]{
"ur:bytes/1of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/typjqlj2vyu9uf2snqd5k43n4vtcavrh5vzst7748ug8asggre70pj3uphqtl6jm30a4umlujxhazpxr4f6kyy94m0z3rr739jr7uppxnq2m565edzsdp5ah4xmrzwp2x678p2mzd4t8pd953luy8axe59trr2n8c740ptrvul3mlupm9jty8ceht",
"ur:bytes/2of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/er5j0zwp7rr2v5avm77csd3pnn5mjljtlq4mq570qcvxfty82v9v86yrdq2qt5r2dynu6huzcvjl6vajrvv5e2nntmhmh4vejy58gm4vw5m4qm8t02afknuvry6zuk0d9qvhu8v3lsyzadx9xfjudgjchf2463uegeydaq2y8lacv7rnp7u0wyqx5",
"ur:bytes/3of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/frp6eht8lrclw8ktf6yz54n9hlpdaqmw5rf7ttadjvzn35ymas2x5ndwjp26dtn8qqv6ndnsrh0fy7f8nvhtfy6u32f376zyjryeujvju6ms9gelua68lqa60wyarldf59xlpcnfes8gd0y0znfmnrj27p0vzv7rauua5fue4kwwjypsz2j32qqkc",
"ur:bytes/4of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/vwenyvwg3x3vwklgfqthlqng3zwxw928wz65u6lyfyeg5a75mmqaw0fxp8xp47rlq76xx9rsxghy9ynpsmlp3f6p9574pxgjdnr3002w3yxp6nxdmru59f8ye4yrjmxwqtsjwpjzgfrz0c9r6p99t0d57njl2s62jln8325q0hv35llnwumnda4g4",
"ur:bytes/5of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/eqqkevqhhgm0hyc77fmva38dytq6a52ft5kl8v7wvmqr7kull2zrf0cw37c5nh55upgt8ksh3hclwmqq5dnvk2qpl27lrg0fpnfu630vk75npfqtz529tamtwfk42te3cgfjfxfd5ftllz779y3al4ws66u8yvl6ug2llt97ektzfyyeul35yl2n8",
"ur:bytes/6of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/k6kekcfcar4kn8rx98r8ape2wnnwzrxesgashcqkud325gtgmztf7jfp3nqmhld5r8trwpxtx2lwpcuz4ddrdt5vh7up75p5ule7xdvfpeq982cgnqc8rmn96qrqsm88cnvh3d4z2t6xf8lqz3d94pz9wk426un6f7gudmw8lu0n5mmxpe5zpcgaw",
"ur:bytes/7of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/eafht5w0f8yy33pdc68se6tj8c0azgy3jqulufwr6wm2fkgx2us753zu4c7zzlzaekg8w7rn3pjwr5vg6q2k7fw88zxf0czn37a3s00qwaf7h49t74he9xkwr9dalfww0hyn9hen2wf5q7sq4d60w8hqcer7y5k0hqa46jaeg56hk92xd0vz4",
});
Assert.assertEquals(random, TestUtils.bytesToHex(LegacyURDecoder.decode(result).toBytes()));
Arrays.stream(result).forEach(part -> Assert.assertTrue(LegacyURDecoder.isLegacyURFragment(part)));
}
@Test
public void testLegacyEncodeShort() throws Exception {
String random = "7e4a61385e2550981b4b5633ab178eb077a30505fb";
UR ur = UR.fromBytes(TestUtils.hexToBytes(random));
LegacyUREncoder legacyUREncoder = new LegacyUREncoder(ur);
String[] result = legacyUREncoder.encode();
Assert.assertArrayEquals(result, new String[] {
"ur:bytes/24ly5cfctcj4pxqmfdtr82ch36c80gc9qhasmxdxs3"
});
Assert.assertTrue(LegacyURDecoder.isLegacyURFragment(result[0]));
}
@Test
public void testLegacyDecode() throws Exception{
String[] fragments = new String[]{
"ur:bytes/1of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/typjqlj2vyu9uf2snqd5k43n4vtcavrh5vzst7748ug8asggre70pj3uphqtl6jm30a4umlujxhazpxr4f6kyy94m0z3rr739jr7uppxnq2m565edzsdp5ah4xmrzwp2x678p2mzd4t8pd953luy8axe59trr2n8c740ptrvul3mlupm9jty8cehter5j0zwp7rr2v5a",
"ur:bytes/2of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/vm77csd3pnn5mjljtlq4mq570qcvxfty82v9v86yrdq2qt5r2dynu6huzcvjl6vajrvv5e2nntmhmh4vejy58gm4vw5m4qm8t02afknuvry6zuk0d9qvhu8v3lsyzadx9xfjudgjchf2463uegeydaq2y8lacv7rnp7u0wyqx5frp6eht8lrclw8ktf6yz54n9hlpdaq",
"ur:bytes/3of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/mw5rf7ttadjvzn35ymas2x5ndwjp26dtn8qqv6ndnsrh0fy7f8nvhtfy6u32f376zyjryeujvju6ms9gelua68lqa60wyarldf59xlpcnfes8gd0y0znfmnrj27p0vzv7rauua5fue4kwwjypsz2j32qqkcvwenyvwg3x3vwklgfqthlqng3zwxw928wz65u6lyfyeg5",
"ur:bytes/4of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/a75mmqaw0fxp8xp47rlq76xx9rsxghy9ynpsmlp3f6p9574pxgjdnr3002w3yxp6nxdmru59f8ye4yrjmxwqtsjwpjzgfrz0c9r6p99t0d57njl2s62jln8325q0hv35llnwumnda4g4eqqkevqhhgm0hyc77fmva38dytq6a52ft5kl8v7wvmqr7kull2zrf0cw37c5",
"ur:bytes/5of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/nh55upgt8ksh3hclwmqq5dnvk2qpl27lrg0fpnfu630vk75npfqtz529tamtwfk42te3cgfjfxfd5ftllz779y3al4ws66u8yvl6ug2llt97ektzfyyeul35yl2n8k6kekcfcar4kn8rx98r8ape2wnnwzrxesgashcqkud325gtgmztf7jfp3nqmhld5r8trwpxtx2l",
"ur:bytes/6of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/wpcuz4ddrdt5vh7up75p5ule7xdvfpeq982cgnqc8rmn96qrqsm88cnvh3d4z2t6xf8lqz3d94pz9wk426un6f7gudmw8lu0n5mmxpe5zpcgaweafht5w0f8yy33pdc68se6tj8c0azgy3jqulufwr6wm2fkgx2us753zu4c7zzlzaekg8w7rn3pjwr5vg6q2k7fw88z",
"ur:bytes/7of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/xf0czn37a3s00qwaf7h49t74he9xkwr9dalfww0hyn9hen2wf5q7sq4d60w8hqcer7y5k0hqa46jaeg56hk92xd0vz4",
};
UR ur = LegacyURDecoder.decode(fragments);
Assert.assertEquals(TestUtils.bytesToHex(ur.toBytes()), "7e4a61385e2550981b4b5633ab178eb077a30505fbd53f107ec1081e7cf0ca3c0dc0bfea5b8bfb5e6ffc91afd104c3aa756210b5dbc5118fd12c87ee04269815ba6a9968a0d0d3b7a9b631382a36bc70ab626d5670b4b48ff843f4d9a15631aa67c7aaf0ac6ce7e3bff03b2c9643e3375e47493c4e0f8635329d66fdec41b10ce74dcbf25fc15d829e7830c325643a98561f441b40a02e8353493e6afc16192fe99d90d8ca65539af77ddeaccc8943a37563a9ba83675bd5d4da7c60c9a172cf6940cbf0ec8fe04175a629932e3512c5d2aaea3cca3246f40a21ffdc33c3987dc7b880351230eb3759fe3c7dc7b2d3a20a95996ff0b7a0dba834f96beb64c14e3426fb051a936ba41569ab99c0066a6d9c0777a49e49e6cbad24d722a4c7da112432679264b9adc0a8cff9dd1fe0ee9ee2747f6a68537c389a7303a1af23c534ee6392bc17b04cf0fbce7689e66b673a440c04a9454005b0c76664639113458eb7d0902eff04d11138ce2a8ee16a9cd7c8926514efa9bd83ae7a4c139835f0fe0f68c628e0645c8524c30dfc314e825a7aa13224d98e2f7a9d12183a999bb1f28549c99a9072d99c05c24e0c84848c4fc147a094ab7b69e9cbea86952fccf15500fbb234ffe6ee6e6ded515c8016cb017ba36fb931ef276cec4ed22c1aed1495d2df3b3ce66c03f5b9ffa8434bf0e8fb149de94e050b3da178df1f76c00a366cb2801fabdf1a1e90cd3cd45ecb7a930a40b151455f76b726d552f31c21324992da257ff8bde2923dfd5d0d6b87233fae215ffacbecd96249099e7e3427d533db56cdb09c7475b4ce3314e33f43953a7370866cc11d85f00b71b15510b46c4b4fa490c660ddfeda0ceb1b8265995f7071c155ad1b57465fdc0fa81a73f9f19ac4872029d5844c1838f732e803043673e26cbc5b51297a324ff00a2d2d4222bad556b93d27c8e376e3ff8f9d37b3073410708ebb3d4dd7473d27212310b71a3c33a5c8f87f44824640e7f8970f4eda9364195c87a91172b8f085f1773641dde1ce21938746234055bc971ce2325f814e3eec60f781dd4faf52afd5be4a6b38656f7e9739f724cb7ccd4e4d01e802add3dc7b83191f894b3ee0ed752ee514d5ec55");
Arrays.stream(fragments).forEach(part -> Assert.assertTrue(LegacyURDecoder.isLegacyURFragment(part)));
}
@Test
public void testLegacyDecodeParts() throws Exception{
String[] fragments = new String[]{
"ur:bytes/1of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/typjqlj2vyu9uf2snqd5k43n4vtcavrh5vzst7748ug8asggre70pj3uphqtl6jm30a4umlujxhazpxr4f6kyy94m0z3rr739jr7uppxnq2m565edzsdp5ah4xmrzwp2x678p2mzd4t8pd953luy8axe59trr2n8c740ptrvul3mlupm9jty8cehter5j0zwp7rr2v5a",
"ur:bytes/2of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/vm77csd3pnn5mjljtlq4mq570qcvxfty82v9v86yrdq2qt5r2dynu6huzcvjl6vajrvv5e2nntmhmh4vejy58gm4vw5m4qm8t02afknuvry6zuk0d9qvhu8v3lsyzadx9xfjudgjchf2463uegeydaq2y8lacv7rnp7u0wyqx5frp6eht8lrclw8ktf6yz54n9hlpdaq",
"ur:bytes/3of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/mw5rf7ttadjvzn35ymas2x5ndwjp26dtn8qqv6ndnsrh0fy7f8nvhtfy6u32f376zyjryeujvju6ms9gelua68lqa60wyarldf59xlpcnfes8gd0y0znfmnrj27p0vzv7rauua5fue4kwwjypsz2j32qqkcvwenyvwg3x3vwklgfqthlqng3zwxw928wz65u6lyfyeg5",
"ur:bytes/4of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/a75mmqaw0fxp8xp47rlq76xx9rsxghy9ynpsmlp3f6p9574pxgjdnr3002w3yxp6nxdmru59f8ye4yrjmxwqtsjwpjzgfrz0c9r6p99t0d57njl2s62jln8325q0hv35llnwumnda4g4eqqkevqhhgm0hyc77fmva38dytq6a52ft5kl8v7wvmqr7kull2zrf0cw37c5",
"ur:bytes/5of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/nh55upgt8ksh3hclwmqq5dnvk2qpl27lrg0fpnfu630vk75npfqtz529tamtwfk42te3cgfjfxfd5ftllz779y3al4ws66u8yvl6ug2llt97ektzfyyeul35yl2n8k6kekcfcar4kn8rx98r8ape2wnnwzrxesgashcqkud325gtgmztf7jfp3nqmhld5r8trwpxtx2l",
"ur:bytes/6of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/wpcuz4ddrdt5vh7up75p5ule7xdvfpeq982cgnqc8rmn96qrqsm88cnvh3d4z2t6xf8lqz3d94pz9wk426un6f7gudmw8lu0n5mmxpe5zpcgaweafht5w0f8yy33pdc68se6tj8c0azgy3jqulufwr6wm2fkgx2us753zu4c7zzlzaekg8w7rn3pjwr5vg6q2k7fw88z",
"ur:bytes/7of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/xf0czn37a3s00qwaf7h49t74he9xkwr9dalfww0hyn9hen2wf5q7sq4d60w8hqcer7y5k0hqa46jaeg56hk92xd0vz4",
};
LegacyURDecoder legacyURDecoder = new LegacyURDecoder();
for(Iterator<String> iter = Arrays.stream(fragments).iterator(); iter.hasNext(); ) {
String fragment = iter.next();
Assert.assertTrue(LegacyURDecoder.isLegacyURFragment(fragment));
legacyURDecoder.receivePart(fragment);
Assert.assertEquals(iter.hasNext(), !legacyURDecoder.isComplete());
}
UR ur = legacyURDecoder.decode();
Assert.assertEquals(TestUtils.bytesToHex(ur.toBytes()), "7e4a61385e2550981b4b5633ab178eb077a30505fbd53f107ec1081e7cf0ca3c0dc0bfea5b8bfb5e6ffc91afd104c3aa756210b5dbc5118fd12c87ee04269815ba6a9968a0d0d3b7a9b631382a36bc70ab626d5670b4b48ff843f4d9a15631aa67c7aaf0ac6ce7e3bff03b2c9643e3375e47493c4e0f8635329d66fdec41b10ce74dcbf25fc15d829e7830c325643a98561f441b40a02e8353493e6afc16192fe99d90d8ca65539af77ddeaccc8943a37563a9ba83675bd5d4da7c60c9a172cf6940cbf0ec8fe04175a629932e3512c5d2aaea3cca3246f40a21ffdc33c3987dc7b880351230eb3759fe3c7dc7b2d3a20a95996ff0b7a0dba834f96beb64c14e3426fb051a936ba41569ab99c0066a6d9c0777a49e49e6cbad24d722a4c7da112432679264b9adc0a8cff9dd1fe0ee9ee2747f6a68537c389a7303a1af23c534ee6392bc17b04cf0fbce7689e66b673a440c04a9454005b0c76664639113458eb7d0902eff04d11138ce2a8ee16a9cd7c8926514efa9bd83ae7a4c139835f0fe0f68c628e0645c8524c30dfc314e825a7aa13224d98e2f7a9d12183a999bb1f28549c99a9072d99c05c24e0c84848c4fc147a094ab7b69e9cbea86952fccf15500fbb234ffe6ee6e6ded515c8016cb017ba36fb931ef276cec4ed22c1aed1495d2df3b3ce66c03f5b9ffa8434bf0e8fb149de94e050b3da178df1f76c00a366cb2801fabdf1a1e90cd3cd45ecb7a930a40b151455f76b726d552f31c21324992da257ff8bde2923dfd5d0d6b87233fae215ffacbecd96249099e7e3427d533db56cdb09c7475b4ce3314e33f43953a7370866cc11d85f00b71b15510b46c4b4fa490c660ddfeda0ceb1b8265995f7071c155ad1b57465fdc0fa81a73f9f19ac4872029d5844c1838f732e803043673e26cbc5b51297a324ff00a2d2d4222bad556b93d27c8e376e3ff8f9d37b3073410708ebb3d4dd7473d27212310b71a3c33a5c8f87f44824640e7f8970f4eda9364195c87a91172b8f085f1773641dde1ce21938746234055bc971ce2325f814e3eec60f781dd4faf52afd5be4a6b38656f7e9739f724cb7ccd4e4d01e802add3dc7b83191f894b3ee0ed752ee514d5ec55");
}
@Test
public void testLegacyDecodeShort() throws Exception {
String[] fragments = new String[]{
"ur:bytes/24ly5cfctcj4pxqmfdtr82ch36c80gc9qhasmxdxs3"
};
UR ur = LegacyURDecoder.decode(fragments);
Assert.assertEquals(TestUtils.bytesToHex(ur.toBytes()), "7e4a61385e2550981b4b5633ab178eb077a30505fb");
Assert.assertTrue(LegacyURDecoder.isLegacyURFragment(fragments[0]));
}
@Test
public void testDecodeShortUR1() throws Exception {
String[] fragments = new String[]{
"UR:BYTES/1OF2/EFE6JTF4UM5NW43JVL95KX7NYQHX5V9HYGAAEQWN620XHSQ3ZNMS0PF2GR/TZAHQUMZWNLSZQZJQGQQQQQP4K6PXJYRYULEQDCUXER58CVPDHNSN80N39WME90TE5VMAWPJQRKQQQQQQQQ0LLLLLUQ7SQCQQQQQQQQQZCQPG4MKDDMGDJNQU53PZXVKD007R505KCSCZQQQQQQQQQGPRAVPKQQQQQQQQQQKQQ29MGDUNFESKL5AYZ03TTLECZT0DW7C".toLowerCase(),
"UR:BYTES/2OF2/EFE6JTF4UM5NW43JVL95KX7NYQHX5V9HYGAAEQWN620XHSQ3ZNMS0PF2GR/N5NZYPSREN29X2CN2RSYEWHLJYZKHKCGH5U80D8UHRXHP2HD553EECGJ23A3SQQQQQQ9GQQQSQQQQQYQQQQQPQQQQQQQQPSQQQQQQQQ0YTU7N".toLowerCase()
};
UR ur = LegacyURDecoder.decode(fragments);
Assert.assertEquals(TestUtils.bytesToHex(ur.toBytes()), "70736274ff0100520200000001adb4134883273f90371c364743e1816de7099df3895dbc95ebcd19beb83200ec0000000000ffffffff01e80300000000000016001457766b7686ca60e5221119966bdfe1d1f4b62181000000000001011f581b0000000000001600145da1bc9a730b7e9d209f15aff9c096f6bbd89d26220603ccd4532b1350e04cbaff91056bdb08bd3877b4fcb8cd70aaeda5239ce112547b180000000054000080000000800000008000000000060000000000");
Arrays.stream(fragments).forEach(part -> Assert.assertTrue(LegacyURDecoder.isLegacyURFragment(part)));
}
}