WIP
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user