WIP
This commit is contained in:
@@ -20,6 +20,9 @@ public class Testinator {
|
|||||||
case "base58" -> {
|
case "base58" -> {
|
||||||
System.out.print(base58(args[1]));
|
System.out.print(base58(args[1]));
|
||||||
}
|
}
|
||||||
|
case "rlp" -> {
|
||||||
|
System.out.print(rlp(args[1]));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
System.err.println("Error: " + e.getMessage());
|
System.err.println("Error: " + e.getMessage());
|
||||||
@@ -53,4 +56,14 @@ public class Testinator {
|
|||||||
Files.write(decPath, decBytes);
|
Files.write(decPath, decBytes);
|
||||||
return encPath.toString() + " " + decPath.toString();
|
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() ->
|
mods() ->
|
||||||
#{"base64" => fun base64/0,
|
#{"base64" => fun base64/0,
|
||||||
"base58" => fun base58/0}.
|
"base58" => fun base58/0,
|
||||||
|
"rlp" => fun rlp/0}.
|
||||||
|
|
||||||
|
|
||||||
-spec start(ArgV) -> ok
|
-spec start(ArgV) -> ok
|
||||||
@@ -99,8 +100,8 @@ base58() ->
|
|||||||
ok = filelib:ensure_dir(TestFile),
|
ok = filelib:ensure_dir(TestFile),
|
||||||
ok = file:write_file(TestFile, rand:bytes(rand:uniform(5000))),
|
ok = file:write_file(TestFile, rand:bytes(rand:uniform(5000))),
|
||||||
{ok, Bytes} = file:read_file(TestFile),
|
{ok, Bytes} = file:read_file(TestFile),
|
||||||
Base64 = base58:binary_to_base58(Bytes),
|
Base58 = base58:binary_to_base58(Bytes),
|
||||||
ok = file:write_file(ConvFile, Base64),
|
ok = file:write_file(ConvFile, Base58),
|
||||||
Run = unicode:characters_to_list(["bin/run base58 ", temp_dir()]),
|
Run = unicode:characters_to_list(["bin/run base58 ", temp_dir()]),
|
||||||
[JavaEncPath, JavaDecPath] = string:split(os:cmd(Run), " "),
|
[JavaEncPath, JavaDecPath] = string:split(os:cmd(Run), " "),
|
||||||
{ok, E_Enc_Bytes} = file:read_file(ConvFile),
|
{ok, E_Enc_Bytes} = file:read_file(ConvFile),
|
||||||
@@ -112,3 +113,23 @@ base58() ->
|
|||||||
E_BinHash = crypto:hash(sha512, E_Dec_Bytes),
|
E_BinHash = crypto:hash(sha512, E_Dec_Bytes),
|
||||||
J_BinHash = crypto:hash(sha512, J_Dec_Bytes),
|
J_BinHash = crypto:hash(sha512, J_Dec_Bytes),
|
||||||
E_Hash =:= J_Hash andalso E_BinHash =:= J_BinHash.
|
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