@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 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 wire bytes. No chain field, codec identifier, hash algorithm or supply
6
- representation changes as part of this migration.
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
- 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.
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
+ }
@@ -0,0 +1,2 @@
1
+ export * from './framing.js';
2
+ export * from './value.js';
@@ -0,0 +1,2 @@
1
+ export * from './framing.js';
2
+ export * from './value.js';
@@ -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;
@@ -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.0.0",
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": {