From 406f4e5b4ac279a8178ba3db844c21c919efedfa Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Fri, 23 Oct 2026 14:24:06 +0000 Subject: [PATCH 1/4] test(cachekit): drive decode-bounds vectors through both ByteStorage.unpack bindings cachekit-core 0.7.0 pre-scans the envelope bytes in ByteStorage::retrieve before its typed decode. Run every decode-bounds.json vector through the NAPI unpack in the Node lane and the wasm unpack inside workerd, so both envelope entry points prove they apply it. Reject vectors must fail with the pre-scan message matched from its start; vectors that break one rule alone also pin that rule's reason, because the pre-scan's end-of-input rejection carries the same prefix. Accept vectors must get past the pre-scan. The vector loader and these assertions move to a shared fixture module that workerd can load. --- .../test/fixtures/decode-bounds-vectors.ts | 116 ++++++++++++++++++ .../protocol/decode-bounds.protocol.test.ts | 88 +++++-------- .../workers/decode-bounds.workers.test.ts | 30 +++++ 3 files changed, 180 insertions(+), 54 deletions(-) create mode 100644 packages/cachekit/test/fixtures/decode-bounds-vectors.ts create mode 100644 packages/cachekit/test/workers/decode-bounds.workers.test.ts diff --git a/packages/cachekit/test/fixtures/decode-bounds-vectors.ts b/packages/cachekit/test/fixtures/decode-bounds-vectors.ts new file mode 100644 index 00000000..77d89fb1 --- /dev/null +++ b/packages/cachekit/test/fixtures/decode-bounds-vectors.ts @@ -0,0 +1,116 @@ +/** + * The vendored protocol/test-vectors/decode-bounds.json, typed, plus the + * envelope pre-scan assertions both ByteStorage lanes use: the NAPI lane + * (test/protocol/decode-bounds.protocol.test.ts, which sha256-pins the file) + * and the Workers lane (test/workers/decode-bounds.workers.test.ts). Plain TS + * with no node:* imports, so workerd can load it. + */ + +import { expect } from 'vitest'; +import fixture from '../protocol/fixtures/decode-bounds.json' with { type: 'json' }; +import { assertFixture, table, type ObjectShape } from './fixture-shape.js'; +import { hexToBytes } from './wire-vectors.js'; + +export interface Vector { + name: string; + construction: { repeat_hex: string; count: number; suffix_hex: string }; + input_hex: string; + nesting_depth: number; + declared_slots: number; + reject_reasons?: string[]; +} + +interface VectorFile { + spec: string; + reject_vectors: Vector[]; + accept_vectors: Vector[]; +} + +const VECTOR_SHAPE: ObjectShape = { + name: 'string', + construction: { repeat_hex: 'string', count: 'number', suffix_hex: 'string' }, + input_hex: 'string', + nesting_depth: 'number', + declared_slots: 'number', + reject_reasons: [table('string'), 'undefined'], +}; + +function assertVectorFile(value: unknown): asserts value is VectorFile { + assertFixture( + value, + { spec: 'string', reject_vectors: table(VECTOR_SHAPE), accept_vectors: table(VECTOR_SHAPE) }, + 'decode-bounds.json' + ); +} + +const parsed: unknown = fixture; +assertVectorFile(parsed); +export const vectors: VectorFile = parsed; + +/** field_notes.construction: input = fromhex(repeat_hex) * count + fromhex(suffix_hex). */ +export function build(v: Vector): Uint8Array { + const unit = hexToBytes(v.construction.repeat_hex); + const suffix = hexToBytes(v.construction.suffix_hex); + const bytes = new Uint8Array(unit.length * v.construction.count + suffix.length); + for (let i = 0; i < v.construction.count; i++) bytes.set(unit, i * unit.length); + bytes.set(suffix, unit.length * v.construction.count); + return bytes; +} + +/** + * How both `unpack` bindings surface a cachekit-core envelope pre-scan + * rejection: `ByteStorageError::DeserializationFailed`'s display, then the + * pre-scan's own tag, then the reason. + */ +const ENVELOPE_PRE_SCAN = 'deserialization failed: decode pre-scan: '; + +/** cachekit-core's nesting bound for the envelope decode (not this package's). */ +const ENVELOPE_MAX_DEPTH = 100; + +/** + * The error a reject vector must raise at `unpack`, matched from its start: a + * typed-decode error can echo attacker bytes mid-message, so only a prefix + * match is unforgeable. + * + * The prefix alone is not enough. The pre-scan's own end-of-input rejection + * carries it too, so with the slot check deleted every overclaim-only vector + * still fails with the prefix, once the walk runs out of input. Vectors that + * break one rule alone therefore pin that rule's reason, as cachekit-core's + * own tests/decode_bounds_vectors.rs does: overclaim-only ones the byte or slot + * budget, depth-only ones (structurally complete, so nothing else can reject + * them) the depth bound itself. + */ +function envelopeRejection(v: Vector): RegExp { + switch (v.reject_reasons?.join()) { + case 'depth': + return new RegExp(`^${ENVELOPE_PRE_SCAN}nests deeper than ${ENVELOPE_MAX_DEPTH} levels$`); + case 'overclaim': + return new RegExp(`^${ENVELOPE_PRE_SCAN}declares more `); + default: + return new RegExp(`^${ENVELOPE_PRE_SCAN}`); + } +} + +export function expectEnvelopePreScanRejects( + unpack: (bytes: Uint8Array) => unknown, + v: Vector +): void { + expect(() => unpack(build(v))).toThrow(envelopeRejection(v)); +} + +/** + * An accept vector is not an envelope, so the typed decode after the pre-scan + * may still refuse it; the pre-scan itself must not. + */ +export function expectEnvelopePreScanAccepts( + unpack: (bytes: Uint8Array) => unknown, + v: Vector +): void { + let message = ''; + try { + unpack(build(v)); + } catch (error) { + message = error instanceof Error ? error.message : String(error); + } + expect(message).not.toContain('decode pre-scan: '); +} diff --git a/packages/cachekit/test/protocol/decode-bounds.protocol.test.ts b/packages/cachekit/test/protocol/decode-bounds.protocol.test.ts index 56889af3..ce068683 100644 --- a/packages/cachekit/test/protocol/decode-bounds.protocol.test.ts +++ b/packages/cachekit/test/protocol/decode-bounds.protocol.test.ts @@ -6,13 +6,12 @@ * package: the auto-mode serializer, the interop/v1 value decoder and the * invalidation-event decoder. Spec: protocol/spec/interop-mode.md#decode-bounds * (LAB-2503); the bounds themselves are `assertDecodeDepth` (LAB-2487). The - * ByteStorage envelope is decoded in Rust (cachekit-core, reached via NAPI - * `unpack` / wasm), not here. cachekit-io/cachekit-core#80 adds the envelope - * pre-scan to `ByteStorage::retrieve` and runs the protocol's - * decode-bounds.json 1.1.0 through it in - * core CI. The cachekit-core 0.6.0 this repo pins has no envelope pre-scan, so - * `unpack` gains the guard only when the pin moves to a core release that - * carries it. + * ByteStorage envelope is decoded in Rust instead: `unpack` reaches + * cachekit-core's `ByteStorage::retrieve`, which pre-scans the envelope bytes + * before decoding them (cachekit-core 0.7.0 and later). The envelope entry + * point is covered in both lanes: the NAPI `unpack` here, and the wasm + * `unpack` inside workerd in test/workers/decode-bounds.workers.test.ts. Both + * share their assertions through test/fixtures/decode-bounds-vectors.ts. * * Provenance: cachekit-io/protocol @ 42ea601f (the merge of * cachekit-io/protocol#164, the revision that last touched the fixture; @@ -30,6 +29,7 @@ import { readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, it, expect } from 'vitest'; +import { ByteStorage } from '@cachekit-io/cachekit-core-ts'; import { MessagePackSerializer, assertDecodeDepth } from '../../src/serialization/serializer.js'; import { decodeInteropValue } from '../../src/serialization/interop.js'; import { deserializeEvent } from '../../src/invalidation/event.js'; @@ -39,60 +39,23 @@ import { MAX_INVALIDATION_EVENT_DEPTH, } from '../../src/constants.js'; import { SerializationError } from '../../src/errors.js'; -import { assertFixture, table, type ObjectShape } from '../fixtures/fixture-shape.js'; +import { + build, + expectEnvelopePreScanAccepts, + expectEnvelopePreScanRejects, + vectors, + type Vector, +} from '../fixtures/decode-bounds-vectors.js'; /** sha256 of test-vectors/decode-bounds.json at the provenance above. */ const FIXTURE_SHA256 = 'c52c27f724fe138e63440dc0306936b48fe389e2bd823c058b86b30d854e2e2e'; // pragma: allowlist secret -interface Vector { - name: string; - construction: { repeat_hex: string; count: number; suffix_hex: string }; - input_hex: string; - nesting_depth: number; - declared_slots: number; - reject_reasons?: string[]; -} - -interface VectorFile { - spec: string; - reject_vectors: Vector[]; - accept_vectors: Vector[]; -} - +// Raw bytes of the same file the shared module imports: the pin covers every +// byte, not a re-serialisation. const raw = readFileSync( join(dirname(fileURLToPath(import.meta.url)), 'fixtures', 'decode-bounds.json') ); -const VECTOR_SHAPE: ObjectShape = { - name: 'string', - construction: { repeat_hex: 'string', count: 'number', suffix_hex: 'string' }, - input_hex: 'string', - nesting_depth: 'number', - declared_slots: 'number', - reject_reasons: [table('string'), 'undefined'], -}; - -function assertVectorFile(value: unknown): asserts value is VectorFile { - assertFixture( - value, - { spec: 'string', reject_vectors: table(VECTOR_SHAPE), accept_vectors: table(VECTOR_SHAPE) }, - 'decode-bounds.json' - ); -} - -const parsed: unknown = JSON.parse(raw.toString('utf8')); -assertVectorFile(parsed); -const vectors: VectorFile = parsed; - -/** field_notes.construction: input = fromhex(repeat_hex) * count + fromhex(suffix_hex). */ -function build(v: Vector): Buffer { - const unit = Buffer.from(v.construction.repeat_hex, 'hex'); - return Buffer.concat([ - Buffer.alloc(unit.length * v.construction.count, unit), - Buffer.from(v.construction.suffix_hex, 'hex'), - ]); -} - const serializer = new MessagePackSerializer(); /** What each accept vector must decode to on the value paths (keyed by name; a @@ -204,7 +167,7 @@ describe('Protocol decode-bounds vectors (spec/interop-mode.md#decode-bounds)', it('every construction reproduces its input_hex', () => { for (const v of [...vectors.reject_vectors, ...vectors.accept_vectors]) { - expect(build(v).toString('hex'), v.name).toBe(v.input_hex); + expect(Buffer.from(build(v)).toString('hex'), v.name).toBe(v.input_hex); } }); @@ -267,6 +230,23 @@ describe('Protocol decode-bounds vectors (spec/interop-mode.md#decode-bounds)', }); }); + /** + * The ByteStorage envelope entry point: cachekit-core pre-scans the envelope + * bytes before its typed decode, so each vector arrives there as a forged + * envelope. The Workers lane runs the same assertions through the wasm build. + */ + describe('ByteStorage.unpack (NAPI)', () => { + const storage = new ByteStorage(); + + it.each(vectors.reject_vectors)('rejects $name at the envelope pre-scan', (v) => { + expectEnvelopePreScanRejects((b) => storage.unpack(b), v); + }); + + it.each(vectors.accept_vectors)('gets $name past the envelope pre-scan', (v) => { + expectEnvelopePreScanAccepts((b) => storage.unpack(b), v); + }); + }); + describe('accept vectors', () => { it.each(vectors.accept_vectors)('MessagePackSerializer.decode accepts $name', (v) => { expect(serializer.decode(build(v))).toEqual(EXPECTED[v.name]); diff --git a/packages/cachekit/test/workers/decode-bounds.workers.test.ts b/packages/cachekit/test/workers/decode-bounds.workers.test.ts new file mode 100644 index 00000000..ebc3cbc2 --- /dev/null +++ b/packages/cachekit/test/workers/decode-bounds.workers.test.ts @@ -0,0 +1,30 @@ +/** + * Decode Bounds — Workers lane + * + * Runs protocol/test-vectors/decode-bounds.json (vendored in + * test/protocol/fixtures/ and sha256-pinned by the Node lane, + * test/protocol/decode-bounds.protocol.test.ts) through the wasm ByteStorage + * `unpack` inside real workerd: the Workers build's envelope entry point must + * apply cachekit-core's envelope pre-scan exactly as the NAPI one does. The + * assertions are shared with the Node lane (test/fixtures/decode-bounds-vectors.ts). + */ + +import { describe, it } from 'vitest'; +import { ByteStorage } from '../../src/workers/runtime.js'; +import { + expectEnvelopePreScanAccepts, + expectEnvelopePreScanRejects, + vectors, +} from '../fixtures/decode-bounds-vectors.js'; + +describe('decode-bounds vectors (wasm ByteStorage.unpack)', () => { + const storage = new ByteStorage(); + + it.each(vectors.reject_vectors)('rejects $name at the envelope pre-scan', (v) => { + expectEnvelopePreScanRejects((b) => storage.unpack(b), v); + }); + + it.each(vectors.accept_vectors)('gets $name past the envelope pre-scan', (v) => { + expectEnvelopePreScanAccepts((b) => storage.unpack(b), v); + }); +}); From 47bcd1faae02a03c05444fcc269db61ab1cf15e5 Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Fri, 23 Oct 2026 14:24:58 +0000 Subject: [PATCH 2/4] test(cachekit): match the decode-bounds header to the ticket-id cleanup --- .../cachekit/test/protocol/decode-bounds.protocol.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cachekit/test/protocol/decode-bounds.protocol.test.ts b/packages/cachekit/test/protocol/decode-bounds.protocol.test.ts index ce068683..5cd78074 100644 --- a/packages/cachekit/test/protocol/decode-bounds.protocol.test.ts +++ b/packages/cachekit/test/protocol/decode-bounds.protocol.test.ts @@ -4,8 +4,8 @@ * Executes protocol/test-vectors/decode-bounds.json (vendored in ./fixtures/, * sha256-pinned below) against every untrusted MessagePack decode site in this * package: the auto-mode serializer, the interop/v1 value decoder and the - * invalidation-event decoder. Spec: protocol/spec/interop-mode.md#decode-bounds - * (LAB-2503); the bounds themselves are `assertDecodeDepth` (LAB-2487). The + * invalidation-event decoder. Spec: protocol/spec/interop-mode.md#decode-bounds. + * The bounds themselves are `assertDecodeDepth`. The * ByteStorage envelope is decoded in Rust instead: `unpack` reaches * cachekit-core's `ByteStorage::retrieve`, which pre-scans the envelope bytes * before decoding them (cachekit-core 0.7.0 and later). The envelope entry From 0ac479a6cf3f7b586d0e94f0b75029d8aca15be0 Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Fri, 23 Oct 2026 14:50:30 +0000 Subject: [PATCH 3/4] test(cachekit): tighten the envelope decode-bounds assertions Accept vectors now require core's typed-decode failure rather than any error without the pre-scan tag, so a trap or other non-decode error cannot pass for "past the pre-scan". build() hands every site an offset view with reserved-marker guard bytes on both sides, restoring the byteOffset coverage the pooled Buffer gave the value sites. The fixture is checked by its typed JSON import, as wire-vectors.ts does, and build() reuses construct(). --- .../test/fixtures/decode-bounds-vectors.ts | 89 +++++++++---------- .../protocol/decode-bounds.protocol.test.ts | 5 -- 2 files changed, 41 insertions(+), 53 deletions(-) diff --git a/packages/cachekit/test/fixtures/decode-bounds-vectors.ts b/packages/cachekit/test/fixtures/decode-bounds-vectors.ts index 77d89fb1..ecca936a 100644 --- a/packages/cachekit/test/fixtures/decode-bounds-vectors.ts +++ b/packages/cachekit/test/fixtures/decode-bounds-vectors.ts @@ -8,8 +8,7 @@ import { expect } from 'vitest'; import fixture from '../protocol/fixtures/decode-bounds.json' with { type: 'json' }; -import { assertFixture, table, type ObjectShape } from './fixture-shape.js'; -import { hexToBytes } from './wire-vectors.js'; +import { construct } from './wire-vectors.js'; export interface Vector { name: string; @@ -26,46 +25,38 @@ interface VectorFile { accept_vectors: Vector[]; } -const VECTOR_SHAPE: ObjectShape = { - name: 'string', - construction: { repeat_hex: 'string', count: 'number', suffix_hex: 'string' }, - input_hex: 'string', - nesting_depth: 'number', - declared_slots: 'number', - reject_reasons: [table('string'), 'undefined'], -}; +// Annotated, not cast: `pnpm type-check:tests` then checks every entry of the +// JSON import against the interface, where a cast lets a short entry through. +export const vectors: VectorFile = fixture; -function assertVectorFile(value: unknown): asserts value is VectorFile { - assertFixture( - value, - { spec: 'string', reject_vectors: table(VECTOR_SHAPE), accept_vectors: table(VECTOR_SHAPE) }, - 'decode-bounds.json' - ); -} - -const parsed: unknown = fixture; -assertVectorFile(parsed); -export const vectors: VectorFile = parsed; +/** Bytes either side of a built input; see build(). */ +const GUARD = 8; -/** field_notes.construction: input = fromhex(repeat_hex) * count + fromhex(suffix_hex). */ +/** + * field_notes.construction: input = fromhex(repeat_hex) * count + fromhex(suffix_hex). + * + * Returned as a view at a non-zero offset into a larger buffer, as a pooled + * Node Buffer or a network read arrives, with the reserved marker 0xc1 on both + * sides: a decoder that ignores byteOffset, or reads past byteLength, sees + * bytes that are not the input. + */ export function build(v: Vector): Uint8Array { - const unit = hexToBytes(v.construction.repeat_hex); - const suffix = hexToBytes(v.construction.suffix_hex); - const bytes = new Uint8Array(unit.length * v.construction.count + suffix.length); - for (let i = 0; i < v.construction.count; i++) bytes.set(unit, i * unit.length); - bytes.set(suffix, unit.length * v.construction.count); - return bytes; + const input = construct([ + { hex: v.construction.repeat_hex, count: v.construction.count }, + { hex: v.construction.suffix_hex, count: 1 }, + ]); + const padded = new Uint8Array(input.length + 2 * GUARD).fill(0xc1); + padded.set(input, GUARD); + return padded.subarray(GUARD, GUARD + input.length); } /** - * How both `unpack` bindings surface a cachekit-core envelope pre-scan - * rejection: `ByteStorageError::DeserializationFailed`'s display, then the - * pre-scan's own tag, then the reason. + * How both `unpack` bindings surface a cachekit-core envelope decode failure: + * `ByteStorageError::DeserializationFailed`'s display. A pre-scan rejection + * then carries the pre-scan's own tag, then the reason. */ -const ENVELOPE_PRE_SCAN = 'deserialization failed: decode pre-scan: '; - -/** cachekit-core's nesting bound for the envelope decode (not this package's). */ -const ENVELOPE_MAX_DEPTH = 100; +const DESERIALIZATION_FAILED = 'deserialization failed: '; +const ENVELOPE_PRE_SCAN = `${DESERIALIZATION_FAILED}decode pre-scan: `; /** * The error a reject vector must raise at `unpack`, matched from its start: a @@ -73,20 +64,24 @@ const ENVELOPE_MAX_DEPTH = 100; * match is unforgeable. * * The prefix alone is not enough. The pre-scan's own end-of-input rejection - * carries it too, so with the slot check deleted every overclaim-only vector - * still fails with the prefix, once the walk runs out of input. Vectors that - * break one rule alone therefore pin that rule's reason, as cachekit-core's + * carries it too, so with the slot check deleted the overclaim-only vectors it + * guards still fail with the prefix, once the walk runs out of input. Depth-only and + * overclaim-only vectors therefore pin their rule's reason, as cachekit-core's * own tests/decode_bounds_vectors.rs does: overclaim-only ones the byte or slot * budget, depth-only ones (structurally complete, so nothing else can reject - * them) the depth bound itself. + * them) the depth bound itself, which is cachekit-core's, not this package's. */ function envelopeRejection(v: Vector): RegExp { switch (v.reject_reasons?.join()) { case 'depth': - return new RegExp(`^${ENVELOPE_PRE_SCAN}nests deeper than ${ENVELOPE_MAX_DEPTH} levels$`); + return new RegExp(`^${ENVELOPE_PRE_SCAN}nests deeper than 100 levels$`); case 'overclaim': return new RegExp(`^${ENVELOPE_PRE_SCAN}declares more `); default: + // A vector that breaks several rules can trip any of them first. An + // incomplete-only vector fits the spec's slot budget but not core's + // stricter running one, so core rejects it there rather than at end of + // input. Either is the pre-scan, so only the prefix is pinned. return new RegExp(`^${ENVELOPE_PRE_SCAN}`); } } @@ -100,17 +95,15 @@ export function expectEnvelopePreScanRejects( /** * An accept vector is not an envelope, so the typed decode after the pre-scan - * may still refuse it; the pre-scan itself must not. + * refuses it, and that refusal is required: an error that is not core's + * decode failure (a wasm trap, say) would otherwise pass for "past the + * pre-scan" without showing the pre-scan ran to completion. */ export function expectEnvelopePreScanAccepts( unpack: (bytes: Uint8Array) => unknown, v: Vector ): void { - let message = ''; - try { - unpack(build(v)); - } catch (error) { - message = error instanceof Error ? error.message : String(error); - } - expect(message).not.toContain('decode pre-scan: '); + expect(() => unpack(build(v))).toThrow( + new RegExp(`^${DESERIALIZATION_FAILED}(?!decode pre-scan: )`) + ); } diff --git a/packages/cachekit/test/protocol/decode-bounds.protocol.test.ts b/packages/cachekit/test/protocol/decode-bounds.protocol.test.ts index 5cd78074..9ee8e92d 100644 --- a/packages/cachekit/test/protocol/decode-bounds.protocol.test.ts +++ b/packages/cachekit/test/protocol/decode-bounds.protocol.test.ts @@ -230,11 +230,6 @@ describe('Protocol decode-bounds vectors (spec/interop-mode.md#decode-bounds)', }); }); - /** - * The ByteStorage envelope entry point: cachekit-core pre-scans the envelope - * bytes before its typed decode, so each vector arrives there as a forged - * envelope. The Workers lane runs the same assertions through the wasm build. - */ describe('ByteStorage.unpack (NAPI)', () => { const storage = new ByteStorage(); From b41d3c593b4425581125d2b789a4ffd1291dc0df Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Fri, 23 Oct 2026 15:01:35 +0000 Subject: [PATCH 4/4] test(cachekit): spell the pre-scan tag once in the envelope assertions --- .../test/fixtures/decode-bounds-vectors.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/cachekit/test/fixtures/decode-bounds-vectors.ts b/packages/cachekit/test/fixtures/decode-bounds-vectors.ts index ecca936a..7efb5da8 100644 --- a/packages/cachekit/test/fixtures/decode-bounds-vectors.ts +++ b/packages/cachekit/test/fixtures/decode-bounds-vectors.ts @@ -56,7 +56,8 @@ export function build(v: Vector): Uint8Array { * then carries the pre-scan's own tag, then the reason. */ const DESERIALIZATION_FAILED = 'deserialization failed: '; -const ENVELOPE_PRE_SCAN = `${DESERIALIZATION_FAILED}decode pre-scan: `; +const PRE_SCAN_TAG = 'decode pre-scan: '; +const ENVELOPE_PRE_SCAN = `${DESERIALIZATION_FAILED}${PRE_SCAN_TAG}`; /** * The error a reject vector must raise at `unpack`, matched from its start: a @@ -65,11 +66,12 @@ const ENVELOPE_PRE_SCAN = `${DESERIALIZATION_FAILED}decode pre-scan: `; * * The prefix alone is not enough. The pre-scan's own end-of-input rejection * carries it too, so with the slot check deleted the overclaim-only vectors it - * guards still fail with the prefix, once the walk runs out of input. Depth-only and - * overclaim-only vectors therefore pin their rule's reason, as cachekit-core's - * own tests/decode_bounds_vectors.rs does: overclaim-only ones the byte or slot - * budget, depth-only ones (structurally complete, so nothing else can reject - * them) the depth bound itself, which is cachekit-core's, not this package's. + * guards still fail with the prefix, once the walk runs out of input. + * Depth-only and overclaim-only vectors therefore pin their rule's reason, as + * cachekit-core's own tests/decode_bounds_vectors.rs does: overclaim-only ones + * the byte or slot budget, depth-only ones (structurally complete, so nothing + * else can reject them) the depth bound itself, which is cachekit-core's, not + * this package's. */ function envelopeRejection(v: Vector): RegExp { switch (v.reject_reasons?.join()) { @@ -104,6 +106,6 @@ export function expectEnvelopePreScanAccepts( v: Vector ): void { expect(() => unpack(build(v))).toThrow( - new RegExp(`^${DESERIALIZATION_FAILED}(?!decode pre-scan: )`) + new RegExp(`^${DESERIALIZATION_FAILED}(?!${PRE_SCAN_TAG})`) ); }