fix(managed): classify truncated #US and #Blob length prefixes by item

A two- or four-byte compressed-length prefix cut off by the end of the
artifact went through the generic range check and was reported as
invalid-row. The prefix reader now uses the item's own code for both
reserved and truncated prefixes: invalid-string for #US, invalid-blob for
#Blob. The unused generic range helper is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
NickandClaude Opus 5.5 committed 2026-10-13 07:21:56 +00:00
1 parent 7f05157562
commit 1c20e2f511
2 files changed
+41 -21

No files matched your search

+26 -1
View File
@@ -1,6 +1,9 @@
import { describe, expect, it } from "vitest";
import { readMetadataBlob } from "./ManagedMetadataHeaps.js";
import {
readMetadataBlob,
readMetadataUserString,
} from "./ManagedMetadataHeaps.js";
import type { ManagedMetadataLayout } from "./ManagedMetadataLayout.js";
describe("managed metadata heaps", () => {
@@ -31,4 +34,26 @@ describe("managed metadata heaps", () => {
itemLength,
);
});
it.each([[[0x80]], [[0xc0, 0x00, 0x00]]])(
"reports a #US length prefix cut off by the end of the artifact as a string failure",
(prefix) => {
// #US ends at the end of the artifact, inside a multi-byte length prefix.
const bytes = Buffer.from([0x00, ...prefix]);
const layout = {
userStrings: { name: "#US", offset: 0, size: bytes.length },
} as unknown as ManagedMetadataLayout;
expect(() =>
readMetadataUserString(bytes, layout, "0x70000001"),
).toThrowError(
expect.objectContaining({
issue: expect.objectContaining({
code: "invalid-string",
scope: "metadata.#US:0x70000001",
}),
}),
);
},
);
});
+15 -20
View File
@@ -8,21 +8,6 @@ import {
} from "./ManagedMetadataLayout.js";
import { managedFailure } from "./ManagedReaderFailure.js";
const requireRange = (
bytes: Buffer,
offset: number,
length: number,
scope: string,
): void => {
if (offset < 0 || length < 0 || offset > bytes.length - length)
throw managedFailure(
"invalid-row",
scope,
`${scope} leaves the artifact byte range`,
offset >= 0 ? offset : null,
);
};
/** Cursor for checked fixed-width metadata row decoding. */
export class MetadataRowCursor {
#offset: number;
@@ -81,18 +66,28 @@ const compressedLength = (
scope: string,
item: keyof typeof PREFIXED_ITEMS,
): { readonly length: number; readonly prefix: number } => {
requireRange(bytes, offset, 1, scope);
const { code, label } = PREFIXED_ITEMS[item];
const requirePrefix = (size: number): void => {
if (offset < 0 || offset > bytes.length - size)
throw managedFailure(
code,
scope,
`${label} length prefix leaves the artifact byte range`,
offset >= 0 ? offset : null,
);
};
requirePrefix(1);
const first = bytes[offset] ?? 0;
if ((first & 0x80) === 0) return { length: first, prefix: 1 };
if ((first & 0xc0) === 0x80) {
requireRange(bytes, offset, 2, scope);
requirePrefix(2);
return {
length: ((first & 0x3f) << 8) | (bytes[offset + 1] ?? 0),
prefix: 2,
};
}
if ((first & 0xe0) === 0xc0) {
requireRange(bytes, offset, 4, scope);
requirePrefix(4);
return {
length:
(first & 0x1f) * 0x01_00_00_00 +
@@ -103,9 +98,9 @@ const compressedLength = (
};
}
throw managedFailure(
PREFIXED_ITEMS[item].code,
code,
scope,
`${PREFIXED_ITEMS[item].label} uses a reserved compressed-length prefix`,
`${label} uses a reserved compressed-length prefix`,
offset,
);
};