This commit is contained in:
2026-07-03 00:33:23 +09:00
parent 8624ed6317
commit 9492d193d6
3 changed files with 228 additions and 3 deletions
+13
View File
@@ -20,6 +20,9 @@ public class Testinator {
case "base58" -> {
System.out.print(base58(args[1]));
}
case "rlp" -> {
System.out.print(rlp(args[1]));
}
}
} catch (IOException e) {
System.err.println("Error: " + e.getMessage());
@@ -53,4 +56,14 @@ public class Testinator {
Files.write(decPath, decBytes);
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),
Files.write(decPath, decBytes);
return encPath.toString() + " " + decPath.toString();
}
}
@@ -0,0 +1,191 @@
/*
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
* Project: Gajumaru Core Java Libraries <gajumaru.io>
*
* This program is dual-licensed:
* 1) Under the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option)
* any later version (AGPL-3.0-or-later).
*
* 2) Under a commercial/proprietary license available directly from QPQ AG.
* If you wish to use this software outside the strict constraints of the
* AGPLv3 (e.g. within a closed-source or proprietary product), you must
* purchase a commercial license from QPQ AG.
*
* Authors:
* - Craig Everett <craigeverett@qpq.swiss>
*
* SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial
*/
package swiss.qpq.gajumaru.core.encoding;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public final class RLP {
// Integrated data models
// 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 final class RLP_Item extends RLP_Data {
public final byte[] bytes;
public RLP_Item(byte[] bytes) {
this.bytes = bytes != null ? bytes : new byte[0];
}
}
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<>();
}
}
private RLP() {}
public static byte[] encode(RLP_Data data) {
if (data instanceof RLP_Item item) {
return encodeItem(item.bytes);
} else if (data instanceof RLP_List list) {
return encodeList(list.items);
}
throw new IllegalArgumentException("Unsupported RLP type");
}
private static byte[] encodeItem(byte[] bytes) {
if (bytes.length == 1 && (bytes[0] & 0xFF) <= 0x7F) {
return bytes;
}
return prefixData(bytes, 0x80, 0xB7);
}
private static byte[] encodeList(List<RLP_Data> items) {
if (items.isEmpty()) {
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;
for (int i = 0; i < items.size(); i++) {
encodedChildren[i] = encode(items.get(i));
totalPayloadLength += 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];
System.arraycopy(prefix, 0, result, 0, prefix.length);
int writePtr = prefix.length;
for (byte[] child : encodedChildren) {
System.arraycopy(child, 0, result, writePtr, child.length);
writePtr += child.length;
}
return result;
}
private static byte[] prefixData(byte[] payload, int shortOffset, int longOffset) {
byte[] prefix = prefixLength(payload.length, shortOffset, longOffset);
byte[] result = new byte[prefix.length + payload.length];
System.arraycopy(prefix, 0, result, 0, prefix.length);
System.arraycopy(payload, 0, result, prefix.length, payload.length);
return result;
}
private static byte[] prefixLength(int length, int shortOffset, int longOffset) {
if (length <= 55) {
return new byte[] { (byte) (shortOffset + length) };
}
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;
}
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];
for (int i = size - 1; i >= 0; i--) {
sigBytes[i] = (byte) (val & 0xFF);
val >>>= 8;
}
return sigBytes;
}
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) {
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;
}
return new RLP_List(elements);
}
private static int bigEndianToInt(byte[] bytes, int start, int end) {
int result = 0;
for (int i = start; i < end; i++) {
result = (result << 8) | (bytes[i] & 0xFF);
}
return result;
}
}
+24 -3
View File
@@ -13,7 +13,8 @@
mods() ->
#{"base64" => fun base64/0,
"base58" => fun base58/0}.
"base58" => fun base58/0,
"rlp" => fun rlp/0}.
-spec start(ArgV) -> ok
@@ -99,8 +100,8 @@ base58() ->
ok = filelib:ensure_dir(TestFile),
ok = file:write_file(TestFile, rand:bytes(rand:uniform(5000))),
{ok, Bytes} = file:read_file(TestFile),
Base64 = base58:binary_to_base58(Bytes),
ok = file:write_file(ConvFile, Base64),
Base58 = base58:binary_to_base58(Bytes),
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),
@@ -112,3 +113,23 @@ base58() ->
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.
rlp() ->
Anchor = <<"I thought what I'd do was, I'd pretend I was one of those deaf-mutes.">>,
Data =
[rand:bytes(rand:uniform(20)),
[rand:bytes(rand:uniform(20)),
Anchor,
rand:bytes(rand:uniform(20)),
rand:bytes(rand:uniform(5000))],
rand:bytes(rand:uniform(2000))],
RLP = gmser_rlp:encode(Data),
RLP_File = filename:join(temp_dir(), "rlp.test"),
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.