@leofcoin/binary 0.0.0-stage → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -2
- package/exports/index.d.ts +34 -0
- package/exports/index.js +216 -0
- package/package.json +32 -4
package/README.md
CHANGED
|
@@ -1,3 +1,48 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @leofcoin/binary
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`@leofcoin/binary` is a dependency-free shared implementation of unsigned
|
|
4
|
+
base-128 integers and length-prefixed byte frames. Numeric values retain the
|
|
5
|
+
existing wire bytes. No chain field, codec identifier, hash algorithm or supply
|
|
6
|
+
representation changes as part of this migration.
|
|
7
|
+
|
|
8
|
+
It is independent of `@leofcoin/codecs`, which remains the registry of codec
|
|
9
|
+
identifiers. This package does not import or re-export that registry.
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import { BinaryReader, BinaryWriter, varUintLength } from '@leofcoin/binary'
|
|
13
|
+
|
|
14
|
+
const signature = new Uint8Array(64)
|
|
15
|
+
const writer = new BinaryWriter(varUintLength(1) + varUintLength(129) + signature.length)
|
|
16
|
+
writer.writeVarUint(1).writeVarUint(129).writeBytes(signature)
|
|
17
|
+
const reader = new BinaryReader(writer.finish())
|
|
18
|
+
const version = reader.readVarUint()
|
|
19
|
+
const codec = reader.readVarUint()
|
|
20
|
+
const decodedSignature = reader.readBytes(reader.remaining)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- `writeVarUint` / `readVarUint`: unsigned safe-integer Numbers, at most 8 bytes.
|
|
24
|
+
- `writeVarBigInt` / `readVarBigInt`: unsigned BigInts through `2^256-1`, at most 37 bytes.
|
|
25
|
+
- `writeFrame` / `readFrame`: Number length prefix plus bytes. A failed operation
|
|
26
|
+
does not advance the cursor. Writes check capacity before changing bytes.
|
|
27
|
+
- `encodeFrames` / `decodeFrames`: a sequence of those frames; optional raw prefix
|
|
28
|
+
bytes on encode, corresponding prefix length on decode. Encoding allocates one
|
|
29
|
+
output buffer, without allocating a varint array for every field. Decoding is
|
|
30
|
+
iterative, so many fields do not exhaust the call stack.
|
|
31
|
+
|
|
32
|
+
Readers return **views**, not copies; modifying a view modifies the input buffer.
|
|
33
|
+
`writer.finish()` likewise returns a view. Use `finish(true)` to retain an
|
|
34
|
+
independent copy before resetting/reusing a writer. Writers have fixed capacity;
|
|
35
|
+
the caller supplies the capacity or an existing Uint8Array. There is no hidden
|
|
36
|
+
buffer growth or mutable process-global `.bytes` state.
|
|
37
|
+
|
|
38
|
+
New reader calls reject padded/non-canonical integers by default. Existing block
|
|
39
|
+
validator framing explicitly uses `canonical: false` to preserve legacy padded
|
|
40
|
+
prefix acceptance. Tightening that consensus rule needs a separate protocol
|
|
41
|
+
decision. Overflow and truncated fields still fail closed. `decodeFrames` accepts
|
|
42
|
+
an optional `maxFrames` policy; network size/membership limits remain the caller's
|
|
43
|
+
responsibility. BigInt support does not automatically migrate existing fields.
|
|
44
|
+
|
|
45
|
+
Checks: `npm test -w @leofcoin/binary`, `npm test -w @leofcoin/messages`.
|
|
46
|
+
Benchmark: `node test/bench/binary-codec.js`. It compares the actual installed
|
|
47
|
+
legacy framing packages and verifies equal bytes before measuring. Results are
|
|
48
|
+
local CPU timings, not blockchain throughput or transport latency claims.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export declare function varUintLength(value: number): number;
|
|
2
|
+
export declare function varBigIntLength(value: bigint): number;
|
|
3
|
+
/** Fixed-capacity writer. finish() is a view; copy it before reusing/resetting the writer. */
|
|
4
|
+
export declare class BinaryWriter {
|
|
5
|
+
#private;
|
|
6
|
+
constructor(capacity?: number | Uint8Array);
|
|
7
|
+
get position(): number;
|
|
8
|
+
get remaining(): number;
|
|
9
|
+
writeVarUint(value: number): this;
|
|
10
|
+
writeVarBigInt(value: bigint): this;
|
|
11
|
+
writeBytes(value: Uint8Array): this;
|
|
12
|
+
writeFrame(value: Uint8Array): this;
|
|
13
|
+
reset(): this;
|
|
14
|
+
finish(copy?: boolean): Uint8Array;
|
|
15
|
+
}
|
|
16
|
+
/** Reads views of the original buffer. Failed operations leave the cursor unchanged. */
|
|
17
|
+
export declare class BinaryReader {
|
|
18
|
+
#private;
|
|
19
|
+
constructor(buffer: Uint8Array);
|
|
20
|
+
get position(): number;
|
|
21
|
+
get remaining(): number;
|
|
22
|
+
readVarUint(canonical?: boolean): number;
|
|
23
|
+
readVarBigInt(canonical?: boolean): bigint;
|
|
24
|
+
readBytes(length: number): Uint8Array;
|
|
25
|
+
readFrame(canonical?: boolean): Uint8Array;
|
|
26
|
+
}
|
|
27
|
+
/** Existing smart-concat bytes, with exactly one output allocation and no prefix arrays. */
|
|
28
|
+
export declare function encodeFrames(frames: readonly Uint8Array[], prefix?: Uint8Array): Uint8Array;
|
|
29
|
+
export interface FrameOptions {
|
|
30
|
+
canonical?: boolean;
|
|
31
|
+
maxFrames?: number;
|
|
32
|
+
}
|
|
33
|
+
/** Iterative framing: bounded by the input length, not recursive stack depth. */
|
|
34
|
+
export declare function decodeFrames(buffer: Uint8Array, prefixLength?: number, options?: FrameOptions): Uint8Array[];
|
package/exports/index.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/** @leofcoin/binary: dependency-free unsigned framing, with no global byte counters. */
|
|
2
|
+
const MAX_BIGINT = (1n << 256n) - 1n;
|
|
3
|
+
function size(value) {
|
|
4
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
5
|
+
throw new RangeError('Expected an unsigned safe integer');
|
|
6
|
+
}
|
|
7
|
+
function bytes(value) {
|
|
8
|
+
if (!(value instanceof Uint8Array))
|
|
9
|
+
throw new TypeError('Expected Uint8Array');
|
|
10
|
+
}
|
|
11
|
+
export function varUintLength(value) {
|
|
12
|
+
size(value);
|
|
13
|
+
let length = 1;
|
|
14
|
+
while (value >= 128) {
|
|
15
|
+
value = Math.floor(value / 128);
|
|
16
|
+
length++;
|
|
17
|
+
}
|
|
18
|
+
return length;
|
|
19
|
+
}
|
|
20
|
+
export function varBigIntLength(value) {
|
|
21
|
+
if (typeof value !== 'bigint')
|
|
22
|
+
throw new TypeError('Expected BigInt');
|
|
23
|
+
if (value < 0n || value > MAX_BIGINT)
|
|
24
|
+
throw new RangeError('Expected an unsigned 256-bit integer');
|
|
25
|
+
let length = 1;
|
|
26
|
+
while (value >= 128n) {
|
|
27
|
+
value >>= 7n;
|
|
28
|
+
length++;
|
|
29
|
+
}
|
|
30
|
+
return length;
|
|
31
|
+
}
|
|
32
|
+
/** Fixed-capacity writer. finish() is a view; copy it before reusing/resetting the writer. */
|
|
33
|
+
export class BinaryWriter {
|
|
34
|
+
#buffer;
|
|
35
|
+
#position = 0;
|
|
36
|
+
constructor(capacity = 64) {
|
|
37
|
+
if (typeof capacity === 'number') {
|
|
38
|
+
size(capacity);
|
|
39
|
+
this.#buffer = new Uint8Array(capacity);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
bytes(capacity);
|
|
43
|
+
this.#buffer = capacity;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
get position() {
|
|
47
|
+
return this.#position;
|
|
48
|
+
}
|
|
49
|
+
get remaining() {
|
|
50
|
+
return this.#buffer.length - this.#position;
|
|
51
|
+
}
|
|
52
|
+
#require(length) {
|
|
53
|
+
size(length);
|
|
54
|
+
if (length > this.remaining)
|
|
55
|
+
throw new RangeError('Binary output buffer is too small');
|
|
56
|
+
}
|
|
57
|
+
writeVarUint(value) {
|
|
58
|
+
this.#require(varUintLength(value));
|
|
59
|
+
this.#writeVarUint(value);
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
#writeVarUint(value) {
|
|
63
|
+
while (value >= 128) {
|
|
64
|
+
this.#buffer[this.#position++] = value % 128 | 128;
|
|
65
|
+
value = Math.floor(value / 128);
|
|
66
|
+
}
|
|
67
|
+
this.#buffer[this.#position++] = value;
|
|
68
|
+
}
|
|
69
|
+
writeVarBigInt(value) {
|
|
70
|
+
this.#require(varBigIntLength(value));
|
|
71
|
+
do {
|
|
72
|
+
const byte = Number(value & 127n);
|
|
73
|
+
value >>= 7n;
|
|
74
|
+
this.#buffer[this.#position++] = byte | (value ? 128 : 0);
|
|
75
|
+
} while (value);
|
|
76
|
+
return this;
|
|
77
|
+
}
|
|
78
|
+
writeBytes(value) {
|
|
79
|
+
bytes(value);
|
|
80
|
+
this.#require(value.length);
|
|
81
|
+
this.#buffer.set(value, this.#position);
|
|
82
|
+
this.#position += value.length;
|
|
83
|
+
return this;
|
|
84
|
+
}
|
|
85
|
+
writeFrame(value) {
|
|
86
|
+
bytes(value);
|
|
87
|
+
// Check the complete operation before writing its length prefix.
|
|
88
|
+
const prefixLength = varUintLength(value.length);
|
|
89
|
+
this.#require(prefixLength + value.length);
|
|
90
|
+
const prefixStart = this.#buffer.byteOffset + this.#position;
|
|
91
|
+
if (value.buffer === this.#buffer.buffer &&
|
|
92
|
+
value.byteOffset < prefixStart + prefixLength &&
|
|
93
|
+
value.byteOffset + value.length > prefixStart)
|
|
94
|
+
throw new RangeError('Frame source overlaps its length prefix');
|
|
95
|
+
this.#writeVarUint(value.length);
|
|
96
|
+
this.#buffer.set(value, this.#position);
|
|
97
|
+
this.#position += value.length;
|
|
98
|
+
return this;
|
|
99
|
+
}
|
|
100
|
+
reset() {
|
|
101
|
+
this.#position = 0;
|
|
102
|
+
return this;
|
|
103
|
+
}
|
|
104
|
+
finish(copy = false) {
|
|
105
|
+
const result = this.#buffer.subarray(0, this.#position);
|
|
106
|
+
// Buffer.slice() aliases memory; this constructor copies every Uint8Array subtype.
|
|
107
|
+
return copy ? new Uint8Array(result) : result;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** Reads views of the original buffer. Failed operations leave the cursor unchanged. */
|
|
111
|
+
export class BinaryReader {
|
|
112
|
+
#buffer;
|
|
113
|
+
#position = 0;
|
|
114
|
+
constructor(buffer) {
|
|
115
|
+
bytes(buffer);
|
|
116
|
+
this.#buffer = buffer;
|
|
117
|
+
}
|
|
118
|
+
get position() {
|
|
119
|
+
return this.#position;
|
|
120
|
+
}
|
|
121
|
+
get remaining() {
|
|
122
|
+
return this.#buffer.length - this.#position;
|
|
123
|
+
}
|
|
124
|
+
readVarUint(canonical = true) {
|
|
125
|
+
let result = 0;
|
|
126
|
+
let multiplier = 1;
|
|
127
|
+
for (let index = 0; index < 8; index++) {
|
|
128
|
+
const cursor = this.#position + index;
|
|
129
|
+
if (cursor >= this.#buffer.length)
|
|
130
|
+
throw new RangeError('Truncated varint');
|
|
131
|
+
const byte = this.#buffer[cursor];
|
|
132
|
+
const payload = byte & 127;
|
|
133
|
+
if (index === 7 && payload > Math.floor((Number.MAX_SAFE_INTEGER - result) / multiplier))
|
|
134
|
+
throw new RangeError('Varint exceeds safe integer range');
|
|
135
|
+
result += payload * multiplier;
|
|
136
|
+
if (byte < 128) {
|
|
137
|
+
if (canonical && index > 0 && payload === 0)
|
|
138
|
+
throw new RangeError('Non-canonical varint');
|
|
139
|
+
this.#position = cursor + 1;
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
multiplier *= 128;
|
|
143
|
+
}
|
|
144
|
+
throw new RangeError('Varint exceeds safe integer range');
|
|
145
|
+
}
|
|
146
|
+
readVarBigInt(canonical = true) {
|
|
147
|
+
let result = 0n;
|
|
148
|
+
for (let index = 0; index < 37; index++) {
|
|
149
|
+
const cursor = this.#position + index;
|
|
150
|
+
if (cursor >= this.#buffer.length)
|
|
151
|
+
throw new RangeError('Truncated varint');
|
|
152
|
+
const byte = this.#buffer[cursor];
|
|
153
|
+
result |= BigInt(byte & 127) << BigInt(index * 7);
|
|
154
|
+
if (result > MAX_BIGINT)
|
|
155
|
+
throw new RangeError('Varint exceeds 256 bits');
|
|
156
|
+
if (byte < 128) {
|
|
157
|
+
if (canonical && index > 0 && byte === 0)
|
|
158
|
+
throw new RangeError('Non-canonical varint');
|
|
159
|
+
this.#position = cursor + 1;
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
throw new RangeError('Varint exceeds 256 bits');
|
|
164
|
+
}
|
|
165
|
+
readBytes(length) {
|
|
166
|
+
size(length);
|
|
167
|
+
if (length > this.remaining)
|
|
168
|
+
throw new RangeError('Truncated binary field');
|
|
169
|
+
const start = this.#position;
|
|
170
|
+
this.#position += length;
|
|
171
|
+
return this.#buffer.subarray(start, this.#position);
|
|
172
|
+
}
|
|
173
|
+
readFrame(canonical = true) {
|
|
174
|
+
const start = this.#position;
|
|
175
|
+
try {
|
|
176
|
+
return this.readBytes(this.readVarUint(canonical));
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
this.#position = start;
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/** Existing smart-concat bytes, with exactly one output allocation and no prefix arrays. */
|
|
185
|
+
export function encodeFrames(frames, prefix) {
|
|
186
|
+
if (!Array.isArray(frames))
|
|
187
|
+
throw new TypeError('Expected an array of byte frames');
|
|
188
|
+
if (prefix !== undefined)
|
|
189
|
+
bytes(prefix);
|
|
190
|
+
let length = prefix?.length ?? 0;
|
|
191
|
+
for (const frame of frames) {
|
|
192
|
+
bytes(frame);
|
|
193
|
+
length += varUintLength(frame.length) + frame.length;
|
|
194
|
+
size(length);
|
|
195
|
+
}
|
|
196
|
+
const writer = new BinaryWriter(length);
|
|
197
|
+
if (prefix)
|
|
198
|
+
writer.writeBytes(prefix);
|
|
199
|
+
for (const frame of frames)
|
|
200
|
+
writer.writeFrame(frame);
|
|
201
|
+
return writer.finish();
|
|
202
|
+
}
|
|
203
|
+
/** Iterative framing: bounded by the input length, not recursive stack depth. */
|
|
204
|
+
export function decodeFrames(buffer, prefixLength = 0, options = {}) {
|
|
205
|
+
const reader = new BinaryReader(buffer);
|
|
206
|
+
reader.readBytes(prefixLength);
|
|
207
|
+
const maxFrames = options.maxFrames ?? buffer.length;
|
|
208
|
+
size(maxFrames);
|
|
209
|
+
const frames = [];
|
|
210
|
+
while (reader.remaining) {
|
|
211
|
+
if (frames.length >= maxFrames)
|
|
212
|
+
throw new RangeError('Too many binary frames');
|
|
213
|
+
frames.push(reader.readFrame(options.canonical ?? true));
|
|
214
|
+
}
|
|
215
|
+
return frames;
|
|
216
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leofcoin/binary",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Dependency-free binary Reader/Writer, unsigned varints and byte framing",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": "./exports/index.js",
|
|
13
|
+
"types": "./exports/index.d.ts"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"exports",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc6 -p tsconfig.json",
|
|
22
|
+
"prepack": "npm run build",
|
|
23
|
+
"test": "node --test"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@typescript/native": "npm:typescript@^7.0.2",
|
|
27
|
+
"typescript": "npm:@typescript/typescript6@^6.0.2"
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/leofcoin/monorepo.git",
|
|
32
|
+
"directory": "packages/binary"
|
|
33
|
+
}
|
|
34
|
+
}
|