links-notation 0.22.0 → 0.24.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 +3 -0
- package/dist/index.js +904 -109
- package/index.d.ts +96 -0
- package/package.json +4 -4
- package/src/Binary.js +772 -0
- package/src/Link.js +3 -47
- package/src/ReferenceLiteral.js +66 -0
- package/src/grammar.pegjs +12 -2
- package/src/index.js +16 -0
- package/src/parser-generated.js +262 -89
- package/tests/Binary.test.js +165 -0
- package/tests/EmptyReference.test.js +2 -2
- package/tests/ReferenceFidelity.test.js +99 -0
- package/bun.lock +0 -213
package/src/Binary.js
ADDED
|
@@ -0,0 +1,772 @@
|
|
|
1
|
+
/** Binary links notation v1. Packet and document layers share no transport. */
|
|
2
|
+
import { Link } from './Link.js';
|
|
3
|
+
import { formatReference } from './ReferenceLiteral.js';
|
|
4
|
+
import { Parser } from './Parser.js';
|
|
5
|
+
|
|
6
|
+
const U64 = (1n << 64n) - 1n;
|
|
7
|
+
const WIDTHS = [1, 2, 4, 8];
|
|
8
|
+
function requireValue(condition, message) {
|
|
9
|
+
if (!condition) throw new RangeError(message);
|
|
10
|
+
}
|
|
11
|
+
function uint64(value) {
|
|
12
|
+
requireValue(
|
|
13
|
+
typeof value === 'bigint' || (Number.isSafeInteger(value) && value >= 0),
|
|
14
|
+
'expected uint64'
|
|
15
|
+
);
|
|
16
|
+
const result = BigInt(value);
|
|
17
|
+
requireValue(result >= 0n && result <= U64, 'uint64 overflow');
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
export class External {
|
|
21
|
+
constructor(value) {
|
|
22
|
+
this.value = uint64(value);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export class ArityRange {
|
|
26
|
+
constructor(min = 2, max = 2) {
|
|
27
|
+
this.min = uint64(min);
|
|
28
|
+
this.max = max === null ? null : uint64(max);
|
|
29
|
+
requireValue(
|
|
30
|
+
this.min > 0n &&
|
|
31
|
+
this.min <= U64 >> 4n &&
|
|
32
|
+
(this.max === null || this.max >= this.min),
|
|
33
|
+
'invalid arity'
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
contains(length) {
|
|
37
|
+
return (
|
|
38
|
+
BigInt(length) >= this.min &&
|
|
39
|
+
(this.max === null || BigInt(length) <= this.max)
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
get fixed() {
|
|
43
|
+
return this.min === this.max;
|
|
44
|
+
}
|
|
45
|
+
static parse(text) {
|
|
46
|
+
requireValue(/^[0-9]+(?:\.\.[0-9]*)?$/.test(text), 'invalid arity');
|
|
47
|
+
const parts = text.split('..');
|
|
48
|
+
return new ArityRange(
|
|
49
|
+
BigInt(parts[0]),
|
|
50
|
+
parts.at(-1) === '' ? null : BigInt(parts.at(-1))
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export class DecodeLimits {
|
|
55
|
+
constructor(values = {}) {
|
|
56
|
+
Object.assign(
|
|
57
|
+
this,
|
|
58
|
+
{
|
|
59
|
+
maxLinks: 2 ** 22,
|
|
60
|
+
maxReferences: 2 ** 24,
|
|
61
|
+
maxNodes: 2 ** 22,
|
|
62
|
+
maxStringBytes: 64 * 2 ** 20,
|
|
63
|
+
maxDepth: 64,
|
|
64
|
+
},
|
|
65
|
+
values
|
|
66
|
+
);
|
|
67
|
+
for (const key of Object.keys(this)) {
|
|
68
|
+
requireValue(
|
|
69
|
+
[
|
|
70
|
+
'maxLinks',
|
|
71
|
+
'maxReferences',
|
|
72
|
+
'maxNodes',
|
|
73
|
+
'maxStringBytes',
|
|
74
|
+
'maxDepth',
|
|
75
|
+
].includes(key),
|
|
76
|
+
'unknown limit'
|
|
77
|
+
);
|
|
78
|
+
requireValue(
|
|
79
|
+
Number.isSafeInteger(this[key]) && this[key] >= 0,
|
|
80
|
+
'invalid limit'
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
static unlimited() {
|
|
85
|
+
return new DecodeLimits({
|
|
86
|
+
maxLinks: Number.MAX_SAFE_INTEGER,
|
|
87
|
+
maxReferences: Number.MAX_SAFE_INTEGER,
|
|
88
|
+
maxNodes: Number.MAX_SAFE_INTEGER,
|
|
89
|
+
maxStringBytes: Number.MAX_SAFE_INTEGER,
|
|
90
|
+
maxDepth: Number.MAX_SAFE_INTEGER,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export class BinaryLinoOptions {
|
|
95
|
+
constructor(
|
|
96
|
+
externalReferences = false,
|
|
97
|
+
arity = new ArityRange(),
|
|
98
|
+
packedWidths = false
|
|
99
|
+
) {
|
|
100
|
+
Object.assign(this, { externalReferences, arity, packedWidths });
|
|
101
|
+
}
|
|
102
|
+
static ofPacket(packet) {
|
|
103
|
+
let min = 2,
|
|
104
|
+
max = 2;
|
|
105
|
+
for (const [, link] of packet.links()) {
|
|
106
|
+
min = Math.min(min, link.length);
|
|
107
|
+
max = Math.max(max, link.length);
|
|
108
|
+
}
|
|
109
|
+
return new BinaryLinoOptions(
|
|
110
|
+
packet.externalReferences,
|
|
111
|
+
new ArityRange(min, max),
|
|
112
|
+
new Set(packet.sections.map((s) => s.width)).size > 1
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export class Section {
|
|
117
|
+
constructor(gap, arity, width, links) {
|
|
118
|
+
Object.assign(this, { gap: uint64(gap), arity, width, links });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function referenceWidth(reference, external) {
|
|
122
|
+
const value =
|
|
123
|
+
reference instanceof External ? reference.value : uint64(reference);
|
|
124
|
+
requireValue(
|
|
125
|
+
!(reference instanceof External) || external,
|
|
126
|
+
'external references disabled'
|
|
127
|
+
);
|
|
128
|
+
for (const width of WIDTHS)
|
|
129
|
+
if (value < 1n << BigInt(width * 8 - Number(external))) return width;
|
|
130
|
+
throw new RangeError('reference exceeds capacity');
|
|
131
|
+
}
|
|
132
|
+
function leb(out, input) {
|
|
133
|
+
let value = uint64(input);
|
|
134
|
+
while (value >= 128n) {
|
|
135
|
+
out.push(Number(value & 127n) | 128);
|
|
136
|
+
value >>= 7n;
|
|
137
|
+
}
|
|
138
|
+
out.push(Number(value));
|
|
139
|
+
}
|
|
140
|
+
/** Cursor reads a single packet without consuming any following packet. */
|
|
141
|
+
export class PacketReader {
|
|
142
|
+
constructor(bytes, offset = 0) {
|
|
143
|
+
requireValue(
|
|
144
|
+
bytes instanceof Uint8Array &&
|
|
145
|
+
Number.isSafeInteger(offset) &&
|
|
146
|
+
offset >= 0 &&
|
|
147
|
+
offset <= bytes.length,
|
|
148
|
+
'invalid byte cursor'
|
|
149
|
+
);
|
|
150
|
+
this.bytes = bytes;
|
|
151
|
+
this.offset = offset;
|
|
152
|
+
}
|
|
153
|
+
raw(width) {
|
|
154
|
+
requireValue(
|
|
155
|
+
width <= this.bytes.length - this.offset,
|
|
156
|
+
'unexpected end of packet'
|
|
157
|
+
);
|
|
158
|
+
let result = 0n;
|
|
159
|
+
for (let i = 0; i < width; i++)
|
|
160
|
+
result |= BigInt(this.bytes[this.offset++]) << BigInt(i * 8);
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
leb() {
|
|
164
|
+
let value = 0n;
|
|
165
|
+
for (let shift = 0n; shift < 64n; shift += 7n) {
|
|
166
|
+
const byte = this.raw(1);
|
|
167
|
+
requireValue(shift !== 63n || (byte & 127n) <= 1n, 'LEB128 overflow');
|
|
168
|
+
value |= (byte & 127n) << shift;
|
|
169
|
+
if (byte < 128n) return value;
|
|
170
|
+
}
|
|
171
|
+
throw new RangeError('LEB128 overflow');
|
|
172
|
+
}
|
|
173
|
+
read(limits = new DecodeLimits()) {
|
|
174
|
+
if (this.offset === this.bytes.length) return null;
|
|
175
|
+
const header = Number(this.raw(1));
|
|
176
|
+
requireValue((header & 0xf0) === 0x10, 'unsupported binary version');
|
|
177
|
+
const sections = [],
|
|
178
|
+
counts = [];
|
|
179
|
+
if (header & 2) {
|
|
180
|
+
requireValue((header & 12) === 0, 'explicit header width bits set');
|
|
181
|
+
const sectionCount = this.leb();
|
|
182
|
+
requireValue(
|
|
183
|
+
sectionCount <= BigInt(limits.maxLinks),
|
|
184
|
+
'too many sections'
|
|
185
|
+
);
|
|
186
|
+
let address = 1n,
|
|
187
|
+
total = 0n;
|
|
188
|
+
for (let i = 0n; i < sectionCount; i++) {
|
|
189
|
+
const shape = this.leb(),
|
|
190
|
+
gap = shape & 4n ? this.leb() : 0n;
|
|
191
|
+
const min = shape >> 4n,
|
|
192
|
+
extra = shape & 8n ? this.leb() : null;
|
|
193
|
+
const arity = new ArityRange(
|
|
194
|
+
min,
|
|
195
|
+
extra === null ? min : extra === 0n ? null : uint64(min + extra)
|
|
196
|
+
);
|
|
197
|
+
const count = this.leb();
|
|
198
|
+
address = uint64(address + gap + count);
|
|
199
|
+
total += count;
|
|
200
|
+
requireValue(total <= BigInt(limits.maxLinks), 'too many links');
|
|
201
|
+
sections.push(new Section(gap, arity, WIDTHS[Number(shape & 3n)], []));
|
|
202
|
+
counts.push(Number(count));
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
const count = this.leb();
|
|
206
|
+
uint64(6n + count);
|
|
207
|
+
requireValue(count <= BigInt(limits.maxLinks), 'too many links');
|
|
208
|
+
if (count) {
|
|
209
|
+
sections.push(
|
|
210
|
+
new Section(5, new ArityRange(), WIDTHS[(header >> 2) & 3], [])
|
|
211
|
+
);
|
|
212
|
+
counts.push(Number(count));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
let references = 0n;
|
|
216
|
+
for (let i = 0; i < sections.length; i++) {
|
|
217
|
+
const section = sections[i];
|
|
218
|
+
for (let j = 0; j < counts[i]; j++) {
|
|
219
|
+
const length = uint64(
|
|
220
|
+
section.arity.min + (section.arity.fixed ? 0n : this.leb())
|
|
221
|
+
);
|
|
222
|
+
requireValue(section.arity.contains(length), 'link outside arity');
|
|
223
|
+
references += length;
|
|
224
|
+
requireValue(
|
|
225
|
+
references <= BigInt(limits.maxReferences),
|
|
226
|
+
'too many references'
|
|
227
|
+
);
|
|
228
|
+
const link = [],
|
|
229
|
+
top = 1n << BigInt(section.width * 8 - 1);
|
|
230
|
+
for (let k = 0n; k < length; k++) {
|
|
231
|
+
const raw = this.raw(section.width);
|
|
232
|
+
link.push(
|
|
233
|
+
header & 1 && raw >= top
|
|
234
|
+
? new External(raw === top ? 0n : (top << 1n) - raw)
|
|
235
|
+
: raw
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
section.links.push(link);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return new LinksPacket(Boolean(header & 1), sections);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
export class LinksPacket {
|
|
245
|
+
constructor(externalReferences = false, sections = []) {
|
|
246
|
+
Object.assign(this, { externalReferences, sections });
|
|
247
|
+
}
|
|
248
|
+
links() {
|
|
249
|
+
const result = [];
|
|
250
|
+
let address = 1n;
|
|
251
|
+
for (const section of this.sections) {
|
|
252
|
+
address = uint64(address + uint64(section.gap));
|
|
253
|
+
for (const link of section.links) {
|
|
254
|
+
result.push([
|
|
255
|
+
address,
|
|
256
|
+
link.map((r) => (r instanceof External ? r : uint64(r))),
|
|
257
|
+
]);
|
|
258
|
+
address = uint64(address + 1n);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
validate(limits = new DecodeLimits()) {
|
|
264
|
+
requireValue(
|
|
265
|
+
this.sections.length <= limits.maxLinks &&
|
|
266
|
+
this.links().length <= limits.maxLinks,
|
|
267
|
+
'too many links or sections'
|
|
268
|
+
);
|
|
269
|
+
let references = 0;
|
|
270
|
+
for (const s of this.sections) {
|
|
271
|
+
requireValue(WIDTHS.includes(s.width), 'invalid width');
|
|
272
|
+
new ArityRange(s.arity.min, s.arity.max);
|
|
273
|
+
for (const link of s.links) {
|
|
274
|
+
requireValue(s.arity.contains(link.length), 'link outside arity');
|
|
275
|
+
references += link.length;
|
|
276
|
+
requireValue(references <= limits.maxReferences, 'too many references');
|
|
277
|
+
for (const r of link)
|
|
278
|
+
requireValue(
|
|
279
|
+
referenceWidth(r, this.externalReferences) <= s.width,
|
|
280
|
+
'reference outside width'
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return this;
|
|
285
|
+
}
|
|
286
|
+
toBytes(limits = new DecodeLimits()) {
|
|
287
|
+
this.validate(limits);
|
|
288
|
+
const s = this.sections[0];
|
|
289
|
+
const compact =
|
|
290
|
+
!s ||
|
|
291
|
+
(this.sections.length === 1 &&
|
|
292
|
+
s.gap === 5n &&
|
|
293
|
+
s.arity.min === 2n &&
|
|
294
|
+
s.arity.max === 2n &&
|
|
295
|
+
s.links.length > 0);
|
|
296
|
+
const out = [0x10 | Number(this.externalReferences)];
|
|
297
|
+
if (compact) {
|
|
298
|
+
out[0] |= WIDTHS.indexOf(s?.width ?? 1) << 2;
|
|
299
|
+
leb(out, s?.links.length ?? 0);
|
|
300
|
+
} else {
|
|
301
|
+
out[0] |= 2;
|
|
302
|
+
leb(out, this.sections.length);
|
|
303
|
+
for (const s of this.sections) {
|
|
304
|
+
leb(
|
|
305
|
+
out,
|
|
306
|
+
(s.arity.min << 4n) |
|
|
307
|
+
BigInt(
|
|
308
|
+
WIDTHS.indexOf(s.width) |
|
|
309
|
+
(s.gap ? 4 : 0) |
|
|
310
|
+
(s.arity.fixed ? 0 : 8)
|
|
311
|
+
)
|
|
312
|
+
);
|
|
313
|
+
if (s.gap) leb(out, s.gap);
|
|
314
|
+
if (!s.arity.fixed)
|
|
315
|
+
leb(out, s.arity.max === null ? 0n : s.arity.max - s.arity.min);
|
|
316
|
+
leb(out, s.links.length);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
for (const s of this.sections)
|
|
320
|
+
for (const link of s.links) {
|
|
321
|
+
if (!s.arity.fixed) leb(out, BigInt(link.length) - s.arity.min);
|
|
322
|
+
for (const r of link) {
|
|
323
|
+
const bits = BigInt(s.width * 8);
|
|
324
|
+
let raw =
|
|
325
|
+
r instanceof External
|
|
326
|
+
? r.value === 0n
|
|
327
|
+
? 1n << (bits - 1n)
|
|
328
|
+
: (1n << bits) - r.value
|
|
329
|
+
: uint64(r);
|
|
330
|
+
for (let i = 0; i < s.width; i++) {
|
|
331
|
+
out.push(Number(raw & 255n));
|
|
332
|
+
raw >>= 8n;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return Uint8Array.from(out);
|
|
337
|
+
}
|
|
338
|
+
static fromBytes(bytes, limits = new DecodeLimits()) {
|
|
339
|
+
const reader = new PacketReader(bytes),
|
|
340
|
+
packet = reader.read(limits);
|
|
341
|
+
requireValue(packet !== null, 'empty input');
|
|
342
|
+
requireValue(reader.offset === bytes.length, 'trailing bytes');
|
|
343
|
+
return packet;
|
|
344
|
+
}
|
|
345
|
+
static parseLinks(text) {
|
|
346
|
+
return text
|
|
347
|
+
.split(';')
|
|
348
|
+
.filter((s) => s.trim())
|
|
349
|
+
.map((entry) => {
|
|
350
|
+
const [a, refs] = entry.split(':');
|
|
351
|
+
return [
|
|
352
|
+
uint64(BigInt(a.trim())),
|
|
353
|
+
refs
|
|
354
|
+
.trim()
|
|
355
|
+
.split(/\s+/)
|
|
356
|
+
.filter(Boolean)
|
|
357
|
+
.map((r) =>
|
|
358
|
+
r.startsWith('#')
|
|
359
|
+
? new External(BigInt(r.slice(1)))
|
|
360
|
+
: uint64(BigInt(r))
|
|
361
|
+
),
|
|
362
|
+
];
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
static pack(externalReferences, input, packedWidths = false) {
|
|
366
|
+
const links = input.map(([a, r]) => [uint64(a), r]);
|
|
367
|
+
let previous = 0n;
|
|
368
|
+
const needs = links.map(([a, refs]) => {
|
|
369
|
+
requireValue(
|
|
370
|
+
a > previous && a < U64 && refs.length > 0,
|
|
371
|
+
'invalid address order or empty link'
|
|
372
|
+
);
|
|
373
|
+
previous = a;
|
|
374
|
+
return refs.reduce(
|
|
375
|
+
(need, r) => Math.max(need, referenceWidth(r, externalReferences)),
|
|
376
|
+
1
|
|
377
|
+
);
|
|
378
|
+
});
|
|
379
|
+
function layout(packed) {
|
|
380
|
+
if (!links.length) return [];
|
|
381
|
+
const widest = needs.reduce((a, b) => Math.max(a, b), 1),
|
|
382
|
+
opens = [],
|
|
383
|
+
bests = [];
|
|
384
|
+
let costs = Array(8).fill(Infinity);
|
|
385
|
+
const cheapest = (c) =>
|
|
386
|
+
c.reduce((best, v, s) => (v < c[best] ? s : best), 0);
|
|
387
|
+
for (let i = 0; i < links.length; i++) {
|
|
388
|
+
const best = cheapest(costs),
|
|
389
|
+
before = i ? costs[best] : 0,
|
|
390
|
+
next = Array(8).fill(Infinity);
|
|
391
|
+
let mask = 0;
|
|
392
|
+
bests.push(best);
|
|
393
|
+
for (let state = 0; state < 8; state++) {
|
|
394
|
+
const width = WIDTHS[state >> 1],
|
|
395
|
+
variable = state & 1;
|
|
396
|
+
if ((!packed && width !== widest) || width < needs[i]) continue;
|
|
397
|
+
const opening = before + 2 + variable;
|
|
398
|
+
const continuing =
|
|
399
|
+
i &&
|
|
400
|
+
links[i - 1][0] + 1n === links[i][0] &&
|
|
401
|
+
(variable || links[i - 1][1].length === links[i][1].length)
|
|
402
|
+
? costs[state]
|
|
403
|
+
: Infinity;
|
|
404
|
+
if (continuing <= opening)
|
|
405
|
+
next[state] = continuing + links[i][1].length * width + variable;
|
|
406
|
+
else {
|
|
407
|
+
next[state] = opening + links[i][1].length * width + variable;
|
|
408
|
+
mask |= 1 << state;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
opens.push(mask);
|
|
412
|
+
costs = next;
|
|
413
|
+
}
|
|
414
|
+
const result = [];
|
|
415
|
+
let end = links.length,
|
|
416
|
+
state = cheapest(costs);
|
|
417
|
+
for (let i = links.length - 1; i >= 0; i--)
|
|
418
|
+
if (opens[i] & (1 << state)) {
|
|
419
|
+
result.push([end - i, WIDTHS[state >> 1]]);
|
|
420
|
+
end = i;
|
|
421
|
+
state = bests[i];
|
|
422
|
+
}
|
|
423
|
+
return result.reverse();
|
|
424
|
+
}
|
|
425
|
+
function packet(plan) {
|
|
426
|
+
const sections = [];
|
|
427
|
+
let index = 0,
|
|
428
|
+
address = 1n;
|
|
429
|
+
for (const [count, width] of plan) {
|
|
430
|
+
const members = links.slice(index, index + count),
|
|
431
|
+
start = members[0][0];
|
|
432
|
+
let min = Infinity,
|
|
433
|
+
max = 0;
|
|
434
|
+
for (const [, r] of members) {
|
|
435
|
+
min = Math.min(min, r.length);
|
|
436
|
+
max = Math.max(max, r.length);
|
|
437
|
+
}
|
|
438
|
+
sections.push(
|
|
439
|
+
new Section(
|
|
440
|
+
start - address,
|
|
441
|
+
new ArityRange(min, max),
|
|
442
|
+
width,
|
|
443
|
+
members.map(([, r]) => r)
|
|
444
|
+
)
|
|
445
|
+
);
|
|
446
|
+
index += count;
|
|
447
|
+
address = start + BigInt(count);
|
|
448
|
+
}
|
|
449
|
+
return new LinksPacket(externalReferences, sections);
|
|
450
|
+
}
|
|
451
|
+
const uniform = packet(layout(false));
|
|
452
|
+
if (!packedWidths) return uniform;
|
|
453
|
+
const packed = packet(layout(true));
|
|
454
|
+
return packed.toBytes(DecodeLimits.unlimited()).length <
|
|
455
|
+
uniform.toBytes(DecodeLimits.unlimited()).length
|
|
456
|
+
? packed
|
|
457
|
+
: uniform;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
class Encoder {
|
|
461
|
+
constructor(options) {
|
|
462
|
+
this.options = options;
|
|
463
|
+
this.doublets = [];
|
|
464
|
+
this.tuples = [];
|
|
465
|
+
this.created = new Map();
|
|
466
|
+
this.powers = [['i', 1n]];
|
|
467
|
+
}
|
|
468
|
+
link(items) {
|
|
469
|
+
const key = items.map(([k, v]) => `${k}${v}`).join(',');
|
|
470
|
+
if (!this.created.has(key)) {
|
|
471
|
+
const doublet = items.length === 2 && items.every(([k]) => k !== 't'),
|
|
472
|
+
target = doublet ? this.doublets : this.tuples;
|
|
473
|
+
const node = [doublet ? 'd' : 't', BigInt(target.length)];
|
|
474
|
+
target.push(items);
|
|
475
|
+
this.created.set(key, node);
|
|
476
|
+
}
|
|
477
|
+
return this.created.get(key);
|
|
478
|
+
}
|
|
479
|
+
chain(items) {
|
|
480
|
+
let tail = ['i', 0n];
|
|
481
|
+
for (let i = items.length - 1; i >= 0; i--)
|
|
482
|
+
tail = this.link([items[i], tail]);
|
|
483
|
+
return tail;
|
|
484
|
+
}
|
|
485
|
+
typed(marker, elements) {
|
|
486
|
+
const items = [['i', BigInt(marker)], ...elements];
|
|
487
|
+
return items.length !== 2 && this.options.arity.contains(items.length)
|
|
488
|
+
? this.link(items)
|
|
489
|
+
: this.link([items[0], this.chain(elements)]);
|
|
490
|
+
}
|
|
491
|
+
list(elements) {
|
|
492
|
+
if (!elements.length) return ['i', 0n];
|
|
493
|
+
return elements.length === 2 || this.options.arity.contains(elements.length)
|
|
494
|
+
? this.link(elements)
|
|
495
|
+
: this.typed(4, elements);
|
|
496
|
+
}
|
|
497
|
+
unary(value) {
|
|
498
|
+
const powers = [];
|
|
499
|
+
for (let bit = 63; bit >= 0; bit--)
|
|
500
|
+
if (value & (1n << BigInt(bit))) {
|
|
501
|
+
while (this.powers.length <= bit) {
|
|
502
|
+
const p = this.powers.at(-1);
|
|
503
|
+
this.powers.push(this.link([p, p]));
|
|
504
|
+
}
|
|
505
|
+
powers.push(this.powers[bit]);
|
|
506
|
+
}
|
|
507
|
+
let sum = powers.pop() ?? ['i', 0n];
|
|
508
|
+
while (powers.length) sum = this.link([powers.pop(), sum]);
|
|
509
|
+
return sum;
|
|
510
|
+
}
|
|
511
|
+
scalar(value) {
|
|
512
|
+
return this.options.externalReferences && value < 1n << 63n
|
|
513
|
+
? ['e', value]
|
|
514
|
+
: this.unary(value);
|
|
515
|
+
}
|
|
516
|
+
reference(text) {
|
|
517
|
+
if (
|
|
518
|
+
/^(0|[1-9][0-9]*)$/.test(text) &&
|
|
519
|
+
text.length <= 20 &&
|
|
520
|
+
BigInt(text) <= U64
|
|
521
|
+
) {
|
|
522
|
+
const value = BigInt(text);
|
|
523
|
+
return this.options.externalReferences && value < 1n << 63n
|
|
524
|
+
? ['e', value]
|
|
525
|
+
: this.link([['i', 2n], this.unary(value)]);
|
|
526
|
+
}
|
|
527
|
+
return this.typed(
|
|
528
|
+
3,
|
|
529
|
+
Array.from(text, (c) => this.scalar(BigInt(c.codePointAt(0))))
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
encode(node) {
|
|
533
|
+
if (node.id !== null && !node.values.length) return this.reference(node.id);
|
|
534
|
+
if (node.id !== null) {
|
|
535
|
+
const id = this.reference(node.id);
|
|
536
|
+
return this.typed(5, [id, ...node.values.map((v) => this.encode(v))]);
|
|
537
|
+
}
|
|
538
|
+
return this.list(node.values.map((v) => this.encode(v)));
|
|
539
|
+
}
|
|
540
|
+
finish() {
|
|
541
|
+
const resolve = ([k, v]) =>
|
|
542
|
+
k === 'e'
|
|
543
|
+
? new External(v)
|
|
544
|
+
: k === 'i'
|
|
545
|
+
? v
|
|
546
|
+
: 6n + v + (k === 't' ? BigInt(this.doublets.length) : 0n);
|
|
547
|
+
return LinksPacket.pack(
|
|
548
|
+
this.options.externalReferences,
|
|
549
|
+
this.doublets
|
|
550
|
+
.concat(this.tuples)
|
|
551
|
+
.map((items, i) => [6n + BigInt(i), items.map(resolve)]),
|
|
552
|
+
this.options.packedWidths
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
class Decoder {
|
|
557
|
+
constructor(packet, limits) {
|
|
558
|
+
packet.validate(limits);
|
|
559
|
+
this.limits = limits;
|
|
560
|
+
this.links = [];
|
|
561
|
+
this.unary = [];
|
|
562
|
+
this.nodes = limits.maxNodes;
|
|
563
|
+
this.strings = limits.maxStringBytes;
|
|
564
|
+
for (const [address, refs] of packet.links()) {
|
|
565
|
+
requireValue(
|
|
566
|
+
address === 6n + BigInt(this.links.length) &&
|
|
567
|
+
refs.every((r) => r instanceof External || r < address),
|
|
568
|
+
'document addresses must be contiguous and references backward'
|
|
569
|
+
);
|
|
570
|
+
const value = (r) =>
|
|
571
|
+
r instanceof External
|
|
572
|
+
? null
|
|
573
|
+
: r <= 1n
|
|
574
|
+
? r
|
|
575
|
+
: r >= 6n
|
|
576
|
+
? this.unary[Number(r - 6n)]
|
|
577
|
+
: null;
|
|
578
|
+
const a = refs.length === 2 ? value(refs[0]) : null,
|
|
579
|
+
b = refs.length === 2 ? value(refs[1]) : null;
|
|
580
|
+
this.unary.push(a !== null && b !== null && a + b <= U64 ? a + b : null);
|
|
581
|
+
this.links.push(refs);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
chain(tail) {
|
|
585
|
+
const elements = [];
|
|
586
|
+
while (tail !== 0n) {
|
|
587
|
+
requireValue(typeof tail === 'bigint' && tail >= 6n, 'broken chain');
|
|
588
|
+
const items = this.links[Number(tail - 6n)];
|
|
589
|
+
requireValue(
|
|
590
|
+
items.length === 2 && elements.length < this.limits.maxNodes,
|
|
591
|
+
'broken or excessive chain'
|
|
592
|
+
);
|
|
593
|
+
elements.push(items[0]);
|
|
594
|
+
tail = items[1];
|
|
595
|
+
}
|
|
596
|
+
return elements;
|
|
597
|
+
}
|
|
598
|
+
number(r) {
|
|
599
|
+
const value =
|
|
600
|
+
r instanceof External
|
|
601
|
+
? r.value
|
|
602
|
+
: r <= 1n
|
|
603
|
+
? r
|
|
604
|
+
: r >= 6n
|
|
605
|
+
? this.unary[Number(r - 6n)]
|
|
606
|
+
: null;
|
|
607
|
+
requireValue(value !== null, 'expected unary number');
|
|
608
|
+
return value;
|
|
609
|
+
}
|
|
610
|
+
text(text) {
|
|
611
|
+
this.strings -= new globalThis.TextEncoder().encode(text).length;
|
|
612
|
+
requireValue(this.strings >= 0, 'string budget exceeded');
|
|
613
|
+
return new Link(text);
|
|
614
|
+
}
|
|
615
|
+
decode(r, depth) {
|
|
616
|
+
requireValue(
|
|
617
|
+
depth < this.limits.maxDepth && --this.nodes >= 0,
|
|
618
|
+
'node or depth budget exceeded'
|
|
619
|
+
);
|
|
620
|
+
if (r instanceof External) return this.text(String(r.value));
|
|
621
|
+
if (r === 0n) return new Link();
|
|
622
|
+
requireValue(r >= 6n, 'standalone marker');
|
|
623
|
+
const items = this.links[Number(r - 6n)];
|
|
624
|
+
let elements;
|
|
625
|
+
if (typeof items[0] === 'bigint' && items[0] >= 1n && items[0] <= 5n) {
|
|
626
|
+
const marker = Number(items[0]);
|
|
627
|
+
elements =
|
|
628
|
+
items.length !== 2 || marker === 2
|
|
629
|
+
? items.slice(1)
|
|
630
|
+
: this.chain(items[1]);
|
|
631
|
+
if (marker === 2) {
|
|
632
|
+
requireValue(elements.length === 1, 'number needs one value');
|
|
633
|
+
return this.text(String(this.number(elements[0])));
|
|
634
|
+
}
|
|
635
|
+
if (marker === 3) {
|
|
636
|
+
const chars = [];
|
|
637
|
+
let byteCount = 0;
|
|
638
|
+
for (const e of elements) {
|
|
639
|
+
const p = this.number(e);
|
|
640
|
+
requireValue(
|
|
641
|
+
p <= 0x10ffffn && !(p >= 0xd800n && p <= 0xdfffn),
|
|
642
|
+
'invalid Unicode scalar'
|
|
643
|
+
);
|
|
644
|
+
const size = p <= 0x7fn ? 1 : p <= 0x7ffn ? 2 : p <= 0xffffn ? 3 : 4;
|
|
645
|
+
requireValue(
|
|
646
|
+
size <= this.strings - byteCount,
|
|
647
|
+
'string budget exceeded'
|
|
648
|
+
);
|
|
649
|
+
byteCount += size;
|
|
650
|
+
chars.push(String.fromCodePoint(Number(p)));
|
|
651
|
+
}
|
|
652
|
+
return this.text(chars.join(''));
|
|
653
|
+
}
|
|
654
|
+
if (marker === 5) {
|
|
655
|
+
requireValue(elements.length > 0, 'identified needs id');
|
|
656
|
+
const id = this.decode(elements[0], depth);
|
|
657
|
+
requireValue(
|
|
658
|
+
id.id !== null && !id.values.length,
|
|
659
|
+
'id must be reference'
|
|
660
|
+
);
|
|
661
|
+
return new Link(
|
|
662
|
+
id.id,
|
|
663
|
+
elements.slice(1).map((e) => this.decode(e, depth + 1))
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
requireValue(marker === 4, 'invalid typed marker');
|
|
667
|
+
} else elements = items;
|
|
668
|
+
return new Link(
|
|
669
|
+
null,
|
|
670
|
+
elements.map((e) => this.decode(e, depth + 1))
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
document() {
|
|
674
|
+
if (!this.links.length) return [];
|
|
675
|
+
const root = this.links.at(-1);
|
|
676
|
+
let elements;
|
|
677
|
+
if (root.length === 2 && root[0] === 4n) elements = this.chain(root[1]);
|
|
678
|
+
else {
|
|
679
|
+
requireValue(
|
|
680
|
+
!(typeof root[0] === 'bigint' && root[0] >= 1n && root[0] <= 5n),
|
|
681
|
+
'root must be list'
|
|
682
|
+
);
|
|
683
|
+
elements = root;
|
|
684
|
+
}
|
|
685
|
+
return elements.map((e) => this.decode(e, 0));
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
function canonical(node) {
|
|
689
|
+
if (
|
|
690
|
+
node.id === null &&
|
|
691
|
+
node.values.length === 1 &&
|
|
692
|
+
node.values[0].id !== null &&
|
|
693
|
+
!node.values[0].values.length
|
|
694
|
+
)
|
|
695
|
+
return node.values[0];
|
|
696
|
+
return new Link(node.id, node.values.map(canonical));
|
|
697
|
+
}
|
|
698
|
+
export function formatBinaryReference(text) {
|
|
699
|
+
return formatReference(text);
|
|
700
|
+
}
|
|
701
|
+
export function formatBinaryDocument(document) {
|
|
702
|
+
function nested(node, top = false) {
|
|
703
|
+
if (node.id !== null && !node.values.length)
|
|
704
|
+
return formatBinaryReference(node.id);
|
|
705
|
+
const values = node.values.map((v) => nested(v)).join(' ');
|
|
706
|
+
if (node.id !== null)
|
|
707
|
+
return `(${formatBinaryReference(node.id)}: ${values})`;
|
|
708
|
+
if (
|
|
709
|
+
node.values.length === 1 &&
|
|
710
|
+
node.values[0].id !== null &&
|
|
711
|
+
!node.values[0].values.length
|
|
712
|
+
)
|
|
713
|
+
return `((${values}))`;
|
|
714
|
+
return top && node.values.length >= 2 ? values : `(${values})`;
|
|
715
|
+
}
|
|
716
|
+
return document.map((n) => nested(n, true)).join('\n');
|
|
717
|
+
}
|
|
718
|
+
export class BinaryLinoCodec {
|
|
719
|
+
constructor(options = new BinaryLinoOptions(), limits = new DecodeLimits()) {
|
|
720
|
+
Object.assign(this, { options, limits });
|
|
721
|
+
}
|
|
722
|
+
encodePacket(document) {
|
|
723
|
+
requireValue(this.options.arity.contains(2), 'arity must contain 2');
|
|
724
|
+
const pending = document.map((n) => [n, 0]);
|
|
725
|
+
let nodes = 0,
|
|
726
|
+
strings = 0;
|
|
727
|
+
while (pending.length) {
|
|
728
|
+
const [n, depth] = pending.pop();
|
|
729
|
+
requireValue(
|
|
730
|
+
n instanceof Link && depth < this.limits.maxDepth,
|
|
731
|
+
'invalid model or excessive depth'
|
|
732
|
+
);
|
|
733
|
+
nodes += 1 + Number(n.id !== null && n.values.length > 0);
|
|
734
|
+
if (n.id !== null) {
|
|
735
|
+
requireValue(
|
|
736
|
+
typeof n.id === 'string' && n.id.isWellFormed(),
|
|
737
|
+
'invalid Unicode string'
|
|
738
|
+
);
|
|
739
|
+
strings += new globalThis.TextEncoder().encode(n.id).length;
|
|
740
|
+
}
|
|
741
|
+
requireValue(
|
|
742
|
+
nodes <= this.limits.maxNodes && strings <= this.limits.maxStringBytes,
|
|
743
|
+
'model budget exceeded'
|
|
744
|
+
);
|
|
745
|
+
for (const v of n.values) pending.push([v, depth + 1]);
|
|
746
|
+
}
|
|
747
|
+
const encoder = new Encoder(this.options);
|
|
748
|
+
if (document.length) encoder.list(document.map((n) => encoder.encode(n)));
|
|
749
|
+
return encoder.finish().validate(this.limits);
|
|
750
|
+
}
|
|
751
|
+
encode(document) {
|
|
752
|
+
return this.encodePacket(document).toBytes(this.limits);
|
|
753
|
+
}
|
|
754
|
+
decodePacket(packet) {
|
|
755
|
+
return new Decoder(packet, this.limits).document();
|
|
756
|
+
}
|
|
757
|
+
decode(bytes) {
|
|
758
|
+
return this.decodePacket(LinksPacket.fromBytes(bytes, this.limits));
|
|
759
|
+
}
|
|
760
|
+
parseDocument(text, parser = new Parser()) {
|
|
761
|
+
return parser.parse(text).map(canonical);
|
|
762
|
+
}
|
|
763
|
+
formatDocument(document) {
|
|
764
|
+
return formatBinaryDocument(document);
|
|
765
|
+
}
|
|
766
|
+
encodeText(text, parser) {
|
|
767
|
+
return this.encode(this.parseDocument(text, parser));
|
|
768
|
+
}
|
|
769
|
+
decodeText(bytes) {
|
|
770
|
+
return this.formatDocument(this.decode(bytes));
|
|
771
|
+
}
|
|
772
|
+
}
|