@altopelago/aeon-aes 0.12.0 → 0.13.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.
Files changed (59) hide show
  1. package/README.md +31 -2
  2. package/dist/events.d.ts +5 -0
  3. package/dist/events.d.ts.map +1 -1
  4. package/dist/events.js +3 -0
  5. package/dist/events.js.map +1 -1
  6. package/dist/film-internal.d.ts +137 -0
  7. package/dist/film-internal.d.ts.map +1 -0
  8. package/dist/film-internal.js +702 -0
  9. package/dist/film-internal.js.map +1 -0
  10. package/dist/film.d.ts +64 -0
  11. package/dist/film.d.ts.map +1 -0
  12. package/dist/film.js +22 -0
  13. package/dist/film.js.map +1 -0
  14. package/dist/index.d.ts +3 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +3 -0
  17. package/dist/index.js.map +1 -1
  18. package/dist/modes.d.ts.map +1 -1
  19. package/dist/modes.js +14 -8
  20. package/dist/modes.js.map +1 -1
  21. package/dist/paths.d.ts +1 -0
  22. package/dist/paths.d.ts.map +1 -1
  23. package/dist/paths.js +33 -29
  24. package/dist/paths.js.map +1 -1
  25. package/dist/portable.d.ts +98 -0
  26. package/dist/portable.d.ts.map +1 -0
  27. package/dist/portable.js +524 -0
  28. package/dist/portable.js.map +1 -0
  29. package/dist/references.d.ts.map +1 -1
  30. package/dist/references.js +9 -4
  31. package/dist/references.js.map +1 -1
  32. package/dist/resolve.d.ts.map +1 -1
  33. package/dist/resolve.js +52 -2
  34. package/dist/resolve.js.map +1 -1
  35. package/dist/sha256.d.ts +3 -0
  36. package/dist/sha256.d.ts.map +1 -0
  37. package/dist/sha256.js +82 -0
  38. package/dist/sha256.js.map +1 -0
  39. package/dist/telex-datatype-internal.d.ts +12 -0
  40. package/dist/telex-datatype-internal.d.ts.map +1 -0
  41. package/dist/telex-datatype-internal.js +323 -0
  42. package/dist/telex-datatype-internal.js.map +1 -0
  43. package/dist/telex-incremental-internal.d.ts +50 -0
  44. package/dist/telex-incremental-internal.d.ts.map +1 -0
  45. package/dist/telex-incremental-internal.js +422 -0
  46. package/dist/telex-incremental-internal.js.map +1 -0
  47. package/dist/telex-internal.d.ts +86 -0
  48. package/dist/telex-internal.d.ts.map +1 -0
  49. package/dist/telex-internal.js +1134 -0
  50. package/dist/telex-internal.js.map +1 -0
  51. package/dist/telex-limits-internal.d.ts +35 -0
  52. package/dist/telex-limits-internal.d.ts.map +1 -0
  53. package/dist/telex-limits-internal.js +70 -0
  54. package/dist/telex-limits-internal.js.map +1 -0
  55. package/dist/telex.d.ts +130 -0
  56. package/dist/telex.d.ts.map +1 -0
  57. package/dist/telex.js +46 -0
  58. package/dist/telex.js.map +1 -0
  59. package/package.json +3 -3
