This commit is contained in:
2026-08-12 17:24:13 +09:00
parent 366c6157af
commit 8cff34abb4
5 changed files with 746 additions and 307 deletions
+126 -23
View File
@@ -2,8 +2,13 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Base64;
import java.util.ArrayList;
import java.util.List;
import java.math.BigInteger;
import swiss.qpq.gajumaru.core.encoding.Base58;
import swiss.qpq.gajumaru.core.encoding.RLP;
import swiss.qpq.gajumaru.core.formatting.GajuFormat;
public class Testinator {
public static void main(String[] args) {
@@ -20,11 +25,23 @@ public class Testinator {
case "base58" -> {
System.out.print(base58(args[1]));
}
case "base58_check" -> {
System.out.print(base58_check(args[1]));
}
case "rlp" -> {
System.out.print(rlp(args[1]));
}
case "rlp_stream" -> {
System.out.print(rlp_stream(args[1]));
}
case "rlp_fail" -> {
System.out.print(rlp_fail(args[1]));
}
case "gaju_format" -> {
System.out.print(gaju_format(args[1]));
}
}
} catch (IOException e) {
} catch (Exception e) {
System.err.println("Error: " + e.getMessage());
System.exit(1);
}
@@ -32,38 +49,124 @@ public class Testinator {
private static String base64(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "base64.test");
Path encPath = Path.of(workingPath, "base64.java.txt");
Path decPath = Path.of(workingPath, "base64.java.back");
byte[] rawBytes = Files.readAllBytes(testPath);
byte[] encBytes = Base64.getEncoder().encode(rawBytes);
Files.write(encPath, encBytes);
byte[] readEncBytes = Files.readAllBytes(encPath);
byte[] decBytes = Base64.getDecoder().decode(readEncBytes);
Files.write(decPath, decBytes);
Path encPath = Path.of(workingPath, "base64.java.txt");
Path decPath = Path.of(workingPath, "base64.java.back");
byte[] rawB = Files.readAllBytes(testPath);
byte[] encB = Base64.getEncoder().encode(rawB);
Files.write(encPath, encB);
byte[] readE = Files.readAllBytes(encPath);
byte[] decB = Base64.getDecoder().decode(readE);
Files.write(decPath, decB);
return encPath.toString() + " " + decPath.toString();
}
private static String base58(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "base58.test");
Path encPath = Path.of(workingPath, "base58.java.txt");
Path decPath = Path.of(workingPath, "base58.java.back");
byte[] rawBytes = Files.readAllBytes(testPath);
String encString = Base58.encode(rawBytes);
Files.write(encPath, encString.getBytes());
byte[] readEncBytes = Files.readAllBytes(encPath);
String readEncString = new String(readEncBytes);
byte[] decBytes = Base58.decode(readEncString);
Files.write(decPath, decBytes);
Path encPath = Path.of(workingPath, "base58.java.txt");
Path decPath = Path.of(workingPath, "base58.java.back");
byte[] rawB = Files.readAllBytes(testPath);
String encS = Base58.encode(rawB);
Files.write(encPath, encS.getBytes());
byte[] readE = Files.readAllBytes(encPath);
String readS = new String(readE);
byte[] decB = Base58.decode(readS);
Files.write(decPath, decB);
return encPath.toString() + " " + decPath.toString();
}
private static String base58_check(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "base58_check.test");
Path encPath = Path.of(workingPath, "base58_check.java.txt");
Path decPath = Path.of(workingPath, "base58_check.java.back");
byte[] rawB = Files.readAllBytes(testPath);
String encS = Base58.checkEncode(rawB);
Files.write(encPath, encS.getBytes());
byte[] readE = Files.readAllBytes(encPath);
String readS = new String(readE);
byte[] decB = Base58.checkDecode(readS);
Files.write(decPath, decB);
return encPath.toString() + " " + decPath.toString();
}
private static String rlp(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "rlp.test");
Path encPath = Path.of(workingPath, "rlp.java.back");
byte[] encBytes = Files.readAllBytes(testPath);
RLP_Data decRLP = RLP.decode(encBytes),
Path resPath = Path.of(workingPath, "rlp.java.back");
byte[] encB = Files.readAllBytes(testPath);
Files.write(decPath, decBytes);
return encPath.toString() + " " + decPath.toString();
RLP.RLP_Data decRLP = RLP.decode(encB);
RLP.RLP_List root = decRLP.asList();
RLP.RLP_List sub = root.getItems().get(1).asList();
byte[] anchor = sub.getItems().get(1).asItem().getBytes();
byte[] reEnc = RLP.encode(decRLP);
Files.write(resPath, reEnc);
return new String(anchor) + "|" + resPath.toString();
}
private static String rlp_stream(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "rlp_stream.test");
byte[] buffer = Files.readAllBytes(testPath);
List<String> hashes = new ArrayList<>();
int offset = 0;
while (offset < buffer.length) {
RLP.DecodeResult res = RLP.decodeFrom(buffer, offset);
hashes.add(Integer.toString(res.data().hashCode())); // Just to verify we got objects
offset += res.consumed();
}
return Integer.toString(hashes.size());
}
private static String rlp_fail(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "rlp_fail.test");
byte[] buffer = Files.readAllBytes(testPath);
try {
RLP.decode(buffer);
return "SUCCESS";
} catch (RLP.RLPException e) {
return "RLP_EXCEPTION: " + e.getMessage();
} catch (Exception e) {
return "OTHER_EXCEPTION: " + e.getClass().getSimpleName();
}
}
private static String gaju_format(String workingPath) throws IOException {
Path testPath = Path.of(workingPath, "gaju_format.test");
Path resPath = Path.of(workingPath, "gaju_format.java.txt");
List<String> lines = Files.readAllLines(testPath);
List<String> results = new ArrayList<>();
for (String line : lines) {
if (line.trim().isEmpty()) continue;
String[] p = line.split("\\|");
String op = p[0];
switch (op) {
case "amount" -> {
GajuFormat.Type style = GajuFormat.Type.valueOf(p[1]);
GajuFormat.Unit unit = GajuFormat.Unit.valueOf(p[2]);
char sep = p[3].charAt(0);
int span = Integer.parseInt(p[4]);
BigInteger val = new BigInteger(p[5]);
results.add(GajuFormat.amount(new GajuFormat.FormatSpec(style, unit, sep, span), val));
}
case "approx" -> {
GajuFormat.Type style = GajuFormat.Type.valueOf(p[1]);
GajuFormat.Unit unit = GajuFormat.Unit.valueOf(p[2]);
char sep = p[3].charAt(0);
int span = Integer.parseInt(p[4]);
BigInteger val = new BigInteger(p[5]);
int prec = Integer.parseInt(p[6]);
results.add(GajuFormat.approxAmount(new GajuFormat.FormatSpec(style, unit, sep, span), val, prec));
}
case "read" -> {
byte[] b = GajuFormat.read(p[1]);
results.add(new BigInteger(b).toString());
}
}
}
Files.write(resPath, results);
return resPath.toString();
}
}
@@ -20,9 +20,12 @@
package swiss.qpq.gajumaru.core.encoding;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
// Stateless Base58 implementation.
// Stateless Base58 and Base58Check implementation.
public final class Base58 {
private static final char[] ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".toCharArray();
@@ -52,25 +55,20 @@ public final class Base58 {
char[] encoded = new char[temp.length * 2];
int outputStart = encoded.length;
// Treat 'temp' as one giant number. Process until the entire array is reduced to zeros.
// i indicates the position of the most significant leading zero.
int i = zeros;
while (i < temp.length) {
int remainder = 0;
// Perform long division from left to right across the active bytes
for (int j = i; j < temp.length; j++) {
int currentByte = temp[j] & 0xFF; // Safe unsigned byte conversion
int totalValue = (remainder * 256) + currentByte;
temp[j] = (byte) (totalValue / 58); // Mutate array with the quotient
remainder = totalValue % 58; // Carry the remainder to the next byte
temp[j] = (byte) (totalValue / 58);
remainder = totalValue % 58;
}
// The final remainder of this pass is our next Base58 digit character
encoded[--outputStart] = ALPHABET[remainder];
// Advance the pointer forward if the current head byte has been ground down to 0
if (temp[i] == 0) {
i++;
}
@@ -94,19 +92,16 @@ public final class Base58 {
return new byte[0];
}
// Convert the string into numeric index offsets
byte[] input58 = new byte[input.length()];
for (int i = 0; i < input.length(); i++) {
char c = input.charAt(i);
int digit = (c < 128) ? INDEXES[c] : -1;
if (digit < 0) {
// I picked the wrong week to stop drinking...
throw new IllegalArgumentException("Illegal Base58 character encountered: " + c);
throw new IllegalArgumentException(String.format("Illegal Base58 character '%c' at index %d", c, i));
}
input58[i] = (byte) digit;
}
// Count leading zeros to reconstruct leading 0 bytes
int zeros = 0;
while (zeros < input58.length && input58[zeros] == 0) {
zeros++;
@@ -120,33 +115,70 @@ public final class Base58 {
while (i < input58.length) {
int remainder = 0;
// Long division from left to right across the remaining numeric character indices
for (int j = i; j < input58.length; j++) {
int currentBase58Digit = input58[j] & 0xFF;
int totalValue = (remainder * 58) + currentBase58Digit; // Shift base by 58
input58[j] = (byte) (totalValue / 256); // Mutate array with the base-256 quotient
remainder = totalValue % 256; // Carry the byte remainder forward
input58[j] = (byte) (totalValue / 256);
remainder = totalValue % 256;
}
// The remainder of this pass is the next raw base-256 byte payload
decoded[--outputStart] = (byte) remainder;
// Advance past this leading index if its value has been exhausted or is 0
while (i < input58.length && input58[i] == 0) {
i++;
}
}
// Strip extra leading zero allocations from the worst-case boundary buffer
while (outputStart < decoded.length && decoded[outputStart] == 0) {
outputStart++;
}
// Re-inject the necessary leading padding zeros
byte[] result = new byte[decoded.length - outputStart + zeros];
System.arraycopy(decoded, outputStart, result, zeros, decoded.length - outputStart);
return result;
}
// Base58Check
//Encodes bytes with a 4-byte double-SHA256 checksum.
public static String checkEncode(byte[] input) {
byte[] checksum = doubleSha256(input);
byte[] combined = new byte[input.length + 4];
System.arraycopy(input, 0, combined, 0, input.length);
System.arraycopy(checksum, 0, combined, input.length, 4);
return encode(combined);
}
// Decodes a Base58Check string and validates the checksum.
public static byte[] checkDecode(String input) throws IllegalArgumentException {
byte[] decoded = decode(input);
if (decoded.length < 4) {
throw new IllegalArgumentException("Base58Check input too short");
}
byte[] data = Arrays.copyOfRange(decoded, 0, decoded.length - 4);
byte[] actual = Arrays.copyOfRange(decoded, decoded.length - 4, decoded.length);
byte[] expected = doubleSha256(data);
for (int i = 0; i < 4; i++) {
if (actual[i] != expected[i]) {
throw new IllegalArgumentException("Base58Check checksum mismatch");
}
}
return data;
}
// Internal Utilities
private static byte[] doubleSha256(byte[] data) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
return digest.digest(digest.digest(data));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-256 not available", e);
}
}
}
@@ -30,22 +30,55 @@ public final class RLP {
// RLP only has two types: byte arrays and lists of lists of byte arrays
// TODO: Comment this better with references.
public abstract static class RLP_Data {}
public static class RLPException extends RuntimeException {
public RLPException(String message) {
super(message);
}
}
// Data Models - lists and items
public abstract static class RLP_Data {
public boolean isItem() { return this instanceof RLP_Item; }
public boolean isList() { return this instanceof RLP_List; }
public RLP_Item asItem() {
if (isItem()) return (RLP_Item) this;
throw new RLPException("Expected RLP Item, found List");
}
public RLP_List asList() {
if (isList()) return (RLP_List) this;
throw new RLPException("Expected RLP List, found Item");
}
}
public static final class RLP_Item extends RLP_Data {
public final byte[] bytes;
public RLP_Item(byte[] bytes) {
this.bytes = bytes != null ? bytes : new byte[0];
this.bytes = (bytes != null) ? bytes : new byte[0];
}
public byte[] getBytes() { return bytes; }
}
public static final class RLP_List extends RLP_Data {
public final List<RLP_Data> items;
public RLP_List(List<RLP_Data> items) {
this.items = items != null ? items : new ArrayList<>();
this.items = (items != null) ? items : new ArrayList<>();
}
public List<RLP_Data> getItems() { return items; }
}
public record DecodeResult(RLP_Data data, int consumed) {}
// API
private RLP() {}
public static byte[] encode(RLP_Data data) {
@@ -54,7 +87,67 @@ public final class RLP {
} else if (data instanceof RLP_List list) {
return encodeList(list.items);
}
throw new IllegalArgumentException("Unsupported RLP type");
throw new RLPException("Unsupported RLP data type");
}
public static RLP_Data decode(byte[] buffer) {
if (buffer == null || buffer.length == 0) {
return new RLP_Item(new byte[0]);
}
DecodeResult result = decodeFrom(buffer, 0);
if (result.consumed != buffer.length) {
throw new RLPException("Buffer contains " + (buffer.length - result.consumed) + " trailing bytes");
}
return result.data;
}
// Decodes a single RLP object from a buffer starting at the given offset.
// Useful for stream decoding.
public static DecodeResult decodeFrom(byte[] buffer, int offset) {
if (buffer == null || offset >= buffer.length) {
throw new RLPException("Buffer underflow at offset " + offset);
}
int prefix = buffer[offset] & 0xFF;
// 1. Single byte [0x00, 0x7F] (itself)
if (prefix <= 0x7F) {
return new DecodeResult(new RLP_Item(new byte[] { (byte) prefix }), 1);
}
// 2. Short string [0x80, 0xB7] (length 0-55)
if (prefix <= 0xB7) {
int payloadLen = prefix - 0x80;
checkBounds(buffer, offset + 1, payloadLen);
byte[] payload = Arrays.copyOfRange(buffer, offset + 1, offset + 1 + payloadLen);
return new DecodeResult(new RLP_Item(payload), 1 + payloadLen);
}
// 3. Long string [0xB8, 0xBF] (length > 55)
if (prefix <= 0xBF) {
int lenLen = prefix - 0xB7;
checkBounds(buffer, offset + 1, lenLen);
int payloadLen = bigEndianToInt(buffer, offset + 1, offset + 1 + lenLen);
checkBounds(buffer, offset + 1 + lenLen, payloadLen);
byte[] payload = Arrays.copyOfRange(buffer, offset + 1 + lenLen, offset + 1 + lenLen + payloadLen);
return new DecodeResult(new RLP_Item(payload), 1 + lenLen + payloadLen);
}
// 4. Short list [0xC0, 0xF7] (total length 0-55)
if (prefix <= 0xF7) {
int payloadLen = prefix - 0xC0;
checkBounds(buffer, offset + 1, payloadLen);
RLP_List list = parseListSequence(buffer, offset + 1, offset + 1 + payloadLen);
return new DecodeResult(list, 1 + payloadLen);
}
// 5. Long list [0xF8, 0xFF] (total length > 55)
int lenLen = prefix - 0xF7;
checkBounds(buffer, offset + 1, lenLen);
int payloadLen = bigEndianToInt(buffer, offset + 1, offset + 1 + lenLen);
checkBounds(buffer, offset + 1 + lenLen, payloadLen);
RLP_List list = parseListSequence(buffer, offset + 1 + lenLen, offset + 1 + lenLen + payloadLen);
return new DecodeResult(list, 1 + lenLen + payloadLen);
}
private static byte[] encodeItem(byte[] bytes) {
@@ -69,25 +162,21 @@ public final class RLP {
return new byte[] { (byte) 0xC0 };
}
// Pass 1: Encode all sub-elements and compute exact combined byte length
byte[][] encodedChildren = new byte[items.size()][];
int totalPayloadLength = 0;
int totalPayloadLen = 0;
for (int i = 0; i < items.size(); i++) {
encodedChildren[i] = encode(items.get(i));
totalPayloadLength += encodedChildren[i].length;
totalPayloadLen += encodedChildren[i].length;
}
// Pass 2: Generate the structural frame list marker
byte[] prefix = prefixLength(totalPayloadLength, 0xC0, 0xF7);
// Pass 3: Flatten all segments directly into a single linear allocation
byte[] result = new byte[prefix.length + totalPayloadLength];
byte[] prefix = prefixLength(totalPayloadLen, 0xC0, 0xF7);
byte[] result = new byte[prefix.length + totalPayloadLen];
System.arraycopy(prefix, 0, result, 0, prefix.length);
int writePtr = prefix.length;
int ptr = prefix.length;
for (byte[] child : encodedChildren) {
System.arraycopy(child, 0, result, writePtr, child.length);
writePtr += child.length;
System.arraycopy(child, 0, result, ptr, child.length);
ptr += child.length;
}
return result;
}
@@ -100,92 +189,52 @@ public final class RLP {
return result;
}
private static byte[] prefixLength(int length, int shortOffset, int longOffset) {
if (length <= 55) {
return new byte[] { (byte) (shortOffset + length) };
private static byte[] prefixLength(int len, int shortOffset, int longOffset) {
if (len <= 55) {
return new byte[] { (byte) (shortOffset + len) };
}
byte[] lengthBytes = intToBigEndian(length);
byte[] prefix = new byte[1 + lengthBytes.length];
prefix[0] = (byte) (longOffset + lengthBytes.length);
System.arraycopy(lengthBytes, 0, prefix, 1, lengthBytes.length);
return prefix;
byte[] lenB = intToBigEndian(len);
byte[] p = new byte[1 + lenB.length];
p[0] = (byte) (longOffset + lenB.length);
System.arraycopy(lenB, 0, p, 1, lenB.length);
return p;
}
private static byte[] intToBigEndian(int val) {
if (val == 0) return new byte[0];
int size = (Integer.numberOfLeadingZeros(val) == 32) ? 1 : (32 - Integer.numberOfLeadingZeros(val) + 7) / 8;
byte[] sigBytes = new byte[size];
byte[] b = new byte[size];
for (int i = size - 1; i >= 0; i--) {
sigBytes[i] = (byte) (val & 0xFF);
b[i] = (byte) (val & 0xFF);
val >>>= 8;
}
return sigBytes;
return b;
}
public static RLP_Data decode(byte[] bytes) {
if (bytes == null || bytes.length == 0) {
return new RLP_Item(new byte[0]);
}
return decodeRange(bytes, 0, bytes.length);
}
private static RLP_Data decodeRange(byte[] bytes, int start, int end) {
int prefix = bytes[start] & 0xFF;
if (prefix <= 0x7F) {
return new RLP_Item(new byte[] { (byte) prefix });
}
if (prefix <= 0xB7) {
return new RLP_Item(Arrays.copyOfRange(bytes, start + 1, start + 1 + (prefix - 0x80)));
}
if (prefix <= 0xBF) {
int lenLen = prefix - 0xB7;
int len = bigEndianToInt(bytes, start + 1, start + 1 + lenLen);
return new RLP_Item(Arrays.copyOfRange(bytes, start + 1 + lenLen, start + 1 + lenLen + len));
}
if (prefix <= 0xF7) {
int listLen = prefix - 0xC0;
return parseListSequence(bytes, start + 1, start + 1 + listLen);
}
int lenLen = prefix - 0xF7;
int listLen = bigEndianToInt(bytes, start + 1, start + 1 + lenLen);
return parseListSequence(bytes, start + 1 + lenLen, start + 1 + lenLen + listLen);
}
private static RLP_List parseListSequence(byte[] bytes, int cursor, int limit) {
private static RLP_List parseListSequence(byte[] b, int cursor, int limit) {
List<RLP_Data> elements = new ArrayList<>();
while (cursor < limit) {
int itemStart = cursor;
int prefix = bytes[cursor] & 0xFF;
int elementTotalSize;
if (prefix <= 0x7F) {
elementTotalSize = 1;
} else if (prefix <= 0xB7) {
elementTotalSize = 1 + (prefix - 0x80);
} else if (prefix <= 0xBF) {
int lenLen = prefix - 0xB7;
elementTotalSize = 1 + lenLen + bigEndianToInt(bytes, itemStart + 1, itemStart + 1 + lenLen);
} else if (prefix <= 0xF7) {
elementTotalSize = 1 + (prefix - 0xC0);
} else {
int lenLen = prefix - 0xF7;
elementTotalSize = 1 + lenLen + bigEndianToInt(bytes, itemStart + 1, itemStart + 1 + lenLen);
}
elements.add(decodeRange(bytes, itemStart, itemStart + elementTotalSize));
cursor += elementTotalSize;
DecodeResult res = decodeFrom(b, cursor);
elements.add(res.data);
cursor += res.consumed;
}
if (cursor != limit) {
throw new RLPException("List payload overflow or invalid child encoding");
}
return new RLP_List(elements);
}
private static int bigEndianToInt(byte[] bytes, int start, int end) {
int result = 0;
private static int bigEndianToInt(byte[] b, int start, int end) {
int res = 0;
for (int i = start; i < end; i++) {
result = (result << 8) | (bytes[i] & 0xFF);
res = (res << 8) | (b[i] & 0xFF);
}
return res;
}
private static void checkBounds(byte[] buffer, int start, int length) {
if (length < 0 || (start + length) > buffer.length) {
throw new RLPException("Incomplete RLP data: requested " + length + " bytes from offset " + start);
}
return result;
}
}
@@ -1,9 +1,15 @@
package swiss.qpq.gajumaru.core.formatting;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
// GajuFormat provides formatting and parsing for Gaju and Puck units,
// maintaining 1-for-1 parity with the Erlang implementation (hz_format.erl).
// One Gaju = 10^18 Pucks.
public final class GajuFormat {
public enum Type { US, JP, METRIC, LEGACY }
public enum Unit { GAJU, PUCK }
@@ -14,76 +20,116 @@ public final class GajuFormat {
private static final String GAJU_MARK = "";
private static final String PUCK_MARK = "";
private static final int FRACTION_LEN = 18;
private static final BigInteger ONE_GAJU = BigInteger.TEN.pow(FRACTION_LEN);
private static final String[] JP_RANKS = {"", "", "", "", "", "", "", "", "", "", "", "", ""};
private static final String[] METRIC_RANKS = {"", "k", "m", "G", "T", "P", "E", "Z", "Y", "r", "Q"};
private static final String[] LEGACY_RANKS = {"", "k", "m", "b", "T", "q", "E", "Z", "Y", "r", "Q"};
private static final String[] METRIC_RANKS = {"", "k", "m", "g", "t", "p", "e", "z", "y", "r", "Q"};
private static final String[] LEGACY_RANKS = {"", "k", "m", "b", "t", "q", "e", "z", "y", "r", "Q"};
public static String amount(FormatSpec spec, byte[] puckBytes) {
String base10 = GmMonetaryFormatter.bytesToBase10PuckString(puckBytes);
boolean isNegative = puckBytes.length > 0 && (puckBytes[0] & 0x80) != 0; // Handle sign tracking if applicable
return amount(spec, new BigInteger(puckBytes));
}
public static String amount(FormatSpec spec, BigInteger pucks) {
boolean isNegative = pucks.signum() < 0;
BigInteger absPucks = pucks.abs();
return switch (spec.type()) {
case US -> formatWestern(spec, base10);
case JP -> formatMyriad(spec, base10, JP_RANKS, "");
case METRIC -> formatBestern(spec, base10, METRIC_RANKS);
case LEGACY -> formatBestern(spec, base10, LEGACY_RANKS);
case US -> formatWestern(spec, absPucks, isNegative);
case JP -> formatMyriad(spec, absPucks, JP_RANKS, isNegative);
case METRIC -> formatBestern(spec, absPucks, METRIC_RANKS, isNegative, "G", "P");
case LEGACY -> formatBestern(spec, absPucks, LEGACY_RANKS, isNegative, "G", "P");
};
}
private static String formatWestern(FormatSpec spec, String base10) {
public static String approxAmount(FormatSpec spec, BigInteger pucks, int precision) {
boolean isNegative = pucks.signum() < 0;
BigInteger absPucks = pucks.abs();
if (spec.unit() == Unit.PUCK) {
return PUCK_MARK + chunkString(base10, spec.separator(), spec.span(), false);
return amount(spec, pucks);
}
// Split at the 18-digit Gaju <-> Puck boundary
String[] split = splitPucksAndGajus(base10);
String gajuStr = chunkString(split[0], spec.separator(), spec.span(), false);
String puckStr = cleanTrailingZeros(split[1]);
BigInteger[] divRem = absPucks.divideAndRemainder(ONE_GAJU);
String gajuStr = chunkString(divRem[0].toString(), spec.separator(), spec.span(), false);
String sign = isNegative ? "-" : "";
String head = GAJU_MARK + sign + gajuStr;
String puckFull = String.format("%018d", divRem[1]);
int prec = Math.min(precision, FRACTION_LEN);
String significant = puckFull.substring(0, prec);
String rest = puckFull.substring(prec);
boolean hasMore = false;
for (int i = 0; i < rest.length(); i++) {
if (rest.charAt(i) != '0') {
hasMore = true;
break;
}
}
String resultTail = cleanTrailingZeros(significant);
if (resultTail.isEmpty()) {
return hasMore ? head + "...." : head;
}
return head + "." + chunkString(resultTail, spec.separator(), spec.span(), true) + (hasMore ? "..." : "");
}
private static String formatWestern(FormatSpec spec, BigInteger absPucks, boolean isNegative) {
String sign = isNegative ? "-" : "";
if (spec.unit() == Unit.PUCK) {
return PUCK_MARK + sign + chunkString(absPucks.toString(), spec.separator(), spec.span(), false);
}
BigInteger[] divRem = absPucks.divideAndRemainder(ONE_GAJU);
String gajuStr = chunkString(divRem[0].toString(), spec.separator(), spec.span(), false);
String puckStr = String.format("%018d", divRem[1]);
puckStr = cleanTrailingZeros(puckStr);
if (puckStr.isEmpty()) {
return GAJU_MARK + gajuStr;
return GAJU_MARK + sign + gajuStr;
}
return GAJU_MARK + gajuStr + "." + chunkString(puckStr, spec.separator(), spec.span(), true);
return GAJU_MARK + sign + gajuStr + "." + chunkString(puckStr, spec.separator(), spec.span(), true);
}
private static String formatMyriad(FormatSpec spec, String base10, String[] ranks, String negSign) {
private static String formatMyriad(FormatSpec spec, BigInteger absPucks, String[] ranks, boolean isNegative) {
String sign = isNegative ? "" : "";
if (spec.unit() == Unit.PUCK) {
return processRanks(base10, ranks, 4, PUCK_MARK);
return sign + processRanks(absPucks, ranks, 4, PUCK_MARK, false);
}
String[] split = splitPucksAndGajus(base10);
String gajuFormatted = processRanks(split[0], ranks, 4, GAJU_MARK);
String puckClean = cleanTrailingZeros(split[1]);
if (puckClean.isEmpty()) return gajuFormatted;
return gajuFormatted + " " + processRanks(puckClean, ranks, 4, PUCK_MARK);
BigInteger[] divRem = absPucks.divideAndRemainder(ONE_GAJU);
String gajuFormatted = processRanks(divRem[0], ranks, 4, GAJU_MARK, false);
if (divRem[1].equals(BigInteger.ZERO)) return sign + gajuFormatted;
return sign + gajuFormatted + " " + processRanks(divRem[1], ranks, 4, PUCK_MARK, false);
}
private static String formatBestern(FormatSpec spec, String base10, String[] ranks) {
private static String formatBestern(FormatSpec spec, BigInteger absPucks, String[] ranks, boolean isNegative, String gSuffix, String pSuffix) {
String sign = isNegative ? "-" : "";
if (spec.unit() == Unit.PUCK) {
return PUCK_MARK + processRanks(base10, ranks, 3, "P");
return PUCK_MARK + sign + processRanks(absPucks, ranks, 3, pSuffix, true);
}
String[] split = splitPucksAndGajus(base10);
String gajuPart = GAJU_MARK + processRanks(split[0], ranks, 3, "G");
String puckClean = cleanTrailingZeros(split[1]);
if (puckClean.isEmpty()) return gajuPart;
return gajuPart + " " + processRanks(puckClean, ranks, 3, "P");
}
private static String[] splitPucksAndGajus(String base10) {
if (base10.length() <= FRACTION_LEN) {
char[] zeros = new char[FRACTION_LEN - base10.length()];
Arrays.fill(zeros, '0');
return new String[]{"0", new String(zeros) + base10};
}
int cut = base10.length() - FRACTION_LEN;
return new String[]{base10.substring(0, cut), base10.substring(cut)};
BigInteger[] divRem = absPucks.divideAndRemainder(ONE_GAJU);
String gajuPart = GAJU_MARK + sign + processRanks(divRem[0], ranks, 3, gSuffix, true);
if (divRem[1].equals(BigInteger.ZERO)) return gajuPart;
return gajuPart + " " + processRanks(divRem[1], ranks, 3, pSuffix, true);
}
private static String cleanTrailingZeros(String str) {
int idx = str.length() - 1;
while (idx >= 0 && str.charAt(idx) == '0') idx--;
while (idx >= 0) {
char c = str.charAt(idx);
if (Character.isDigit(c)) {
if (c != '0') break;
}
idx--;
}
return idx < 0 ? "" : str.substring(0, idx + 1);
}
@@ -100,91 +146,135 @@ public final class GajuFormat {
return sb.toString();
}
private static String processRanks(String numericStr, String[] ranks, int span, String endingSymbol) {
if (numericStr.equals("0") || numericStr.isEmpty()) return "0 " + endingSymbol;
private static String processRanks(BigInteger value, String[] ranks, int span, String endingSymbol, boolean useSpaces) {
if (value.equals(BigInteger.ZERO)) return "0" + (useSpaces ? " " : "") + endingSymbol;
StringBuilder result = new StringBuilder();
int len = numericStr.length();
BigInteger divisor = BigInteger.TEN.pow(span);
int rankIndex = 0;
for (int i = len; i > 0; i -= span) {
int start = Math.max(0, i - span);
String chunk = numericStr.substring(start, i);
long val = Long.parseLong(chunk);
BigInteger current = value;
while (current.compareTo(BigInteger.ZERO) > 0) {
BigInteger[] dr = current.divideAndRemainder(divisor);
long val = dr[1].longValue();
if (val > 0) {
result.insert(0, val + ranks[rankIndex] + " ");
String rank = ranks[rankIndex];
result.insert(0, val + rank + (useSpaces ? " " : ""));
}
current = dr[0];
rankIndex++;
}
return result.toString().trim() + endingSymbol;
String res = result.toString().trim();
return res + (useSpaces ? " " : "") + endingSymbol;
}
private static final long ONE_GAJU_FACTOR = 1_000_000_000_000_000_000L;
private static final Map<Character, Long> MULTIPLIERS = new HashMap<>();
private static final Map<Character, BigInteger> MULTIPLIERS = new HashMap<>();
static {
// Warhammer 40k heresy meets Base 10k primacy
MULTIPLIERS.put('', 10_000L);
MULTIPLIERS.put('', 100_000_000L);
MULTIPLIERS.put('', 1_000_000_000_000L);
MULTIPLIERS.put('', 10_000_000_000_000_000L);
// NOTE: Higher ranges (垓, 秭) require BigInteger parsing if values exceed Long.MAX_VALUE.
// For standard UI entries, Long tracking handles up to 9 Quintillion Pucks
MULTIPLIERS.put('万', BigInteger.valueOf(10_000L));
MULTIPLIERS.put('', BigInteger.valueOf(100_000_000L));
MULTIPLIERS.put('', BigInteger.valueOf(1_000_000_000_000L));
MULTIPLIERS.put('', BigInteger.valueOf(10_000_000_000_000_000L));
MULTIPLIERS.put('', BigInteger.TEN.pow(20));
MULTIPLIERS.put('秭', BigInteger.TEN.pow(24));
MULTIPLIERS.put('穣', BigInteger.TEN.pow(28));
MULTIPLIERS.put('溝', BigInteger.TEN.pow(32));
MULTIPLIERS.put('澗', BigInteger.TEN.pow(36));
MULTIPLIERS.put('正', BigInteger.TEN.pow(40));
MULTIPLIERS.put('載', BigInteger.TEN.pow(44));
MULTIPLIERS.put('極', BigInteger.TEN.pow(48));
// SI / Heresy notations mapping rules
MULTIPLIERS.put('k', 1_000L);
MULTIPLIERS.put('m', 1_000_000L);
MULTIPLIERS.put('G', 1_000_000_000L);
MULTIPLIERS.put('g', 1_000_000_000L);
MULTIPLIERS.put('b', 1_000_000_000L); // Hertical billion flag
MULTIPLIERS.put('T', 1_000_000_000_000L);
MULTIPLIERS.put('t', 1_000_000_000_000L);
MULTIPLIERS.put('P', 1_000_000_000_000_000L);
MULTIPLIERS.put('p', 1_000_000_000_000_000L);
MULTIPLIERS.put('q', 1_000_000_000_000_000L); // Heretical quadrillion flag
MULTIPLIERS.put('k', BigInteger.valueOf(1_000L));
MULTIPLIERS.put('m', BigInteger.valueOf(1_000_000L));
MULTIPLIERS.put('g', BigInteger.valueOf(1_000_000_000L));
MULTIPLIERS.put('b', BigInteger.valueOf(1_000_000_000L));
MULTIPLIERS.put('t', BigInteger.valueOf(1_000_000_000_000L));
MULTIPLIERS.put('q', BigInteger.valueOf(1_000_000_000_000_000L));
MULTIPLIERS.put('p', BigInteger.valueOf(1_000_000_000_000_000L));
MULTIPLIERS.put('e', BigInteger.TEN.pow(18));
MULTIPLIERS.put('z', BigInteger.TEN.pow(21));
MULTIPLIERS.put('y', BigInteger.TEN.pow(24));
MULTIPLIERS.put('r', BigInteger.TEN.pow(27));
MULTIPLIERS.put('Q', BigInteger.TEN.pow(30));
}
public static byte[] read(String rawInput) {
if (rawInput == null || rawInput.isEmpty()) throw new IllegalArgumentException("Empty string context");
if (rawInput == null || rawInput.isEmpty()) throw new IllegalArgumentException("Empty input");
String input = normalize(rawInput);
boolean isNegative = false;
String input = rawInput.trim();
if (input.startsWith("-") || input.startsWith("") || input.startsWith("")) {
if (input.startsWith("-") || input.startsWith("") || input.startsWith("")) {
isNegative = true;
input = input.substring(1).trim();
}
boolean forceGaju = input.startsWith("");
boolean forcePuck = input.startsWith("");
boolean forceGaju = input.startsWith(GAJU_MARK);
boolean forcePuck = input.startsWith(PUCK_MARK);
if (forceGaju || forcePuck) {
input = input.substring(1).trim();
}
input = input.replace(",", "").replace("_", "").replace(" ", "");
BigInteger gajuTotal = BigInteger.ZERO;
BigInteger puckTotal = BigInteger.ZERO;
if (input.contains(".")) {
int dotIdx = input.indexOf('.');
String gajuString = input.substring(0, dotIdx);
String puckString = input.substring(dotIdx + 1);
StringBuilder sb = new StringBuilder(puckString);
while (sb.length() < 18) sb.append('0');
if (sb.length() > 18) throw new IllegalArgumentException("Precision overflow error");
byte[] gajuBytes = GmMonetaryParser.parseBase10ToBytes(standardizeDigits(gajuString));
byte[] puckBytes = GmMonetaryParser.parseBase10ToBytes(standardizeDigits(sb.toString()));
return addAtomicPuckArrays(multiplyByGajuFactor(gajuBytes), puckBytes, isNegative);
String gajuPart = input.substring(0, dotIdx);
String puckPart = input.substring(dotIdx + 1);
BigInteger gVal = parseRawRanked(gajuPart);
StringBuilder sb = new StringBuilder(puckPart);
while (sb.length() < FRACTION_LEN) sb.append('0');
if (sb.length() > FRACTION_LEN) throw new IllegalArgumentException("Precision overflow");
BigInteger pVal = new BigInteger(sb.toString());
puckTotal = gVal.multiply(ONE_GAJU).add(pVal);
} else {
boolean treatAsGaju = !forcePuck;
StringBuilder digits = new StringBuilder();
for (int i = 0; i < input.length(); i++) {
char c = input.charAt(i);
if (Character.isDigit(c)) {
digits.append(c);
} else if (c == 'G' || c == '木') {
BigInteger val = digits.length() > 0 ? new BigInteger(digits.toString()) : BigInteger.ZERO;
gajuTotal = gajuTotal.add(val);
digits.setLength(0);
treatAsGaju = false;
} else if (c == 'P' || c == '本') {
BigInteger val = digits.length() > 0 ? new BigInteger(digits.toString()) : BigInteger.ZERO;
puckTotal = puckTotal.add(val);
digits.setLength(0);
break;
} else if (MULTIPLIERS.containsKey(c)) {
if (digits.length() == 0) continue;
BigInteger val = new BigInteger(digits.toString()).multiply(MULTIPLIERS.get(c));
if (treatAsGaju) gajuTotal = gajuTotal.add(val);
else puckTotal = puckTotal.add(val);
digits.setLength(0);
}
}
if (digits.length() > 0) {
BigInteger val = new BigInteger(digits.toString());
if (treatAsGaju) gajuTotal = gajuTotal.add(val);
else puckTotal = puckTotal.add(val);
}
puckTotal = gajuTotal.multiply(ONE_GAJU).add(puckTotal);
}
return parseRankedString(input, forcePuck, isNegative);
if (isNegative) puckTotal = puckTotal.negate();
return puckTotal.toByteArray();
}
private static String standardizeDigits(String raw) {
private static String normalize(String raw) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < raw.length(); i++) {
char c = raw.charAt(i);
if (c >= '' && c <= '') {
sb.append((char) (c - '' + '0')); // full-width -> half-width
sb.append((char) (c - '' + '0'));
} else if (c == '' || c == ',' || c == '_' || c == ' ' || c == '\u3000' || c == '\t' || c == '\n' || c == '\r') {
continue;
} else {
sb.append(c);
}
@@ -192,51 +282,24 @@ public final class GajuFormat {
return sb.toString();
}
private static byte[] parseRankedString(String input, boolean forcePuck, boolean isNegative) {
byte[] totalPucks = new byte[]{0};
private static BigInteger parseRawRanked(String input) {
BigInteger total = BigInteger.ZERO;
StringBuilder currentDigits = new StringBuilder();
for (int i = 0; i < input.length(); i++) {
char c = input.charAt(i);
char standardC = (c >= '' && c <= '') ? (char) (c - '' + '0') : c;
if (Character.isDigit(standardC)) {
currentDigits.append(standardC);
} else if (MULTIPLIERS.containsKey(standardC)) {
if (Character.isDigit(c)) {
currentDigits.append(c);
} else if (MULTIPLIERS.containsKey(c)) {
if (currentDigits.length() == 0) continue;
long multiplier = MULTIPLIERS.get(standardC);
long sliceVal = Long.parseLong(currentDigits.toString()) * multiplier;
byte[] blockBytes = GmMonetaryParser.parseBase10ToBytes(Long.toString(sliceVal));
// Unless explicitly marked as a standalone Puck entry, assume Gaju
if (!forcePuck && standardC != 'P' && standardC != 'p') {
blockBytes = multiplyByGajuFactor(blockBytes);
}
totalPucks = addAtomicPuckArrays(totalPucks, blockBytes, false);
currentDigits.setLength(0); // Flush buffers!
BigInteger val = new BigInteger(currentDigits.toString()).multiply(MULTIPLIERS.get(c));
total = total.add(val);
currentDigits.setLength(0);
}
}
if (currentDigits.length() > 0) {
byte[] remainingBytes = GmMonetaryParser.parseBase10ToBytes(currentDigits.toString());
if (!forcePuck) remainingBytes = multiplyByGajuFactor(remainingBytes);
totalPucks = addAtomicPuckArrays(totalPucks, remainingBytes, false);
total = total.add(new BigInteger(currentDigits.toString()));
}
return totalPucks;
}
private static byte[] multiplyByGajuFactor(byte[] base) {
java.math.BigInteger b = new java.math.BigInteger(base);
return b.multiply(java.math.BigInteger.valueOf(ONE_GAJU_FACTOR)).toByteArray();
}
private static byte[] addAtomicPuckArrays(byte[] a, byte[] b, boolean makeNegative) {
java.math.BigInteger biA = new java.math.BigInteger(a);
java.math.BigInteger biB = new java.math.BigInteger(b);
java.math.BigInteger sum = biA.add(biB);
if (makeNegative) sum = sum.negate();
return sum.toByteArray();
return total;
}
}
+242 -50
View File
@@ -1,5 +1,10 @@
%%% @doc
%%% Gajumaru Java Lib Tester: gmt
%%%
%%% This module provides an inter-language test suite for the Gajumaru core
%%% Java libraries. It tests the Java implementation against the canonical
%%% Erlang implementation by generating random test vectors and comparing
%%% the outputs.
%%% @end
-module(gmt).
@@ -11,15 +16,18 @@
-export([start/1]).
%%% Logic
mods() ->
#{"base64" => fun base64/0,
"base58" => fun base58/0,
"rlp" => fun rlp/0}.
#{"base64" => fun base64/0,
"base58" => fun base58/0,
"base58_check" => fun base58_check/0,
"rlp" => fun rlp/0,
"rlp_stream" => fun rlp_stream/0,
"rlp_fail" => fun rlp_fail/0,
"gaju_format" => fun gaju_format/0}.
-spec start(ArgV) -> ok
when ArgV :: [string()].
start([]) ->
Tests = mods(),
ok = run(Tests),
@@ -29,13 +37,14 @@ start(["list"]) ->
ok = lists:foreach(fun display/1, maps:keys(mods())),
zx:silent_stop();
start(Mods) ->
Tests = maps:with(Mods, mods()),
Available = mods(),
Tests = maps:with(Mods, Available),
ok =
case maps:size(Tests) =:= length(Mods) of
true ->
run(Tests);
false ->
NotMods = lists:subtract(Mods, maps:keys(Tests)),
NotMods = lists:subtract(Mods, maps:keys(Available)),
ok = io:format("The following arguments are not testable module names:~n"),
lists:foreach(fun display/1, NotMods)
end,
@@ -45,8 +54,12 @@ display(Name) ->
io:format(" ~ts~n", [Name]).
run(Tests) ->
BaseDir = filename:dirname(zx:get_home()),
ok = file:set_cwd(BaseDir),
{ok, Cwd} = file:get_cwd(),
ok =
case filename:basename(Cwd) of
"test" -> file:set_cwd("..");
_ -> ok
end,
ok = clean(),
ok = build(),
Results = maps:map(fun run/2, Tests),
@@ -57,7 +70,7 @@ run(Name, Test) ->
Test().
clean() ->
Temp = temp_dir(),
Temp = "test/temp",
lists:foreach(fun(D) -> ok = clean(D) end, [Temp]).
clean(Dir) ->
@@ -72,49 +85,93 @@ build() ->
io:format("Compile: ~ts", [Out]).
temp_dir() ->
"test/temp".
{ok, Cwd} = file:get_cwd(),
filename:join(Cwd, "test/temp").
trim(S) ->
Unprintable = fun(C) -> C =< 32 end,
lists:reverse(lists:dropwhile(Unprintable, lists:reverse(lists:dropwhile(Unprintable, S)))).
%%% Test Modules
base64() ->
TestFile = filename:join(temp_dir(), "base64.test"),
ConvFile = filename:join(temp_dir(), "base64.erlang.txt"),
Temp = temp_dir(),
TestFile = filename:join(Temp, "base64.test"),
ConvFile = filename:join(Temp, "base64.erlang.txt"),
ok = filelib:ensure_dir(TestFile),
ok = file:write_file(TestFile, rand:bytes(rand:uniform(5000))),
{ok, Bytes} = file:read_file(TestFile),
Base64 = base64:encode(Bytes),
{ok, B} = file:read_file(TestFile),
Base64 = base64:encode(B),
ok = file:write_file(ConvFile, Base64),
Run = unicode:characters_to_list(["bin/run base64 ", temp_dir()]),
[JavaEncPath, JavaDecPath] = string:split(os:cmd(Run), " "),
{ok, E_Enc_Bytes} = file:read_file(ConvFile),
{ok, J_Enc_Bytes} = file:read_file(JavaEncPath),
E_Hash = crypto:hash(sha512, E_Enc_Bytes),
J_Hash = crypto:hash(sha512, J_Enc_Bytes),
{ok, E_Dec_Bytes} = file:read_file(TestFile),
{ok, J_Dec_Bytes} = file:read_file(JavaDecPath),
E_BinHash = crypto:hash(sha512, E_Dec_Bytes),
J_BinHash = crypto:hash(sha512, J_Dec_Bytes),
E_Hash =:= J_Hash andalso E_BinHash =:= J_BinHash.
Run = "bin/run base64 " ++ Temp,
Out = trim(os:cmd(Run)),
[JEnc, JDec] = string:split(Out, " "),
{ok, EEncB} = file:read_file(ConvFile),
{ok, JEncB} = file:read_file(JEnc),
EHash = crypto:hash(sha512, EEncB),
JHash = crypto:hash(sha512, JEncB),
{ok, EDecB} = file:read_file(TestFile),
{ok, JDecB} = file:read_file(JDec),
EBinHash = crypto:hash(sha512, EDecB),
JBinHash = crypto:hash(sha512, JDecB),
EHash =:= JHash andalso EBinHash =:= JBinHash.
base58() ->
TestFile = filename:join(temp_dir(), "base58.test"),
ConvFile = filename:join(temp_dir(), "base58.erlang.txt"),
Temp = temp_dir(),
TestFile = filename:join(Temp, "base58.test"),
ConvFile = filename:join(Temp, "base58.erlang.txt"),
ok = filelib:ensure_dir(TestFile),
ok = file:write_file(TestFile, rand:bytes(rand:uniform(5000))),
{ok, Bytes} = file:read_file(TestFile),
Base58 = base58:binary_to_base58(Bytes),
{ok, B} = file:read_file(TestFile),
Base58 = base58:binary_to_base58(B),
ok = file:write_file(ConvFile, Base58),
Run = unicode:characters_to_list(["bin/run base58 ", temp_dir()]),
[JavaEncPath, JavaDecPath] = string:split(os:cmd(Run), " "),
{ok, E_Enc_Bytes} = file:read_file(ConvFile),
{ok, J_Enc_Bytes} = file:read_file(JavaEncPath),
E_Hash = crypto:hash(sha512, E_Enc_Bytes),
J_Hash = crypto:hash(sha512, J_Enc_Bytes),
{ok, E_Dec_Bytes} = file:read_file(TestFile),
{ok, J_Dec_Bytes} = file:read_file(JavaDecPath),
E_BinHash = crypto:hash(sha512, E_Dec_Bytes),
J_BinHash = crypto:hash(sha512, J_Dec_Bytes),
E_Hash =:= J_Hash andalso E_BinHash =:= J_BinHash.
Run = "bin/run base58 " ++ Temp,
Out = trim(os:cmd(Run)),
[JEnc, JDec] = string:split(Out, " "),
{ok, EEncB} = file:read_file(ConvFile),
{ok, JEncB} = file:read_file(JEnc),
EHash = crypto:hash(sha512, EEncB),
JHash = crypto:hash(sha512, JEncB),
{ok, EDecB} = file:read_file(TestFile),
{ok, JDecB} = file:read_file(JDec),
EBinHash = crypto:hash(sha512, EDecB),
JBinHash = crypto:hash(sha512, JDecB),
EHash =:= JHash andalso EBinHash =:= JBinHash.
%% @doc
%% Tests Base58Check encoding/decoding parity.
%% @end
-spec base58_check() -> boolean().
base58_check() ->
Temp = temp_dir(),
TestFile = filename:join(Temp, "base58_check.test"),
ConvFile = filename:join(Temp, "base58_check.erlang.txt"),
ok = filelib:ensure_dir(TestFile),
ok = file:write_file(TestFile, rand:bytes(rand:uniform(5000))),
{ok, B} = file:read_file(TestFile),
Checksum = binary:part(crypto:hash(sha256, crypto:hash(sha256, B)), 0, 4),
Base58C = base58:binary_to_base58(<<B/binary, Checksum/binary>>),
ok = file:write_file(ConvFile, Base58C),
Run = "bin/run base58_check " ++ Temp,
Out = trim(os:cmd(Run)),
[JEnc, JDec] = string:split(Out, " "),
{ok, EEncB} = file:read_file(ConvFile),
{ok, JEncB} = file:read_file(JEnc),
EHash = crypto:hash(sha512, EEncB),
JHash = crypto:hash(sha512, JEncB),
{ok, EDecB} = file:read_file(TestFile),
{ok, JDecB} = file:read_file(JDec),
EBinHash = crypto:hash(sha512, EDecB),
JBinHash = crypto:hash(sha512, JDecB),
EHash =:= JHash andalso EBinHash =:= JBinHash.
rlp() ->
Temp = temp_dir(),
Anchor = <<"I thought what I'd do was, I'd pretend I was one of those deaf-mutes.">>,
Data =
[rand:bytes(rand:uniform(20)),
@@ -124,12 +181,147 @@ rlp() ->
rand:bytes(rand:uniform(5000))],
rand:bytes(rand:uniform(2000))],
RLP = gmser_rlp:encode(Data),
RLP_File = filename:join(temp_dir(), "rlp.test"),
RLP_File = filename:join(Temp, "rlp.test"),
ok = filelib:ensure_dir(RLP_File),
ok = file:write_file(RLP_File, RLP),
Run = unicode:characters_to_list(["bin/run rlp ", temp_dir()]),
[Found, JavaEncPath] = string:split(os:cmd(Run), " "),
{ok, E_Enc_Bytes} = file:read_file(RLP_File),
{ok, J_Enc_Bytes} = file:read_file(JavaEncPath),
E_Hash = crypto:hash(sha512, E_Enc_Bytes),
J_Hash = crypto:hash(sha512, J_Enc_Bytes),
E_Hash =:= J_Hash andalso Found =:= Anchor.
Run = "bin/run rlp " ++ Temp,
Out = trim(os:cmd(Run)),
case string:split(Out, "|") of
[Found, JPath] ->
JPathTrimmed = trim(JPath),
{ok, EEncB} = file:read_file(RLP_File),
case file:read_file(JPathTrimmed) of
{ok, JEncB} ->
EHash = crypto:hash(sha512, EEncB),
JHash = crypto:hash(sha512, JEncB),
EHash =:= JHash andalso Found =:= unicode:characters_to_list(Anchor);
{error, R} ->
io:format("Failed to read RLP Java result: ~tp (Path: ~tp)~n", [R, JPathTrimmed]),
false
end;
_ ->
io:format("RLP output mismatch: ~tp~n", [Out]),
false
end.
%% @doc
%% Tests RLP stream decoding by placing multiple objects in one buffer.
%% @end
-spec rlp_stream() -> boolean().
rlp_stream() ->
Temp = temp_dir(),
TestFile = filename:join(Temp, "rlp_stream.test"),
ok = filelib:ensure_dir(TestFile),
Data = [rand:bytes(rand:uniform(100)) || _ <- lists:seq(1, 10)],
RLP = << <<(gmser_rlp:encode(D))/binary>> || D <- Data >>,
ok = file:write_file(TestFile, RLP),
Run = "bin/run rlp_stream " ++ Temp,
Out = trim(os:cmd(Run)),
Out =:= "10".
rlp_fail() ->
Temp = temp_dir(),
TestFile = filename:join(Temp, "rlp_fail.test"),
ok = filelib:ensure_dir(TestFile),
Data = [<<"item1">>, <<"item2">>],
FullRLP = gmser_rlp:encode(Data),
TruncRLP = binary:part(FullRLP, 0, byte_size(FullRLP) - 2),
ok = file:write_file(TestFile, TruncRLP),
Run = "bin/run rlp_fail " ++ Temp,
Out = trim(os:cmd(Run)),
string:prefix(Out, "RLP_EXCEPTION:") =/= nomatch.
gaju_format() ->
Temp = temp_dir(),
TestFile = filename:join(Temp, "gaju_format.test"),
ok = filelib:ensure_dir(TestFile),
Cases = [gen_case() || _ <- lists:seq(1, 100)],
Lines = [serialize_case(C) || C <- Cases],
ok = file:write_file(TestFile, unicode:characters_to_binary([[L, "\n"] || L <- Lines])),
Run = "bin/run gaju_format " ++ Temp,
RawOut = os:cmd(Run),
JavaResPath = trim(RawOut),
case file:read_file(JavaResPath) of
{ok, ResContent} ->
JavaResults = string:split(trim(unicode:characters_to_list(ResContent)), "\n", all),
length(Cases) =:= length(JavaResults) andalso compare_results(Cases, JavaResults);
{error, Reason} ->
io:format("Failed to read Java results from: ~tp (Reason: ~tp)~nRaw Output: ~ts~n", [JavaResPath, Reason, RawOut]),
false
end.
%%% Generatorators
gen_case() ->
Ops = [amount, approx, read],
Op = lists:nth(rand:uniform(length(Ops)), Ops),
gen_case(Op).
gen_case(read) ->
Pucks = random_pucks(12),
Style = random_style(),
{read, hz_format:amount(gaju, Style, Pucks), Pucks};
gen_case(amount) ->
Unit = random_unit(),
Pucks =
case Unit of
gaju -> random_pucks(12);
puck -> random_pucks(8)
end,
Style = random_style(),
Sep = lists:nth(rand:uniform(2), [$,, $_]),
Span = rand:uniform(4),
{amount, Style, Unit, Sep, Span, Pucks, hz_format:amount(Unit, hz_style(Style, Sep, Span), Pucks)};
gen_case(approx) ->
Pucks = random_pucks(12),
Sep = lists:nth(rand:uniform(2), [$,, $_]),
Span = rand:uniform(2) + 2,
Prec = rand:uniform(18),
{approx, us, gaju, Sep, Span, Pucks, Prec, hz_format:approx_amount({Sep, Span}, Prec, Pucks)}.
random_pucks(MaxBytes) ->
Bytes = rand:bytes(rand:uniform(MaxBytes)),
crypto:bytes_to_integer(Bytes).
random_style() ->
lists:nth(rand:uniform(4), [us, jp, metric, legacy]).
random_unit() ->
lists:nth(rand:uniform(2), [gaju, puck]).
hz_style(us, Sep, Span) -> {Sep, Span};
hz_style(Style, _, _) -> Style.
serialize_case({amount, Style, Unit, Sep, Span, Pucks, _Expected}) ->
io_lib:format("amount|~ts|~ts|~c|~b|~b", [string:uppercase(atom_to_list(Style)), string:uppercase(atom_to_list(Unit)), Sep, Span, Pucks]);
serialize_case({approx, Style, Unit, Sep, Span, Pucks, Prec, _Expected}) ->
io_lib:format("approx|~ts|~ts|~c|~b|~b|~b", [string:uppercase(atom_to_list(Style)), string:uppercase(atom_to_list(Unit)), Sep, Span, Pucks, Prec]);
serialize_case({read, Input, _Expected}) ->
InputStr = unicode:characters_to_list(Input),
io_lib:format("read|~ts", [InputStr]).
compare_results([], []) ->
true;
compare_results([Case | Cases], [Result | Results]) ->
case check_case(Case, Result) of
true ->
compare_results(Cases, Results);
false ->
io:format("Parity failure!~nCase: ~tp~nJava Result: ~tp~n", [Case, Result]),
false
end.
check_case({amount, _, _, _, _, _, Expected}, Result) ->
flatten(Expected) =:= flatten(Result);
check_case({approx, _, _, _, _, _, _, Expected}, Result) ->
flatten(Expected) =:= flatten(Result);
check_case({read, _, Expected}, Result) ->
integer_to_list(Expected) =:= flatten(Result).
flatten(B) when is_binary(B) -> unicode:characters_to_list(B);
flatten(L) when is_list(L) -> unicode:characters_to_list(L).