@leofcoin/binary 1.0.0 → 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/exports/index.d.ts +34 -0
- package/exports/index.js +216 -0
- package/package.json +2 -1
|
@@ -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,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leofcoin/binary",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
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": {
|