@leofcoin/binary 1.0.0 → 1.1.0
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 -8
- package/exports/framing.d.ts +34 -0
- package/exports/framing.js +221 -0
- package/exports/index.d.ts +2 -0
- package/exports/index.js +2 -0
- package/exports/value.d.ts +18 -0
- package/exports/value.js +289 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
`@leofcoin/binary` is a dependency-free shared implementation of unsigned
|
|
4
4
|
base-128 integers and length-prefixed byte frames. Numeric values retain the
|
|
5
|
-
existing
|
|
6
|
-
|
|
5
|
+
existing low-level framing bytes. The package also provides versioned typed-value
|
|
6
|
+
encoding for the application protocol described in [the wire specification](../../docs/binary-wire-protocol.md).
|
|
7
7
|
|
|
8
8
|
It is independent of `@leofcoin/codecs`, which remains the registry of codec
|
|
9
9
|
identifiers. This package does not import or re-export that registry.
|
|
@@ -35,14 +35,53 @@ independent copy before resetting/reusing a writer. Writers have fixed capacity;
|
|
|
35
35
|
the caller supplies the capacity or an existing Uint8Array. There is no hidden
|
|
36
36
|
buffer growth or mutable process-global `.bytes` state.
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
validator framing
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
responsibility. BigInt support does not automatically migrate existing fields.
|
|
38
|
+
Reader calls reject padded/non-canonical integers by default. The typed-message
|
|
39
|
+
protocol requires canonical validator framing. An explicit `canonical: false`
|
|
40
|
+
option remains available for offline tooling, not as a network fallback.
|
|
41
|
+
Overflow and truncated fields fail closed. `decodeFrames` accepts an optional
|
|
42
|
+
`maxFrames` policy; network size/membership limits remain the caller's responsibility.
|
|
44
43
|
|
|
45
44
|
Checks: `npm test -w @leofcoin/binary`, `npm test -w @leofcoin/messages`.
|
|
46
45
|
Benchmark: `node test/bench/binary-codec.js`. It compares the actual installed
|
|
47
46
|
legacy framing packages and verifies equal bytes before measuring. Results are
|
|
48
47
|
local CPU timings, not blockchain throughput or transport latency claims.
|
|
48
|
+
|
|
49
|
+
## Typed-value encoder and framing
|
|
50
|
+
|
|
51
|
+
`encodeValue` validates and measures a flat per-call plan, then writes directly
|
|
52
|
+
into one exactly sized output byte buffer. Strings use allocation-free UTF-8
|
|
53
|
+
measurement and direct/encodeInto writes; BigInts use exact 49-bit limbs while
|
|
54
|
+
retaining the same unsigned-varint bytes. No scratch buffers or application
|
|
55
|
+
values are cached across calls.
|
|
56
|
+
|
|
57
|
+
`encodeValueWithPrefix(value, prefix, limits)` includes an existing codec/framing
|
|
58
|
+
header in that same allocation. The prefix is copied; `maxBytes` bounds the
|
|
59
|
+
typed value including its magic/version, separately from caller-provided framing.
|
|
60
|
+
Callers must keep framing bounded and enforce total network message limits.
|
|
61
|
+
|
|
62
|
+
Message schema compilation lives in `@leofcoin/messages`. Only frozen data-property
|
|
63
|
+
schemas cache field names, kinds and codec prefixes. Mutable caller schemas are
|
|
64
|
+
recompiled; decoded messages and caller-owned payloads are not frozen or cached.
|
|
65
|
+
|
|
66
|
+
## Reproducible performance comparison
|
|
67
|
+
|
|
68
|
+
Build first, then snapshot the **pre-change** binary/message outputs once:
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
node scripts/benchmark-wire-codecs.js --snapshot
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Keep the returned directory. After rebuilding changed codecs:
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
node --expose-gc scripts/benchmark-wire-codecs.js <snapshot-directory>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Run from the repository root. The read-only benchmark checks identical bytes,
|
|
81
|
+
decoded values and block/transaction hashes before reporting timings. It uses
|
|
82
|
+
seven alternating samples, medians and source fingerprints. Message cases use
|
|
83
|
+
the actual BlockMessage/TransactionMessage classes with representative encoding
|
|
84
|
+
fixtures; they are not valid-consensus-block or end-to-end throughput benchmarks.
|
|
85
|
+
Snapshots are ignored generated artifacts under `node_modules/.cache`; never
|
|
86
|
+
refresh the baseline midway through a comparison. Rebuild dependencies and run
|
|
87
|
+
multi-node verification only after benchmarks, not concurrently with them.
|
|
@@ -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[];
|
|
@@ -0,0 +1,221 @@
|
|
|
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
|
+
// Count whole eight-byte varint groups before the short tail.
|
|
27
|
+
while (value >= 0x100000000000000n) {
|
|
28
|
+
value >>= 56n;
|
|
29
|
+
length += 8;
|
|
30
|
+
}
|
|
31
|
+
while (value >= 128n) {
|
|
32
|
+
value >>= 7n;
|
|
33
|
+
length++;
|
|
34
|
+
}
|
|
35
|
+
return length;
|
|
36
|
+
}
|
|
37
|
+
/** Fixed-capacity writer. finish() is a view; copy it before reusing/resetting the writer. */
|
|
38
|
+
export class BinaryWriter {
|
|
39
|
+
#buffer;
|
|
40
|
+
#position = 0;
|
|
41
|
+
constructor(capacity = 64) {
|
|
42
|
+
if (typeof capacity === 'number') {
|
|
43
|
+
size(capacity);
|
|
44
|
+
this.#buffer = new Uint8Array(capacity);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
bytes(capacity);
|
|
48
|
+
this.#buffer = capacity;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
get position() {
|
|
52
|
+
return this.#position;
|
|
53
|
+
}
|
|
54
|
+
get remaining() {
|
|
55
|
+
return this.#buffer.length - this.#position;
|
|
56
|
+
}
|
|
57
|
+
#require(length) {
|
|
58
|
+
size(length);
|
|
59
|
+
if (length > this.remaining)
|
|
60
|
+
throw new RangeError('Binary output buffer is too small');
|
|
61
|
+
}
|
|
62
|
+
writeVarUint(value) {
|
|
63
|
+
this.#require(varUintLength(value));
|
|
64
|
+
this.#writeVarUint(value);
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
#writeVarUint(value) {
|
|
68
|
+
while (value >= 128) {
|
|
69
|
+
this.#buffer[this.#position++] = value % 128 | 128;
|
|
70
|
+
value = Math.floor(value / 128);
|
|
71
|
+
}
|
|
72
|
+
this.#buffer[this.#position++] = value;
|
|
73
|
+
}
|
|
74
|
+
writeVarBigInt(value) {
|
|
75
|
+
this.#require(varBigIntLength(value));
|
|
76
|
+
do {
|
|
77
|
+
const byte = Number(value & 127n);
|
|
78
|
+
value >>= 7n;
|
|
79
|
+
this.#buffer[this.#position++] = byte | (value ? 128 : 0);
|
|
80
|
+
} while (value);
|
|
81
|
+
return this;
|
|
82
|
+
}
|
|
83
|
+
writeBytes(value) {
|
|
84
|
+
bytes(value);
|
|
85
|
+
this.#require(value.length);
|
|
86
|
+
this.#buffer.set(value, this.#position);
|
|
87
|
+
this.#position += value.length;
|
|
88
|
+
return this;
|
|
89
|
+
}
|
|
90
|
+
writeFrame(value) {
|
|
91
|
+
bytes(value);
|
|
92
|
+
// Check the complete operation before writing its length prefix.
|
|
93
|
+
const prefixLength = varUintLength(value.length);
|
|
94
|
+
this.#require(prefixLength + value.length);
|
|
95
|
+
const prefixStart = this.#buffer.byteOffset + this.#position;
|
|
96
|
+
if (value.buffer === this.#buffer.buffer &&
|
|
97
|
+
value.byteOffset < prefixStart + prefixLength &&
|
|
98
|
+
value.byteOffset + value.length > prefixStart)
|
|
99
|
+
throw new RangeError('Frame source overlaps its length prefix');
|
|
100
|
+
this.#writeVarUint(value.length);
|
|
101
|
+
this.#buffer.set(value, this.#position);
|
|
102
|
+
this.#position += value.length;
|
|
103
|
+
return this;
|
|
104
|
+
}
|
|
105
|
+
reset() {
|
|
106
|
+
this.#position = 0;
|
|
107
|
+
return this;
|
|
108
|
+
}
|
|
109
|
+
finish(copy = false) {
|
|
110
|
+
const result = this.#buffer.subarray(0, this.#position);
|
|
111
|
+
// Buffer.slice() aliases memory; this constructor copies every Uint8Array subtype.
|
|
112
|
+
return copy ? new Uint8Array(result) : result;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** Reads views of the original buffer. Failed operations leave the cursor unchanged. */
|
|
116
|
+
export class BinaryReader {
|
|
117
|
+
#buffer;
|
|
118
|
+
#position = 0;
|
|
119
|
+
constructor(buffer) {
|
|
120
|
+
bytes(buffer);
|
|
121
|
+
this.#buffer = buffer;
|
|
122
|
+
}
|
|
123
|
+
get position() {
|
|
124
|
+
return this.#position;
|
|
125
|
+
}
|
|
126
|
+
get remaining() {
|
|
127
|
+
return this.#buffer.length - this.#position;
|
|
128
|
+
}
|
|
129
|
+
readVarUint(canonical = true) {
|
|
130
|
+
let result = 0;
|
|
131
|
+
let multiplier = 1;
|
|
132
|
+
for (let index = 0; index < 8; index++) {
|
|
133
|
+
const cursor = this.#position + index;
|
|
134
|
+
if (cursor >= this.#buffer.length)
|
|
135
|
+
throw new RangeError('Truncated varint');
|
|
136
|
+
const byte = this.#buffer[cursor];
|
|
137
|
+
const payload = byte & 127;
|
|
138
|
+
if (index === 7 && payload > Math.floor((Number.MAX_SAFE_INTEGER - result) / multiplier))
|
|
139
|
+
throw new RangeError('Varint exceeds safe integer range');
|
|
140
|
+
result += payload * multiplier;
|
|
141
|
+
if (byte < 128) {
|
|
142
|
+
if (canonical && index > 0 && payload === 0)
|
|
143
|
+
throw new RangeError('Non-canonical varint');
|
|
144
|
+
this.#position = cursor + 1;
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
multiplier *= 128;
|
|
148
|
+
}
|
|
149
|
+
throw new RangeError('Varint exceeds safe integer range');
|
|
150
|
+
}
|
|
151
|
+
readVarBigInt(canonical = true) {
|
|
152
|
+
let result = 0n;
|
|
153
|
+
for (let index = 0; index < 37; index++) {
|
|
154
|
+
const cursor = this.#position + index;
|
|
155
|
+
if (cursor >= this.#buffer.length)
|
|
156
|
+
throw new RangeError('Truncated varint');
|
|
157
|
+
const byte = this.#buffer[cursor];
|
|
158
|
+
result |= BigInt(byte & 127) << BigInt(index * 7);
|
|
159
|
+
if (result > MAX_BIGINT)
|
|
160
|
+
throw new RangeError('Varint exceeds 256 bits');
|
|
161
|
+
if (byte < 128) {
|
|
162
|
+
if (canonical && index > 0 && byte === 0)
|
|
163
|
+
throw new RangeError('Non-canonical varint');
|
|
164
|
+
this.#position = cursor + 1;
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
throw new RangeError('Varint exceeds 256 bits');
|
|
169
|
+
}
|
|
170
|
+
readBytes(length) {
|
|
171
|
+
size(length);
|
|
172
|
+
if (length > this.remaining)
|
|
173
|
+
throw new RangeError('Truncated binary field');
|
|
174
|
+
const start = this.#position;
|
|
175
|
+
this.#position += length;
|
|
176
|
+
return this.#buffer.subarray(start, this.#position);
|
|
177
|
+
}
|
|
178
|
+
readFrame(canonical = true) {
|
|
179
|
+
const start = this.#position;
|
|
180
|
+
try {
|
|
181
|
+
return this.readBytes(this.readVarUint(canonical));
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
this.#position = start;
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/** Existing smart-concat bytes, with exactly one output allocation and no prefix arrays. */
|
|
190
|
+
export function encodeFrames(frames, prefix) {
|
|
191
|
+
if (!Array.isArray(frames))
|
|
192
|
+
throw new TypeError('Expected an array of byte frames');
|
|
193
|
+
if (prefix !== undefined)
|
|
194
|
+
bytes(prefix);
|
|
195
|
+
let length = prefix?.length ?? 0;
|
|
196
|
+
for (const frame of frames) {
|
|
197
|
+
bytes(frame);
|
|
198
|
+
length += varUintLength(frame.length) + frame.length;
|
|
199
|
+
size(length);
|
|
200
|
+
}
|
|
201
|
+
const writer = new BinaryWriter(length);
|
|
202
|
+
if (prefix)
|
|
203
|
+
writer.writeBytes(prefix);
|
|
204
|
+
for (const frame of frames)
|
|
205
|
+
writer.writeFrame(frame);
|
|
206
|
+
return writer.finish();
|
|
207
|
+
}
|
|
208
|
+
/** Iterative framing: bounded by the input length, not recursive stack depth. */
|
|
209
|
+
export function decodeFrames(buffer, prefixLength = 0, options = {}) {
|
|
210
|
+
const reader = new BinaryReader(buffer);
|
|
211
|
+
reader.readBytes(prefixLength);
|
|
212
|
+
const maxFrames = options.maxFrames ?? buffer.length;
|
|
213
|
+
size(maxFrames);
|
|
214
|
+
const frames = [];
|
|
215
|
+
while (reader.remaining) {
|
|
216
|
+
if (frames.length >= maxFrames)
|
|
217
|
+
throw new RangeError('Too many binary frames');
|
|
218
|
+
frames.push(reader.readFrame(options.canonical ?? true));
|
|
219
|
+
}
|
|
220
|
+
return frames;
|
|
221
|
+
}
|
package/exports/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface ValueLimits {
|
|
2
|
+
maxBytes?: number;
|
|
3
|
+
maxDepth?: number;
|
|
4
|
+
maxNodes?: number;
|
|
5
|
+
}
|
|
6
|
+
export declare const BINARY_CONTENT_TYPE = "application/octet-stream";
|
|
7
|
+
/** Versioned canonical typed values. Validation/planning allocates no byte buffers. */
|
|
8
|
+
export declare function encodeValue(value: unknown, options?: ValueLimits): Uint8Array;
|
|
9
|
+
/** Prepend an existing framing/codec header in the same allocation.
|
|
10
|
+
* maxBytes bounds the typed value (including MAGIC), as with encodeValue.
|
|
11
|
+
* The caller-owned prefix is separate framing and is copied, never retained.
|
|
12
|
+
*/
|
|
13
|
+
export declare function encodeValueWithPrefix(value: unknown, prefix: Uint8Array, options?: ValueLimits): Uint8Array;
|
|
14
|
+
/** Strict decoder: byte input, version, canonical ordering, bounds and complete consumption. */
|
|
15
|
+
export declare function decodeValue(input: Uint8Array, options?: ValueLimits): any;
|
|
16
|
+
/** Domain separation prevents RPC bytes from being accepted as worker/chat/peer messages. */
|
|
17
|
+
export declare const encodeWire: (channel: string, value: unknown, limits?: ValueLimits) => Uint8Array<ArrayBufferLike>;
|
|
18
|
+
export declare function decodeWire(channel: string, bytes: Uint8Array, limits?: ValueLimits): any;
|
package/exports/value.js
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { BinaryReader, varUintLength, varBigIntLength } from './framing.js';
|
|
2
|
+
export const BINARY_CONTENT_TYPE = 'application/octet-stream';
|
|
3
|
+
const MAGIC = new Uint8Array([0x4c, 0x46, 0x43, 1]);
|
|
4
|
+
const utf8 = new TextEncoder();
|
|
5
|
+
const text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
|
|
6
|
+
const defaults = { maxBytes: 16 * 1024 * 1024, maxDepth: 64, maxNodes: 200000 };
|
|
7
|
+
const BIGINT_LIMB = 1n << 49n;
|
|
8
|
+
const BIGINT_MASK = BIGINT_LIMB - 1n;
|
|
9
|
+
const limitsFor = (options) => {
|
|
10
|
+
const limits = { ...defaults, ...options };
|
|
11
|
+
for (const value of Object.values(limits))
|
|
12
|
+
if (!Number.isSafeInteger(value) || value < 1)
|
|
13
|
+
throw new RangeError('Invalid binary limits');
|
|
14
|
+
return limits;
|
|
15
|
+
};
|
|
16
|
+
/** Canonical UTF-8 length without allocating a temporary encoded string. */
|
|
17
|
+
function stringLength(value, maximum) {
|
|
18
|
+
if (value.length > maximum)
|
|
19
|
+
throw new RangeError('Invalid binary string');
|
|
20
|
+
let length = value.length;
|
|
21
|
+
for (let index = 0; index < value.length; index++) {
|
|
22
|
+
const code = value.charCodeAt(index);
|
|
23
|
+
if (code < 0x80)
|
|
24
|
+
continue;
|
|
25
|
+
if (code < 0x800) {
|
|
26
|
+
length++;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
30
|
+
const low = value.charCodeAt(++index);
|
|
31
|
+
if (!(low >= 0xdc00 && low <= 0xdfff))
|
|
32
|
+
throw new RangeError('Invalid binary string');
|
|
33
|
+
length += 2;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
if (code >= 0xdc00 && code <= 0xdfff)
|
|
37
|
+
throw new RangeError('Invalid binary string');
|
|
38
|
+
length += 2;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return length;
|
|
42
|
+
}
|
|
43
|
+
/** Versioned canonical typed values. Validation/planning allocates no byte buffers. */
|
|
44
|
+
export function encodeValue(value, options = {}) {
|
|
45
|
+
return encodeValueWithPrefix(value, EMPTY, options);
|
|
46
|
+
}
|
|
47
|
+
const EMPTY = new Uint8Array();
|
|
48
|
+
/** Prepend an existing framing/codec header in the same allocation.
|
|
49
|
+
* maxBytes bounds the typed value (including MAGIC), as with encodeValue.
|
|
50
|
+
* The caller-owned prefix is separate framing and is copied, never retained.
|
|
51
|
+
*/
|
|
52
|
+
export function encodeValueWithPrefix(value, prefix, options = {}) {
|
|
53
|
+
if (!(prefix instanceof Uint8Array))
|
|
54
|
+
throw new TypeError('Expected Uint8Array prefix');
|
|
55
|
+
const limits = limitsFor(options);
|
|
56
|
+
// Flat plan retains validated data-property values; writing never reads user properties again.
|
|
57
|
+
// Internal tag 11 is an untagged map key and is never emitted on the wire.
|
|
58
|
+
const plan = [];
|
|
59
|
+
const ancestors = new Set();
|
|
60
|
+
let size = MAGIC.length;
|
|
61
|
+
let nodes = 0;
|
|
62
|
+
const reserve = (length) => {
|
|
63
|
+
size += length;
|
|
64
|
+
if (size > limits.maxBytes)
|
|
65
|
+
throw new RangeError('Binary value exceeds byte limit');
|
|
66
|
+
};
|
|
67
|
+
const string = (value, tag) => {
|
|
68
|
+
const length = stringLength(value, limits.maxBytes);
|
|
69
|
+
reserve((tag === 7 ? 1 : 0) + varUintLength(length) + length);
|
|
70
|
+
plan.push(tag, value, length);
|
|
71
|
+
};
|
|
72
|
+
const visit = (value, depth) => {
|
|
73
|
+
if (++nodes > limits.maxNodes || depth > limits.maxDepth)
|
|
74
|
+
throw new RangeError('Binary value exceeds complexity limit');
|
|
75
|
+
if (value === undefined || value === null || typeof value === 'boolean') {
|
|
76
|
+
reserve(1);
|
|
77
|
+
plan.push(value === undefined ? 0 : value === null ? 1 : value === false ? 2 : 3);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (typeof value === 'number') {
|
|
81
|
+
if (!Number.isFinite(value))
|
|
82
|
+
throw new TypeError('Binary numbers must be finite');
|
|
83
|
+
reserve(9);
|
|
84
|
+
plan.push(4, value);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (typeof value === 'bigint') {
|
|
88
|
+
const magnitude = value < 0n ? -value : value;
|
|
89
|
+
reserve(1 + varBigIntLength(magnitude));
|
|
90
|
+
plan.push(value < 0n ? 6 : 5, magnitude);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (typeof value === 'string')
|
|
94
|
+
return string(value, 7);
|
|
95
|
+
if (value instanceof Uint8Array) {
|
|
96
|
+
reserve(1 + varUintLength(value.length) + value.length);
|
|
97
|
+
plan.push(8, value, value.length);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (typeof value !== 'object')
|
|
101
|
+
throw new TypeError('Unsupported binary value');
|
|
102
|
+
if (ancestors.has(value))
|
|
103
|
+
throw new TypeError('Cyclic binary value');
|
|
104
|
+
ancestors.add(value);
|
|
105
|
+
try {
|
|
106
|
+
if (Array.isArray(value)) {
|
|
107
|
+
if (value.length > limits.maxNodes - nodes)
|
|
108
|
+
throw new RangeError('Binary array exceeds node limit');
|
|
109
|
+
if (Object.keys(value).length !== value.length || Object.getOwnPropertySymbols(value).length)
|
|
110
|
+
throw new TypeError('Binary arrays cannot have extra properties or holes');
|
|
111
|
+
reserve(1 + varUintLength(value.length));
|
|
112
|
+
plan.push(9, value.length);
|
|
113
|
+
for (let index = 0; index < value.length; index++) {
|
|
114
|
+
const entry = Object.getOwnPropertyDescriptor(value, String(index));
|
|
115
|
+
if (!entry || !('value' in entry))
|
|
116
|
+
throw new TypeError('Sparse arrays and accessors are not binary values');
|
|
117
|
+
visit(entry.value, depth + 1);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
const prototype = Object.getPrototypeOf(value);
|
|
122
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
123
|
+
throw new TypeError('Binary maps must be plain objects');
|
|
124
|
+
const keys = Object.keys(value).sort();
|
|
125
|
+
if (Object.getOwnPropertySymbols(value).length)
|
|
126
|
+
throw new TypeError('Symbol keys are not binary values');
|
|
127
|
+
if (keys.length > limits.maxNodes - nodes)
|
|
128
|
+
throw new RangeError('Binary map exceeds node limit');
|
|
129
|
+
reserve(1 + varUintLength(keys.length));
|
|
130
|
+
plan.push(10, keys.length);
|
|
131
|
+
for (const key of keys) {
|
|
132
|
+
const entry = Object.getOwnPropertyDescriptor(value, key);
|
|
133
|
+
if (!('value' in entry))
|
|
134
|
+
throw new TypeError('Accessors are not binary values');
|
|
135
|
+
string(key, 11);
|
|
136
|
+
visit(entry.value, depth + 1);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
ancestors.delete(value);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
visit(value, 0);
|
|
145
|
+
const capacity = prefix.length + size;
|
|
146
|
+
if (!Number.isSafeInteger(capacity))
|
|
147
|
+
throw new RangeError('Binary output is too large');
|
|
148
|
+
const output = new Uint8Array(capacity);
|
|
149
|
+
output.set(prefix);
|
|
150
|
+
output.set(MAGIC, prefix.length);
|
|
151
|
+
let position = prefix.length + MAGIC.length;
|
|
152
|
+
let view;
|
|
153
|
+
const uint = (value) => {
|
|
154
|
+
while (value >= 128) {
|
|
155
|
+
output[position++] = value % 128 | 128;
|
|
156
|
+
value = Math.floor(value / 128);
|
|
157
|
+
}
|
|
158
|
+
output[position++] = value;
|
|
159
|
+
};
|
|
160
|
+
for (let cursor = 0; cursor < plan.length;) {
|
|
161
|
+
const tag = plan[cursor++];
|
|
162
|
+
if (tag !== 11)
|
|
163
|
+
output[position++] = tag;
|
|
164
|
+
if (tag < 4)
|
|
165
|
+
continue;
|
|
166
|
+
if (tag === 4) {
|
|
167
|
+
view ??= new DataView(output.buffer);
|
|
168
|
+
view.setFloat64(position, plan[cursor++], false);
|
|
169
|
+
position += 8;
|
|
170
|
+
}
|
|
171
|
+
else if (tag === 5 || tag === 6) {
|
|
172
|
+
let value = plan[cursor++];
|
|
173
|
+
// Seven varint bytes fit exactly in a safe-integer Number (49 bits).
|
|
174
|
+
// Extract one BigInt limb rather than shifting/masking BigInt for every byte.
|
|
175
|
+
while (value >= BIGINT_LIMB) {
|
|
176
|
+
let limb = Number(value & BIGINT_MASK);
|
|
177
|
+
for (let index = 0; index < 7; index++) {
|
|
178
|
+
output[position++] = limb % 128 | 128;
|
|
179
|
+
limb = Math.floor(limb / 128);
|
|
180
|
+
}
|
|
181
|
+
value >>= 49n;
|
|
182
|
+
}
|
|
183
|
+
uint(Number(value));
|
|
184
|
+
}
|
|
185
|
+
else if (tag === 7 || tag === 11) {
|
|
186
|
+
const value = plan[cursor++];
|
|
187
|
+
const length = plan[cursor++];
|
|
188
|
+
uint(length);
|
|
189
|
+
if (length === value.length) {
|
|
190
|
+
for (let index = 0; index < length; index++)
|
|
191
|
+
output[position++] = value.charCodeAt(index);
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
const result = utf8.encodeInto(value, output.subarray(position, position + length));
|
|
195
|
+
if (result.read !== value.length || result.written !== length)
|
|
196
|
+
throw new Error('UTF-8 planning mismatch');
|
|
197
|
+
position += length;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
else if (tag === 8) {
|
|
201
|
+
const value = plan[cursor++];
|
|
202
|
+
const length = plan[cursor++];
|
|
203
|
+
if (value.length !== length)
|
|
204
|
+
throw new RangeError('Byte input changed while encoding');
|
|
205
|
+
uint(length);
|
|
206
|
+
output.set(value, position);
|
|
207
|
+
position += length;
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
uint(plan[cursor++]);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (position !== capacity)
|
|
214
|
+
throw new Error('Binary planning mismatch');
|
|
215
|
+
return output;
|
|
216
|
+
}
|
|
217
|
+
/** Strict decoder: byte input, version, canonical ordering, bounds and complete consumption. */
|
|
218
|
+
export function decodeValue(input, options = {}) {
|
|
219
|
+
const limits = limitsFor(options);
|
|
220
|
+
if (!(input instanceof Uint8Array))
|
|
221
|
+
throw new TypeError('Wire payload must be Uint8Array');
|
|
222
|
+
if (input.length > limits.maxBytes)
|
|
223
|
+
throw new RangeError('Binary value exceeds byte limit');
|
|
224
|
+
const reader = new BinaryReader(input);
|
|
225
|
+
if (reader.readBytes(MAGIC.length).some((byte, index) => byte !== MAGIC[index]))
|
|
226
|
+
throw new TypeError('Unsupported binary wire version');
|
|
227
|
+
let nodes = 0;
|
|
228
|
+
const string = () => text.decode(reader.readFrame());
|
|
229
|
+
const read = (depth) => {
|
|
230
|
+
if (++nodes > limits.maxNodes || depth > limits.maxDepth)
|
|
231
|
+
throw new RangeError('Binary value exceeds complexity limit');
|
|
232
|
+
const tag = reader.readBytes(1)[0];
|
|
233
|
+
if (tag === 0)
|
|
234
|
+
return undefined;
|
|
235
|
+
if (tag === 1)
|
|
236
|
+
return null;
|
|
237
|
+
if (tag === 2)
|
|
238
|
+
return false;
|
|
239
|
+
if (tag === 3)
|
|
240
|
+
return true;
|
|
241
|
+
if (tag === 4) {
|
|
242
|
+
const bytes = reader.readBytes(8);
|
|
243
|
+
const value = new DataView(bytes.buffer, bytes.byteOffset, 8).getFloat64(0, false);
|
|
244
|
+
if (!Number.isFinite(value))
|
|
245
|
+
throw new TypeError('Binary numbers must be finite');
|
|
246
|
+
return value;
|
|
247
|
+
}
|
|
248
|
+
if (tag === 5 || tag === 6) {
|
|
249
|
+
const value = reader.readVarBigInt();
|
|
250
|
+
if (tag === 6 && value === 0n)
|
|
251
|
+
throw new TypeError('Non-canonical negative zero');
|
|
252
|
+
return tag === 6 ? -value : value;
|
|
253
|
+
}
|
|
254
|
+
if (tag === 7)
|
|
255
|
+
return string();
|
|
256
|
+
if (tag === 8)
|
|
257
|
+
return new Uint8Array(reader.readFrame());
|
|
258
|
+
if (tag === 9 || tag === 10) {
|
|
259
|
+
const count = reader.readVarUint();
|
|
260
|
+
if (count > limits.maxNodes - nodes || count > reader.remaining)
|
|
261
|
+
throw new RangeError('Invalid binary collection size');
|
|
262
|
+
if (tag === 9)
|
|
263
|
+
return Array.from({ length: count }, () => read(depth + 1));
|
|
264
|
+
const result = {};
|
|
265
|
+
let previous;
|
|
266
|
+
for (let index = 0; index < count; index++) {
|
|
267
|
+
const key = string();
|
|
268
|
+
if (previous !== undefined && key <= previous)
|
|
269
|
+
throw new TypeError('Non-canonical binary map keys');
|
|
270
|
+
previous = key;
|
|
271
|
+
Object.defineProperty(result, key, { value: read(depth + 1), enumerable: true, writable: true, configurable: true });
|
|
272
|
+
}
|
|
273
|
+
return result;
|
|
274
|
+
}
|
|
275
|
+
throw new TypeError('Unknown binary value tag');
|
|
276
|
+
};
|
|
277
|
+
const value = read(0);
|
|
278
|
+
if (reader.remaining)
|
|
279
|
+
throw new TypeError('Trailing binary data');
|
|
280
|
+
return value;
|
|
281
|
+
}
|
|
282
|
+
/** Domain separation prevents RPC bytes from being accepted as worker/chat/peer messages. */
|
|
283
|
+
export const encodeWire = (channel, value, limits) => encodeValue([channel, value], limits);
|
|
284
|
+
export function decodeWire(channel, bytes, limits) {
|
|
285
|
+
const envelope = decodeValue(bytes, limits);
|
|
286
|
+
if (!Array.isArray(envelope) || envelope.length !== 2 || envelope[0] !== channel)
|
|
287
|
+
throw new TypeError('Wrong binary message channel');
|
|
288
|
+
return envelope[1];
|
|
289
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leofcoin/binary",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Dependency-free binary Reader/Writer, unsigned varints and byte framing",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
],
|
|
20
20
|
"scripts": {
|
|
21
21
|
"build": "tsc6 -p tsconfig.json",
|
|
22
|
+
"prepack": "npm run build",
|
|
22
23
|
"test": "node --test"
|
|
23
24
|
},
|
|
24
25
|
"devDependencies": {
|