@@ -0,0 +1,702 @@
1
+ // @ts-nocheck
2
+ // Kept behaviorally aligned with the published Film v1 reference decoder.
3
+ import { COMPLETE_AES_PROFILE, validateTelexRecords, } from './telex.js';
4
+ import { normalizeTelexLimits } from './telex-limits-internal.js';
5
+ export const FILM_VERSION = '1';
6
+ export const FILM_V1_PREAMBLE = Object.freeze([0x4f, 0x5f, 0x5f, 0xff, 0x01]);
7
+ export const DEFAULT_FILM_LIMITS = Object.freeze({
8
+ maxInputBytes: 67_108_864,
9
+ maxRecordBytes: 16_777_216,
10
+ maxFieldBytes: 4_194_304,
11
+ maxBufferedBytes: 16_777_216,
12
+ });
13
+ const CONTEXT_PROFILE = 0x01;
14
+ const CONTEXT_PROJECTION = 0x02;
15
+ const RECORD_HEADER = 0x01;
16
+ const RECORD_DATATYPE = 0x02;
17
+ const RECORD_IDENTITY = 0x04;
18
+ const RECORD_ORIGIN = 0x08;
19
+ const RECORD_SPAN = 0x10;
20
+ const KIND_NAMES = Object.freeze([
21
+ 'StringLiteral',
22
+ 'NumberLiteral',
23
+ 'InfinityLiteral',
24
+ 'NaNLiteral',
25
+ 'NullLiteral',
26
+ 'BooleanLiteral',
27
+ 'ToggleLiteral',
28
+ 'HexLiteral',
29
+ 'RadixLiteral',
30
+ 'EncodingLiteral',
31
+ 'SeparatorLiteral',
32
+ 'SansaAddressLiteral',
33
+ 'DateLiteral',
34
+ 'TimeLiteral',
35
+ 'DateTimeLiteral',
36
+ 'WTCDateTimeLiteral',
37
+ 'ObjectNode',
38
+ 'ListNode',
39
+ 'TupleLiteral',
40
+ 'NodeLiteral',
41
+ 'NodeHead',
42
+ 'CloneReference',
43
+ 'PointerReference',
44
+ ]);
45
+ const VALUELESS_KINDS = new Set([
46
+ 'ObjectNode',
47
+ 'ListNode',
48
+ 'TupleLiteral',
49
+ 'NodeLiteral',
50
+ ]);
51
+ const EXTENSION_NAME = /^x\.[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)+$/u;
52
+ const UTF8 = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
53
+ const MAX_SAFE_BIGINT = BigInt(Number.MAX_SAFE_INTEGER);
54
+ export class FilmDecodeError extends Error {
55
+ constructor(code, message, details = {}) {
56
+ super(message);
57
+ this.name = 'FilmDecodeError';
58
+ this.code = code;
59
+ this.offset = details.offset ?? 0;
60
+ this.record = details.record ?? null;
61
+ this.component = details.component ?? 'stream';
62
+ this.stage = details.stage ?? 'film';
63
+ this.diagnostics = details.diagnostics ?? [];
64
+ }
65
+ }
66
+ /** Normalize the Film-local limits selected by the consumer. */
67
+ export function normalizeFilmLimits(options = {}) {
68
+ const source = { ...options, ...(options.filmLimits ?? {}) };
69
+ return Object.freeze({
70
+ maxInputBytes: limit(source, 'maxInputBytes', DEFAULT_FILM_LIMITS.maxInputBytes),
71
+ maxRecordBytes: limit(source, 'maxRecordBytes', DEFAULT_FILM_LIMITS.maxRecordBytes),
72
+ maxFieldBytes: limit(source, 'maxFieldBytes', DEFAULT_FILM_LIMITS.maxFieldBytes),
73
+ maxBufferedBytes: limit(source, 'maxBufferedBytes', DEFAULT_FILM_LIMITS.maxBufferedBytes),
74
+ });
75
+ }
76
+ /**
77
+ * Decode canonical Film framing and records into owned JavaScript values.
78
+ * The result is provisional: portable AES semantics have not yet been
79
+ * accepted. Use decodeFilm for replay, ingestion, or mutation inputs.
80
+ */
81
+ export function decodeFilmSyntax(input, options = {}) {
82
+ assertByteInput(input);
83
+ const filmLimits = normalizeFilmLimits(options);
84
+ const aesLimits = normalizeAesLimits(options);
85
+ enforceFilmLimit('max_input_bytes', input.byteLength, filmLimits.maxInputBytes, {
86
+ component: 'stream',
87
+ });
88
+ if (input.byteLength < FILM_V1_PREAMBLE.length) {
89
+ throw filmError('FILM_TRUNCATED', input.byteLength, null, 'preamble', 'Film input ended inside the v1 preamble');
90
+ }
91
+ for (let index = 0; index < FILM_V1_PREAMBLE.length; index += 1) {
92
+ if (input[index] !== FILM_V1_PREAMBLE[index]) {
93
+ throw filmError('FILM_INVALID_PREAMBLE', 0, null, 'preamble', 'Expected O__ FF 01');
94
+ }
95
+ }
96
+ const reader = new Reader(input, 0, null);
97
+ reader.position = FILM_V1_PREAMBLE.length;
98
+ const context = reader.readByte('stream-context');
99
+ if ((context & ~0x03) !== 0) {
100
+ throw filmError('FILM_INVALID_CONTEXT', reader.absolutePosition() - 1, null, 'stream-context', 'Reserved stream-context bits must be zero');
101
+ }
102
+ const profileExplicit = (context & CONTEXT_PROFILE) !== 0;
103
+ const projectionExplicit = (context & CONTEXT_PROJECTION) !== 0;
104
+ const profile = profileExplicit
105
+ ? reader.readNonEmptyContextString(filmLimits, 'profile')
106
+ : COMPLETE_AES_PROFILE;
107
+ const projection = projectionExplicit
108
+ ? reader.readNonEmptyContextString(filmLimits, 'projection')
109
+ : null;
110
+ const records = [];
111
+ while (!reader.atEnd()) {
112
+ const recordIndex = records.length;
113
+ if (recordIndex >= aesLimits.maxEvents) {
114
+ throw aesLimitError('max_events', recordIndex + 1, aesLimits.maxEvents, reader.absolutePosition(), recordIndex, 'record');
115
+ }
116
+ reader.record = recordIndex;
117
+ const payloadLength = reader.readLimitedLength('max_record_bytes', filmLimits.maxRecordBytes, 'record-length', 'record');
118
+ if (payloadLength === 0) {
119
+ throw filmError('FILM_NONCANONICAL', reader.absolutePosition(), recordIndex, 'record-length', 'Film record payload length must be positive');
120
+ }
121
+ const payloadOffset = reader.absolutePosition();
122
+ const payload = reader.readExact(payloadLength, 'record');
123
+ records.push(decodeRecord(payload, payloadOffset, recordIndex, filmLimits, aesLimits));
124
+ }
125
+ return {
126
+ profile,
127
+ profileExplicit,
128
+ projection,
129
+ projectionExplicit,
130
+ records,
131
+ };
132
+ }
133
+ /** Decode Film and return an owned stream only after complete AES validation. */
134
+ export function decodeFilm(input, options = {}) {
135
+ const stream = decodeFilmSyntax(input, options);
136
+ validateDecodedStream(stream, input.byteLength, options);
137
+ return stream;
138
+ }
139
+ /**
140
+ * Incremental Film decoder. Complete records may be inspected provisionally,
141
+ * but only a successful push with `{ final: true }` returns `status=complete`.
142
+ */
143
+ export class IncrementalFilmDecoder {
144
+ constructor(options = {}) {
145
+ this.filmLimits = normalizeFilmLimits(options);
146
+ this.aesLimits = normalizeAesLimits(options);
147
+ this.registeredFields = [...(options.registeredFields ?? [])];
148
+ this.buffer = new Uint8Array(0);
149
+ this.totalInputBytes = 0;
150
+ this.absoluteOffset = 0;
151
+ this.context = null;
152
+ this.records = [];
153
+ this.pendingRecordLength = null;
154
+ this.pendingRecordOffset = null;
155
+ this.closed = false;
156
+ }
157
+ push(input, options = {}) {
158
+ if (this.closed)
159
+ throw new TypeError('Film decoder is already closed');
160
+ assertByteInput(input);
161
+ const final = options.final ?? false;
162
+ if (typeof final !== 'boolean')
163
+ throw new TypeError('Film final flag must be boolean');
164
+ try {
165
+ const projectedInputBytes = checkedAdd(this.totalInputBytes, input.byteLength);
166
+ enforceFilmLimit('max_input_bytes', projectedInputBytes, this.filmLimits.maxInputBytes, { offset: projectedInputBytes, component: 'stream' });
167
+ if (input.byteLength === 0)
168
+ return this.pushBounded(input, final);
169
+ const records = [];
170
+ let offset = 0;
171
+ let result;
172
+ while (offset < input.byteLength) {
173
+ const available = this.filmLimits.maxBufferedBytes - this.buffer.byteLength;
174
+ if (available === 0) {
175
+ enforceFilmLimit('max_buffered_bytes', this.buffer.byteLength + 1, this.filmLimits.maxBufferedBytes, this.bufferLimitDetails());
176
+ }
177
+ const width = Math.min(available, input.byteLength - offset);
178
+ const end = offset + width;
179
+ result = this.pushBounded(input.subarray(offset, end), final && end === input.byteLength);
180
+ for (const record of result.records)
181
+ records.push(record);
182
+ offset = end;
183
+ }
184
+ return { ...result, records };
185
+ }
186
+ catch (error) {
187
+ this.closed = true;
188
+ throw error;
189
+ }
190
+ }
191
+ pushBounded(input, final) {
192
+ try {
193
+ this.totalInputBytes = checkedAdd(this.totalInputBytes, input.byteLength);
194
+ enforceFilmLimit('max_input_bytes', this.totalInputBytes, this.filmLimits.maxInputBytes, { offset: this.totalInputBytes, component: 'stream' });
195
+ const bufferedBytes = checkedAdd(this.buffer.byteLength, input.byteLength);
196
+ enforceFilmLimit('max_buffered_bytes', bufferedBytes, this.filmLimits.maxBufferedBytes, this.bufferLimitDetails());
197
+ this.buffer = appendBytes(this.buffer, input);
198
+ const newRecords = [];
199
+ if (this.context === null) {
200
+ const context = readIncrementalContext(this.buffer, this.filmLimits, final);
201
+ if (context === null) {
202
+ this.retainIncompleteBuffer();
203
+ return this.result('need-more-input', newRecords);
204
+ }
205
+ this.context = context.stream;
206
+ this.consume(context.consumedBytes);
207
+ }
208
+ while (true) {
209
+ if (this.pendingRecordLength === null) {
210
+ if (this.buffer.byteLength === 0)
211
+ break;
212
+ if (this.records.length >= this.aesLimits.maxEvents) {
213
+ throw aesLimitError('max_events', this.records.length + 1, this.aesLimits.maxEvents, this.absoluteOffset, this.records.length, 'record');
214
+ }
215
+ const reader = new Reader(this.buffer, this.absoluteOffset, this.records.length);
216
+ let payloadLength;
217
+ try {
218
+ payloadLength = reader.readLimitedLength('max_record_bytes', this.filmLimits.maxRecordBytes, 'record-length', 'record');
219
+ }
220
+ catch (error) {
221
+ if (!final && isTruncation(error))
222
+ break;
223
+ throw error;
224
+ }
225
+ if (payloadLength === 0) {
226
+ throw filmError('FILM_NONCANONICAL', reader.absolutePosition(), this.records.length, 'record-length', 'Film record payload length must be positive');
227
+ }
228
+ this.consume(reader.position);
229
+ this.pendingRecordLength = payloadLength;
230
+ this.pendingRecordOffset = this.absoluteOffset;
231
+ }
232
+ if (this.buffer.byteLength < this.pendingRecordLength)
233
+ break;
234
+ const payload = this.buffer.subarray(0, this.pendingRecordLength);
235
+ const record = decodeRecord(payload, this.pendingRecordOffset, this.records.length, this.filmLimits, this.aesLimits);
236
+ this.consume(this.pendingRecordLength);
237
+ this.pendingRecordLength = null;
238
+ this.pendingRecordOffset = null;
239
+ this.records.push(record);
240
+ newRecords.push(cloneRecord(record));
241
+ }
242
+ if (final) {
243
+ if (this.pendingRecordLength !== null) {
244
+ throw filmError('FILM_TRUNCATED', this.absoluteOffset + this.buffer.byteLength, this.records.length, 'record', 'Film input ended inside a declared record');
245
+ }
246
+ if (this.buffer.byteLength !== 0) {
247
+ const reader = new Reader(this.buffer, this.absoluteOffset, this.records.length);
248
+ reader.readLimitedLength('max_record_bytes', this.filmLimits.maxRecordBytes, 'record-length', 'record');
249
+ throw filmError('FILM_TRUNCATED', this.absoluteOffset + this.buffer.byteLength, this.records.length, 'record', 'Film input ended before a complete record');
250
+ }
251
+ const stream = this.currentStream();
252
+ validateDecodedStream(stream, this.totalInputBytes, {
253
+ registeredFields: this.registeredFields,
254
+ aesLimits: this.aesLimits,
255
+ });
256
+ this.closed = true;
257
+ return {
258
+ status: 'complete',
259
+ records: newRecords,
260
+ stream: cloneStream(stream),
261
+ bufferedBytes: 0,
262
+ inputBytes: this.totalInputBytes,
263
+ };
264
+ }
265
+ this.retainIncompleteBuffer();
266
+ const status = this.pendingRecordLength === null && this.buffer.byteLength === 0
267
+ ? 'provisional'
268
+ : 'need-more-input';
269
+ return this.result(status, newRecords);
270
+ }
271
+ catch (error) {
272
+ this.closed = true;
273
+ throw error;
274
+ }
275
+ }
276
+ finish() {
277
+ return this.push(new Uint8Array(0), { final: true });
278
+ }
279
+ consume(length) {
280
+ this.buffer = length === this.buffer.byteLength
281
+ ? new Uint8Array(0)
282
+ : this.buffer.subarray(length);
283
+ this.absoluteOffset += length;
284
+ }
285
+ retainIncompleteBuffer() {
286
+ enforceFilmLimit('max_buffered_bytes', this.buffer.byteLength, this.filmLimits.maxBufferedBytes, this.bufferLimitDetails());
287
+ if (this.buffer.byteLength !== 0)
288
+ this.buffer = this.buffer.slice();
289
+ }
290
+ bufferLimitDetails() {
291
+ return {
292
+ offset: this.absoluteOffset,
293
+ record: this.context === null ? null : this.records.length,
294
+ component: this.context === null ? 'stream-context' : 'record',
295
+ };
296
+ }
297
+ currentStream() {
298
+ return {
299
+ ...this.context,
300
+ records: this.records,
301
+ };
302
+ }
303
+ result(status, records) {
304
+ return {
305
+ status,
306
+ records,
307
+ stream: this.context === null ? null : cloneStream(this.currentStream()),
308
+ bufferedBytes: this.buffer.byteLength,
309
+ inputBytes: this.totalInputBytes,
310
+ };
311
+ }
312
+ }
313
+ function validateDecodedStream(stream, inputBytes, options) {
314
+ const validation = validateTelexRecords(stream.records, {
315
+ profile: stream.profile,
316
+ projection: stream.projection,
317
+ registeredFields: options.registeredFields ?? [],
318
+ limits: normalizeAesLimits(options),
319
+ });
320
+ if (!validation.valid) {
321
+ const first = validation.diagnostics[0];
322
+ throw new FilmDecodeError('FILM_AES_INVALID', first?.message ?? 'Portable AES validation failed', {
323
+ offset: inputBytes,
324
+ record: first?.record ?? null,
325
+ component: 'aes-events',
326
+ stage: 'aes',
327
+ diagnostics: validation.diagnostics,
328
+ });
329
+ }
330
+ }
331
+ function readIncrementalContext(input, filmLimits, final) {
332
+ const comparable = Math.min(input.byteLength, FILM_V1_PREAMBLE.length);
333
+ for (let index = 0; index < comparable; index += 1) {
334
+ if (input[index] !== FILM_V1_PREAMBLE[index]) {
335
+ throw filmError('FILM_INVALID_PREAMBLE', 0, null, 'preamble', 'Expected O__ FF 01');
336
+ }
337
+ }
338
+ if (input.byteLength < FILM_V1_PREAMBLE.length) {
339
+ if (!final)
340
+ return null;
341
+ throw filmError('FILM_TRUNCATED', input.byteLength, null, 'preamble', 'Film input ended inside the v1 preamble');
342
+ }
343
+ const reader = new Reader(input, 0, null);
344
+ reader.position = FILM_V1_PREAMBLE.length;
345
+ try {
346
+ const context = reader.readByte('stream-context');
347
+ if ((context & ~0x03) !== 0) {
348
+ throw filmError('FILM_INVALID_CONTEXT', reader.absolutePosition() - 1, null, 'stream-context', 'Reserved stream-context bits must be zero');
349
+ }
350
+ const profileExplicit = (context & CONTEXT_PROFILE) !== 0;
351
+ const projectionExplicit = (context & CONTEXT_PROJECTION) !== 0;
352
+ const profile = profileExplicit
353
+ ? reader.readNonEmptyContextString(filmLimits, 'profile')
354
+ : COMPLETE_AES_PROFILE;
355
+ const projection = projectionExplicit
356
+ ? reader.readNonEmptyContextString(filmLimits, 'projection')
357
+ : null;
358
+ return {
359
+ consumedBytes: reader.position,
360
+ stream: {
361
+ profile,
362
+ profileExplicit,
363
+ projection,
364
+ projectionExplicit,
365
+ },
366
+ };
367
+ }
368
+ catch (error) {
369
+ if (!final && isTruncation(error))
370
+ return null;
371
+ throw error;
372
+ }
373
+ }
374
+ function cloneStream(stream) {
375
+ return {
376
+ profile: stream.profile,
377
+ profileExplicit: stream.profileExplicit,
378
+ projection: stream.projection,
379
+ projectionExplicit: stream.projectionExplicit,
380
+ records: stream.records.map(cloneRecord),
381
+ };
382
+ }
383
+ function cloneRecord(record) {
384
+ return Object.fromEntries(Object.entries(record).map(([name, value]) => [
385
+ name,
386
+ cloneFilmValue(value),
387
+ ]));
388
+ }
389
+ function cloneFilmValue(value) {
390
+ if (Array.isArray(value))
391
+ return value.map(cloneFilmValue);
392
+ if (value !== null && typeof value === 'object') {
393
+ return Object.fromEntries(Object.entries(value).map(([name, nested]) => [
394
+ name,
395
+ cloneFilmValue(nested),
396
+ ]));
397
+ }
398
+ return value;
399
+ }
400
+ function appendBytes(left, right) {
401
+ if (left.byteLength === 0)
402
+ return right;
403
+ if (right.byteLength === 0)
404
+ return left;
405
+ const length = checkedAdd(left.byteLength, right.byteLength);
406
+ const output = new Uint8Array(length);
407
+ output.set(left, 0);
408
+ output.set(right, left.byteLength);
409
+ return output;
410
+ }
411
+ function checkedAdd(left, right) {
412
+ const result = left + right;
413
+ if (!Number.isSafeInteger(result)) {
414
+ throw filmError('FILM_INTEGER_OVERFLOW', left, null, 'stream', 'Film byte count exceeds the JavaScript safe-integer domain');
415
+ }
416
+ return result;
417
+ }
418
+ function isTruncation(error) {
419
+ return error instanceof FilmDecodeError && error.code === 'FILM_TRUNCATED';
420
+ }
421
+ export function filmV1IsDraft() {
422
+ return true;
423
+ }
424
+ function decodeRecord(payload, payloadOffset, recordIndex, filmLimits, aesLimits) {
425
+ const reader = new Reader(payload, payloadOffset, recordIndex);
426
+ const control = reader.readByte('record-control');
427
+ if ((control & ~0x1f) !== 0) {
428
+ throw filmError('FILM_INVALID_RECORD', payloadOffset, recordIndex, 'record-control', 'Reserved record-control bits must be zero');
429
+ }
430
+ if ((control & RECORD_SPAN) !== 0 && (control & RECORD_ORIGIN) === 0) {
431
+ throw filmError('FILM_INVALID_RECORD', payloadOffset, recordIndex, 'record-control', 'Span presence requires origin presence');
432
+ }
433
+ const kindOffset = reader.absolutePosition();
434
+ const kindCode = reader.readByte('kind');
435
+ const kind = KIND_NAMES[kindCode - 1];
436
+ if (kind === undefined) {
437
+ throw filmError('FILM_INVALID_KIND', kindOffset, recordIndex, 'kind', 'Unassigned Film v1 kind code');
438
+ }
439
+ const address = reader.readString(filmLimits, 'address');
440
+ const record = {
441
+ [(control & RECORD_HEADER) === 0 ? 'path' : 'header']: address,
442
+ kind,
443
+ };
444
+ if ((control & RECORD_DATATYPE) !== 0) {
445
+ const datatype = reader.readDescriptor(filmLimits, aesLimits, 0, { count: 0 });
446
+ record.datatype = datatype.datatype;
447
+ record.generics = datatype.generics;
448
+ record.clarifiers = datatype.clarifiers;
449
+ }
450
+ if ((control & RECORD_IDENTITY) !== 0) {
451
+ record.identity = reader.readString(filmLimits, 'identity');
452
+ }
453
+ if (!VALUELESS_KINDS.has(kind)) {
454
+ record.value = reader.readString(filmLimits, 'value');
455
+ }
456
+ if ((control & RECORD_ORIGIN) !== 0) {
457
+ record.origin = `sha256:${lowerHex(reader.readExact(32, 'origin'))}`;
458
+ }
459
+ if ((control & RECORD_SPAN) !== 0) {
460
+ const start = reader.readUleb('span-start');
461
+ const end = reader.readUleb('span-end');
462
+ if (start >= end) {
463
+ throw filmError('FILM_INVALID_RECORD', reader.absolutePosition(), recordIndex, 'span', 'Film span requires start < end');
464
+ }
465
+ record.span = `${start}:${end}`;
466
+ }
467
+ let previousExtension = null;
468
+ while (!reader.atEnd()) {
469
+ const nameOffset = reader.absolutePosition();
470
+ const name = reader.readString(filmLimits, 'extension-name');
471
+ if (!EXTENSION_NAME.test(name)) {
472
+ throw filmError('FILM_INVALID_EXTENSION', nameOffset, recordIndex, 'extension-name', `Invalid Film extension name: ${name}`);
473
+ }
474
+ if (previousExtension !== null && previousExtension >= name) {
475
+ throw filmError('FILM_NONCANONICAL', nameOffset, recordIndex, 'extension-name', 'Extensions must be strictly ordered without duplicates');
476
+ }
477
+ record[name] = reader.readString(filmLimits, 'extension-value');
478
+ previousExtension = name;
479
+ }
480
+ return record;
481
+ }
482
+ class Reader {
483
+ constructor(bytes, baseOffset, record) {
484
+ this.bytes = bytes;
485
+ this.baseOffset = baseOffset;
486
+ this.record = record;
487
+ this.position = 0;
488
+ }
489
+ atEnd() {
490
+ return this.position === this.bytes.byteLength;
491
+ }
492
+ absolutePosition() {
493
+ return this.baseOffset + this.position;
494
+ }
495
+ readByte(component) {
496
+ const byte = this.bytes[this.position];
497
+ if (byte === undefined) {
498
+ throw filmError('FILM_TRUNCATED', this.absolutePosition(), this.record, component, 'Film input ended before the required byte');
499
+ }
500
+ this.position += 1;
501
+ return byte;
502
+ }
503
+ readExact(length, component) {
504
+ const end = this.position + length;
505
+ if (!Number.isSafeInteger(end)) {
506
+ throw filmError('FILM_INTEGER_OVERFLOW', this.absolutePosition(), this.record, component, 'Byte range overflow');
507
+ }
508
+ if (end > this.bytes.byteLength) {
509
+ throw filmError('FILM_TRUNCATED', this.absolutePosition(), this.record, component, 'Film input ended inside a declared field or record');
510
+ }
511
+ const value = this.bytes.subarray(this.position, end);
512
+ this.position = end;
513
+ return value;
514
+ }
515
+ readUleb(component) {
516
+ const start = this.absolutePosition();
517
+ let value = 0n;
518
+ for (let index = 0; index < 10; index += 1) {
519
+ const byte = this.readByte(component);
520
+ const payload = BigInt(byte & 0x7f);
521
+ if (index === 9 && payload > 1n) {
522
+ throw filmError('FILM_INTEGER_OVERFLOW', start, this.record, component, 'Unsigned LEB128 value exceeds u64');
523
+ }
524
+ value |= payload << BigInt(index * 7);
525
+ if ((byte & 0x80) === 0) {
526
+ if (ulebWidth(value) !== index + 1) {
527
+ throw filmError('FILM_NONCANONICAL', start, this.record, component, 'Unsigned LEB128 must use its shortest representation');
528
+ }
529
+ return value;
530
+ }
531
+ }
532
+ throw filmError('FILM_INTEGER_OVERFLOW', start, this.record, component, 'Unsigned LEB128 exceeds ten bytes');
533
+ }
534
+ readLimitedLength(counter, limit, component, limitComponent = component) {
535
+ const offset = this.absolutePosition();
536
+ const value = this.readUleb(component);
537
+ if (value > BigInt(limit)) {
538
+ throw new FilmDecodeError('FILM_LIMIT_EXCEEDED', `${counter} observed ${value}, limit ${limit}`, { offset, record: this.record, component: limitComponent });
539
+ }
540
+ return Number(value);
541
+ }
542
+ readCount(component, counter, limit) {
543
+ const offset = this.absolutePosition();
544
+ const value = this.readUleb(component);
545
+ if (value > BigInt(limit)) {
546
+ return throwAesCountLimit(counter, value, limit, offset, this.record, component);
547
+ }
548
+ if (value > MAX_SAFE_BIGINT) {
549
+ throw filmError('FILM_INTEGER_OVERFLOW', offset, this.record, component, 'Film count does not fit the JavaScript safe-integer domain');
550
+ }
551
+ return Number(value);
552
+ }
553
+ readString(filmLimits, component) {
554
+ const offset = this.absolutePosition();
555
+ const length = this.readLimitedLength('max_field_bytes', filmLimits.maxFieldBytes, component);
556
+ const bytes = this.readExact(length, component);
557
+ try {
558
+ return UTF8.decode(bytes);
559
+ }
560
+ catch {
561
+ throw filmError('FILM_INVALID_UTF8', offset, this.record, component, 'Film strings must contain valid UTF-8');
562
+ }
563
+ }
564
+ readNonEmptyContextString(filmLimits, component) {
565
+ const offset = this.absolutePosition();
566
+ const value = this.readString(filmLimits, component);
567
+ if (value.length === 0) {
568
+ throw filmError('FILM_INVALID_CONTEXT', offset, this.record, component, 'Film stream-context strings must not be empty');
569
+ }
570
+ return value;
571
+ }
572
+ readDescriptor(filmLimits, aesLimits, depth, components) {
573
+ claimDatatypeComponent(components, aesLimits.maxDatatypeComponents, this.absolutePosition(), this.record, 'datatype');
574
+ if (depth > aesLimits.maxGenericDepth) {
575
+ throw aesLimitError('max_generic_depth', depth, aesLimits.maxGenericDepth, this.absolutePosition(), this.record, 'datatype');
576
+ }
577
+ const length = this.readLimitedLength('max_field_bytes', filmLimits.maxFieldBytes, 'datatype');
578
+ const offset = this.absolutePosition();
579
+ const body = this.readExact(length, 'datatype');
580
+ const descriptor = new Reader(body, offset, this.record);
581
+ const datatypeOffset = descriptor.absolutePosition();
582
+ const datatype = descriptor.readString(filmLimits, 'datatype-name');
583
+ if (datatype.length === 0) {
584
+ throw filmError('FILM_INVALID_DATATYPE', datatypeOffset, this.record, 'datatype-name', 'Film datatype names must not be empty');
585
+ }
586
+ const genericCount = descriptor.readCount('generic-count', 'max_generic_arguments', aesLimits.maxGenericArguments);
587
+ ensureDatatypeComponentCapacity(components.count, genericCount, aesLimits.maxDatatypeComponents, descriptor.absolutePosition(), this.record, 'generic-count');
588
+ const generics = [];
589
+ for (let index = 0; index < genericCount; index += 1) {
590
+ const tagOffset = descriptor.absolutePosition();
591
+ const tag = descriptor.readByte('generic-tag');
592
+ if (tag === 0x00) {
593
+ generics.push(descriptor.readDescriptor(filmLimits, aesLimits, depth + 1, components));
594
+ }
595
+ else if (tag === 0x02) {
596
+ claimDatatypeComponent(components, aesLimits.maxDatatypeComponents, descriptor.absolutePosition(), this.record, 'generic-number');
597
+ generics.push({
598
+ kind: 'NumberLiteral',
599
+ value: descriptor.readString(filmLimits, 'generic-number'),
600
+ });
601
+ }
602
+ else {
603
+ throw filmError('FILM_INVALID_DATATYPE', tagOffset, this.record, 'generic-tag', 'Film generic tag must be 00 or 02');
604
+ }
605
+ }
606
+ const clarifierCount = descriptor.readCount('clarifier-count', 'max_clarifier_values', aesLimits.maxClarifierValues);
607
+ ensureDatatypeComponentCapacity(components.count, clarifierCount, aesLimits.maxDatatypeComponents, descriptor.absolutePosition(), this.record, 'clarifier-count');
608
+ const clarifiers = [];
609
+ for (let index = 0; index < clarifierCount; index += 1) {
610
+ claimDatatypeComponent(components, aesLimits.maxDatatypeComponents, descriptor.absolutePosition(), this.record, 'clarifier');
611
+ const tagOffset = descriptor.absolutePosition();
612
+ const tag = descriptor.readByte('clarifier-tag');
613
+ if (tag !== 0x01 && tag !== 0x02) {
614
+ throw filmError('FILM_INVALID_DATATYPE', tagOffset, this.record, 'clarifier-tag', 'Film clarifier tag must be 01 or 02');
615
+ }
616
+ clarifiers.push({
617
+ kind: tag === 0x01 ? 'StringLiteral' : 'NumberLiteral',
618
+ value: descriptor.readString(filmLimits, 'clarifier-value'),
619
+ });
620
+ }
621
+ if (!descriptor.atEnd()) {
622
+ throw filmError('FILM_NONCANONICAL', descriptor.absolutePosition(), this.record, 'datatype', 'Datatype descriptor length was not consumed exactly');
623
+ }
624
+ return { datatype, generics, clarifiers };
625
+ }
626
+ }
627
+ function normalizeAesLimits(options) {
628
+ const { filmLimits: _filmLimits, maxInputBytes: _maxInputBytes, maxRecordBytes: _maxRecordBytes, maxFieldBytes: _maxFieldBytes, maxBufferedBytes: _maxBufferedBytes, registeredFields: _registeredFields, limits, aesLimits, ...flatAesLimits } = options;
629
+ return normalizeTelexLimits({
630
+ ...flatAesLimits,
631
+ ...(limits ?? {}),
632
+ ...(aesLimits ?? {}),
633
+ });
634
+ }
635
+ function limit(source, name, fallback) {
636
+ const value = source[name] ?? fallback;
637
+ if (!Number.isSafeInteger(value) || value < 0) {
638
+ throw new TypeError(`${name} must be a non-negative safe integer`);
639
+ }
640
+ return value;
641
+ }
642
+ function enforceFilmLimit(counter, observed, selected, details) {
643
+ if (observed <= selected)
644
+ return;
645
+ throw new FilmDecodeError('FILM_LIMIT_EXCEEDED', `${counter} observed ${observed}, limit ${selected}`, details);
646
+ }
647
+ function aesLimitError(counter, observed, selected, offset, record, component) {
648
+ const diagnostic = {
649
+ code: 'AES_LIMIT_EXCEEDED',
650
+ message: `${counter} observed value ${observed} exceeds configured limit ${selected}`,
651
+ counter,
652
+ observed,
653
+ limit: selected,
654
+ };
655
+ return new FilmDecodeError('FILM_AES_INVALID', diagnostic.message, {
656
+ offset,
657
+ record,
658
+ component,
659
+ stage: 'aes',
660
+ diagnostics: [diagnostic],
661
+ });
662
+ }
663
+ function claimDatatypeComponent(components, selected, offset, record, component) {
664
+ const observed = components.count + 1;
665
+ if (!Number.isSafeInteger(observed) || observed > selected) {
666
+ throw aesLimitError('max_datatype_components', Number.isSafeInteger(observed) ? observed : selected + 1, selected, offset, record, component);
667
+ }
668
+ components.count = observed;
669
+ }
670
+ function ensureDatatypeComponentCapacity(current, additional, selected, offset, record, component) {
671
+ if (additional > selected - current) {
672
+ throw aesLimitError('max_datatype_components', selected + 1, selected, offset, record, component);
673
+ }
674
+ }
675
+ function throwAesCountLimit(counter, observed, selected, offset, record, component) {
676
+ const diagnosticObserved = observed <= MAX_SAFE_BIGINT ? Number(observed) : selected + 1;
677
+ throw aesLimitError(counter, diagnosticObserved, selected, offset, record, component);
678
+ }
679
+ function filmError(code, offset, record, component, message) {
680
+ return new FilmDecodeError(code, message, { offset, record, component });
681
+ }
682
+ function assertByteInput(input) {
683
+ if (!(input instanceof Uint8Array)) {
684
+ throw new TypeError('Film input must be a Uint8Array');
685
+ }
686
+ }
687
+ function ulebWidth(value) {
688
+ let remaining = value;
689
+ let width = 1;
690
+ while (remaining >= 0x80n) {
691
+ remaining >>= 7n;
692
+ width += 1;
693
+ }
694
+ return width;
695
+ }
696
+ function lowerHex(bytes) {
697
+ let output = '';
698
+ for (const byte of bytes)
699
+ output += byte.toString(16).padStart(2, '0');
700
+ return output;
701
+ }
702
+ //# sourceMappingURL=film-internal.js.map