refactor(managed): share compressed integer format decoding

This commit is contained in:
morluto committed 2026-10-24 04:49:21 +08:00
1 parent 87a5be3000
commit 6d4cc6e6a4
7 files changed
+272 -112

No files matched your search

@@ -318,6 +318,95 @@ describe("managed artifact inventory", () => {
);
});
describe("managed attribute SerString encoding", () => {
it.each([
[0, [0x00]],
[0x7f, [0x7f]],
[0x80, [0x80, 0x80]],
[0x3fff, [0xbf, 0xff]],
[0x4000, [0xc0, 0x00, 0x40, 0x00]],
] as const)(
"reads a target-framework SerString of %i UTF-8 bytes",
(length, prefix) => {
const targetFramework =
"é".repeat(Math.floor(length / 2)) + (length % 2 === 0 ? "" : "A");
const value = Buffer.concat([
Buffer.from([1, 0, ...prefix]),
Buffer.from(targetFramework),
Buffer.from([0, 0]),
]);
const bytes = buildManagedPeFixture({ targetFramework });
const result = inspectManagedArtifactBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.target_frameworks).toEqual([targetFramework]);
expect(result.attributes).toMatchObject([
{
decoded_fixed_string: targetFramework,
value_length: value.length,
value_sha256: createHash("sha256").update(value).digest("hex"),
},
]);
expect(result.coverage).toEqual({ state: "complete", issues: [] });
},
);
it.each([
{ name: "missing length", value: [1, 0], decoded: null },
{ name: "null sentinel", value: [1, 0, 0xff], decoded: null },
{ name: "truncated two-byte prefix", value: [1, 0, 0x80], decoded: null },
{
name: "truncated four-byte prefix",
value: [1, 0, 0xc0, 0, 0],
decoded: null,
},
{ name: "reserved prefix", value: [1, 0, 0xe0], decoded: null },
{ name: "content outside value", value: [1, 0, 5, 0x41], decoded: null },
{
name: "maximum length without content",
value: [1, 0, 0xdf, 0xff, 0xff, 0xff],
decoded: null,
},
{ name: "invalid UTF-8", value: [1, 0, 1, 0xff, 0, 0], decoded: null },
{ name: "invalid prolog", value: [0, 0, 1, 0x41, 0, 0], decoded: null },
{
name: "overlong two-byte length",
value: [1, 0, 0x80, 1, 0x41, 0, 0],
decoded: "A",
},
{
name: "overlong four-byte length",
value: [1, 0, 0xc0, 0, 0, 1, 0x41, 0, 0],
decoded: "A",
},
])("preserves attribute evidence for $name", ({ value, decoded }) => {
const customAttributeValue = Buffer.from(value);
const bytes = buildManagedPeFixture({ customAttributeValue });
const result = inspectManagedArtifactBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.attributes).toMatchObject([
{
parent_token: "0x20000001",
constructor_token: "0x0a000001",
type_name: "System.Runtime.Versioning.TargetFrameworkAttribute",
decoded_fixed_string: decoded,
value_length: customAttributeValue.length,
value_sha256: createHash("sha256")
.update(customAttributeValue)
.digest("hex"),
},
]);
expect(result.target_frameworks).toEqual(decoded === null ? [] : [decoded]);
expect(result.metadata.status).toBe("complete");
expect(result.coverage).toEqual({ state: "complete", issues: [] });
});
});
describe("managed artifact coded indexes", () => {
it("reports an invalid ManifestResource implementation while retaining the resource", () => {
const bytes = buildManagedPeFixture({ resourceImplementationRaw: 9 });
+66
View File
@@ -1993,6 +1993,72 @@ it.each([
},
);
it.each([
[[], "truncated compressed integer"],
[[0x80], "reserved compressed integer"],
[[0xc0, 0, 0], "reserved compressed integer"],
[[0xe0], "reserved compressed integer"],
] as const)(
"retains TypeSpec prefix failure %j at its owning row",
(prefix, issue) => {
const bytes = buildManagedPeFixture({
typeSpecSignatures: [Buffer.from([0x12, ...prefix])],
memberRefParentRaw: (1 << 3) | 4,
});
const pe = readManagedPeLayout(bytes);
if (pe.cli === null) throw new Error("fixture has no CLI header");
const { rva, size } = pe.cli.metadata;
const layout = readManagedMetadataLayout(
bytes,
pe.rvaToOffset(rva, size, "metadata"),
size,
);
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.member_refs[0]?.parent_name).toBeNull();
expect(result.coverage.issues).toContainEqual({
code: "invalid-blob",
scope: "metadata.TypeSpec:0x1b000001",
offset: metadataRowOffset(layout, 27, 1),
detail: issue,
});
},
);
it.each([
[[0x01], -64],
[[0x80, 0x01], -8192],
[[0xc0, 0x00, 0x00, 0x01], -268435456],
] as const)(
"preserves compressed signed width for array bound %j",
(bound, lower) => {
const bytes = buildManagedPeFixture({
fieldSignature: Buffer.from([
0x06,
0x14,
0x08,
0x01,
0x00,
0x01,
...bound,
]),
});
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.fields[0]?.signature).toMatchObject({
parse_status: "decoded",
field_type: `i4[${lower}...]`,
});
expect(result.coverage).toEqual({ state: "complete", issues: [] });
},
);
it("decodes generic instantiation member signatures as complete coverage", () => {
// System.Runtime.Caching 0x06000001: instance void (Dictionary<string, object>)
// shape, with TypeRef/TypeSpec coded tokens kept as encoded.
+7 -27
View File
@@ -23,6 +23,7 @@ import type {
} from "./ManagedMetadataLayout.js";
import {
metadataRowCursor,
readManagedCompressedUnsigned,
decodeMetadataCodedIndex,
metadataCodedIndexCoordinate,
type MetadataCodedIndexCoordinate,
@@ -285,38 +286,17 @@ export const readCompressed = (
bytes: Buffer,
offset: number,
): { readonly value: number; readonly next: number } => {
const first = bytes[offset];
if (first === undefined)
const decoded = readManagedCompressedUnsigned(bytes, offset);
if (!decoded.ok)
throw managedFailure(
"invalid-blob",
"signature",
"truncated compressed integer",
decoded.error.kind === "truncated" && decoded.error.width === 1
? "truncated compressed integer"
: "reserved compressed integer",
offset,
);
if ((first & 0x80) === 0) return { value: first, next: offset + 1 };
const second = bytes[offset + 1];
if ((first & 0xc0) === 0x80 && second !== undefined)
return { value: ((first & 0x3f) << 8) | second, next: offset + 2 };
const b1 = bytes[offset + 1];
const b2 = bytes[offset + 2];
const b3 = bytes[offset + 3];
if (
(first & 0xe0) === 0xc0 &&
b1 !== undefined &&
b2 !== undefined &&
b3 !== undefined
)
return {
value:
(first & 0x1f) * 0x01_00_00_00 + b1 * 0x01_00_00 + b2 * 0x0100 + b3,
next: offset + 4,
};
throw managedFailure(
"invalid-blob",
"signature",
"reserved compressed integer",
offset,
);
return { value: decoded.value.value, next: offset + decoded.value.width };
};
const ELEMENT_TYPES = new Map<number, string>([
+48 -31
View File
@@ -1,5 +1,6 @@
import type { ManagedParseIssue } from "../domain/managed/managedArtifact.js";
import { createHash } from "node:crypto";
import { err, ok, type Result } from "../domain/result.js";
import { managedDecodeBudget } from "./ManagedDecodeBudget.js";
@@ -55,6 +56,39 @@ export class MetadataRowCursor {
}
}
interface ManagedCompressedUnsigned {
readonly value: number;
readonly width: 1 | 2 | 4;
readonly minimal: boolean;
}
type ManagedCompressedUnsignedFailure =
| { readonly kind: "truncated"; readonly width: 1 | 2 | 4 }
| { readonly kind: "reserved" };
/** ECMA-335 unsigned encoding facts, without heap or signature admission policy. */
export const readManagedCompressedUnsigned = (
bytes: Buffer,
offset: number,
): Result<ManagedCompressedUnsigned, ManagedCompressedUnsignedFailure> => {
const first = bytes[offset];
if (first === undefined) return err({ kind: "truncated", width: 1 });
if ((first & 0x80) === 0)
return ok({ value: first, width: 1, minimal: true });
const width =
(first & 0xc0) === 0x80 ? 2 : (first & 0xe0) === 0xc0 ? 4 : null;
if (width === null) return err({ kind: "reserved" });
if (offset > bytes.length - width) return err({ kind: "truncated", width });
const value =
width === 2
? ((first & 0x3f) << 8) | (bytes[offset + 1] ?? 0)
: (first & 0x1f) * 0x01_00_00_00 +
(bytes[offset + 1] ?? 0) * 0x01_00_00 +
(bytes[offset + 2] ?? 0) * 0x0100 +
(bytes[offset + 3] ?? 0);
return ok({ value, width, minimal: value >= (width === 2 ? 0x80 : 0x4000) });
};
// Issue code and label for the heap item a compressed length prefixes.
const PREFIXED_ITEMS = {
blob: { code: "invalid-blob", label: "Metadata blob" },
@@ -73,38 +107,21 @@ const compressedLength = (
readonly minimal: boolean;
} => {
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,
);
const decoded = readManagedCompressedUnsigned(bytes, offset);
if (!decoded.ok)
throw managedFailure(
code,
scope,
decoded.error.kind === "truncated"
? `${label} length prefix leaves the artifact byte range`
: `${label} uses a reserved compressed-length prefix`,
offset >= 0 ? offset : null,
);
return {
length: decoded.value.value,
prefix: decoded.value.width,
minimal: decoded.value.minimal,
};
requirePrefix(1);
const first = bytes[offset] ?? 0;
if ((first & 0x80) === 0) return { length: first, prefix: 1, minimal: true };
if ((first & 0xc0) === 0x80) {
requirePrefix(2);
const length = ((first & 0x3f) << 8) | (bytes[offset + 1] ?? 0);
return { length, prefix: 2, minimal: length >= 0x80 };
}
if ((first & 0xe0) === 0xc0) {
requirePrefix(4);
const length =
(first & 0x1f) * 0x01_00_00_00 +
(bytes[offset + 1] ?? 0) * 0x01_00_00 +
(bytes[offset + 2] ?? 0) * 0x0100 +
(bytes[offset + 3] ?? 0);
return { length, prefix: 4, minimal: length >= 0x4000 };
}
throw managedFailure(
code,
scope,
`${label} uses a reserved compressed-length prefix`,
offset,
);
};
/** Read one UTF-8 value from #Strings within its PE/CLI stream extent. */
+8 -35
View File
@@ -16,6 +16,7 @@ import {
} from "./ManagedMetadataLayout.js";
import {
metadataRowCursor,
readManagedCompressedUnsigned,
decodeMetadataCodedIndex,
type ManagedCodedIndex,
metadataToken,
@@ -432,52 +433,24 @@ const attributeTypeName = (
return null;
};
const decodeCompressedLength = (
blob: Buffer,
offset: number,
): { readonly length: number; readonly prefix: number } | undefined => {
const first = blob[offset];
if (first === undefined) return undefined;
if ((first & 0x80) === 0) return { length: first, prefix: 1 };
if ((first & 0xc0) === 0x80 && blob[offset + 1] !== undefined)
return {
length: ((first & 0x3f) << 8) | (blob[offset + 1] ?? 0),
prefix: 2,
};
if (
(first & 0xe0) === 0xc0 &&
blob[offset + 1] !== undefined &&
blob[offset + 2] !== undefined &&
blob[offset + 3] !== undefined
)
return {
length:
(first & 0x1f) * 0x01_00_00_00 +
(blob[offset + 1] ?? 0) * 0x01_00_00 +
(blob[offset + 2] ?? 0) * 0x0100 +
(blob[offset + 3] ?? 0),
prefix: 4,
};
return undefined;
};
const decodeFixedString = (
blob: Buffer,
layout: ManagedMetadataLayout,
): string | null => {
if (blob.length < 3 || blob.readUInt16LE(0) !== 1 || blob[2] === 0xff)
return null;
const length = decodeCompressedLength(blob, 2);
if (length === undefined) return null;
const start = 2 + length.prefix;
if (start > blob.length - length.length) return null;
const decoded = readManagedCompressedUnsigned(blob, 2);
if (!decoded.ok) return null;
const { value: length, width } = decoded.value;
const start = 2 + width;
if (start > blob.length - length) return null;
managedDecodeBudget(layout).reserveString(
length.length,
length,
layout.blob?.offset ?? layout.rootOffset,
);
try {
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(
blob.subarray(start, start + length.length),
blob.subarray(start, start + length),
);
} catch (cause: unknown) {
// Invalid UTF-8 blobs decode as null.
+26 -19
View File
@@ -8,6 +8,7 @@ interface ManagedPeFixtureOptions {
readonly cliFlags?: number;
readonly corruptMetadataSignature?: boolean;
readonly customAttributeTypeRaw?: number;
readonly customAttributeValue?: Buffer;
readonly extendsRaw?: number;
readonly exportedTypes?: readonly {
readonly namespace: string;
@@ -100,6 +101,18 @@ class StringHeap {
}
}
const compressedUnsigned = (value: number): Buffer =>
value <= 0x7f
? Buffer.from([value])
: value <= 0x3fff
? Buffer.from([0x80 | (value >> 8), value & 0xff])
: Buffer.from([
0xc0 | (value >> 24),
(value >> 16) & 0xff,
(value >> 8) & 0xff,
value & 0xff,
]);
class BlobHeap {
readonly #chunks: Buffer[] = [Buffer.from([0])];
#size = 1;
@@ -111,17 +124,7 @@ class BlobHeap {
add(value: Buffer): number {
if (value.length === 0) return 0;
const index = this.#size;
const prefix =
value.length <= 0x7f
? Buffer.from([value.length])
: value.length <= 0x3fff
? Buffer.from([0x80 | (value.length >> 8), value.length & 0xff])
: Buffer.from([
0xc0 | (value.length >> 24),
(value.length >> 16) & 0xff,
(value.length >> 8) & 0xff,
value.length & 0xff,
]);
const prefix = compressedUnsigned(value.length);
this.#chunks.push(prefix, value);
this.#size += value.length + prefix.length;
return index;
@@ -428,9 +431,12 @@ const exportedTypeRow = (
const fixedStringAttributeBlob = (value: string): Buffer => {
const bytes = Buffer.from(textEncoder.encode(value));
if (bytes.length > 0x7f)
throw new RangeError("Managed PE fixture target framework is too long");
return Buffer.concat([Buffer.from([1, 0, bytes.length]), bytes, u16(0)]);
return Buffer.concat([
Buffer.from([1, 0]),
compressedUnsigned(bytes.length),
bytes,
u16(0),
]);
};
/**
@@ -1061,11 +1067,12 @@ const buildManagedFixtureMetadata = (
);
const referenceBlobs = referenceBlobIndexes(blobs, options);
const attributeBlob = blobs.add(
fixedStringAttributeBlob(
options.targetFramework === null
? ".NETCoreApp,Version=v8.0"
: (options.targetFramework ?? ".NETCoreApp,Version=v8.0"),
),
options.customAttributeValue ??
fixedStringAttributeBlob(
options.targetFramework === null
? ".NETCoreApp,Version=v8.0"
: (options.targetFramework ?? ".NETCoreApp,Version=v8.0"),
),
);
const stringSize = indexSize(strings.size);
const blobSize = indexSize(blobs.size);
+28
View File
@@ -526,3 +526,31 @@ cliTest(
}
},
);
cliTest(
"managed CLI retains target frameworks with multi-byte SerString lengths",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-managed-serstring-");
for (const length of [0x80, 0x4000]) {
const targetFramework = "A".repeat(length);
const path = join(root, `framework-${length}.dll`);
await writeFile(path, buildManagedPeFixture({ targetFramework }));
const result = await cli.run({
arguments: ["inspect-managed-artifact", path, "--json"],
});
expect(result.exitCode).toBe(0);
expect(result.json).toMatchObject({
operation: "inspect_managed_artifact",
normalized_result: {
target_frameworks: [targetFramework],
attributes: [
expect.objectContaining({ decoded_fixed_string: targetFramework }),
],
coverage: { state: "complete", issues: [] },
},
});
}
},
);