WIP
This commit is contained in:
@@ -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 >= '0' && c <= '9') {
|
||||
sb.append((char) (c - '0' + '0')); // full-width -> half-width
|
||||
sb.append((char) (c - '0' + '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 >= '0' && c <= '9') ? (char) (c - '0' + '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
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user