diff --git a/src/dotnet/ManagedMemberInspector.test.ts b/src/dotnet/ManagedMemberInspector.test.ts index b91407969e..16fbcacac4 100644 --- a/src/dotnet/ManagedMemberInspector.test.ts +++ b/src/dotnet/ManagedMemberInspector.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it } from "vitest"; import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js"; import { ManagedDecodeBudget } from "./ManagedDecodeBudget.js"; import { edges } from "./ManagedMemberRows.js"; -import type { ManagedMethod } from "./ManagedMemberInspectorCore.js"; +import { signature, type ManagedMethod } from "./ManagedMemberInspectorCore.js"; import { ManagedReaderFailure } from "./ManagedReaderFailure.js"; import { managedMemberInspectionSchema } from "../domain/managed/managedArtifact.js"; import { comparisonLimitations } from "../domain/managed/managedMemberComparisonCoverage.js"; @@ -499,18 +499,197 @@ it.each([ }, ); -it("keeps an admitted but unimplemented element type unsupported", () => { - // ECMA-335 ELEMENT_TYPE_ARRAY is valid, but multidimensional arrays are not decoded here. +it("decodes generic instantiation member signatures as complete coverage", () => { + // System.Runtime.Caching 0x06000001: instance void (Dictionary) + // shape, with TypeRef/TypeSpec coded tokens kept as encoded. const bytes = buildManagedPeFixture({ - fieldSignature: Buffer.from([6, 0x14, 8, 1, 0, 0]), + methodSignature: Buffer.from([ + 0x00, 0x01, 0x01, 0x15, 0x12, 0x71, 0x01, 0x0e, + ]), + fieldSignature: Buffer.from([ + 0x06, 0x15, 0x11, 0x0a, 0x02, 0x13, 0x00, 0x1e, 0x01, + ]), }); const result = inspectManagedMembersBytes( bytes, managedPeFixtureTarget(bytes), ); + + expect(result.methods[0]?.signature).toMatchObject({ + parse_status: "decoded", + return_type: "void", + parameter_types: ["class:113"], + issue: null, + }); expect(result.fields[0]?.signature).toMatchObject({ + parse_status: "decoded", + field_type: "valuetype:10", + }); + expect(result.coverage).toMatchObject({ state: "complete", issues: [] }); +}); + +it.each([ + ["single-dimension general array", [0x14, 0x08, 0x01, 0x00, 0x00], "i4[...]"], + [ + "sized array with signed lower bounds", + [0x14, 0x08, 0x02, 0x02, 0x04, 0x03, 0x02, 0x02, 0x7f], + "i4[1...4,-1...1]", + ], + [ + "partially bounded array", + [0x14, 0x0e, 0x03, 0x01, 0x05, 0x00], + "string[5,,]", + ], + [ + "function pointer", + [0x1b, 0x00, 0x02, 0x08, 0x0e, 0x1c], + "fnptr[default](string,object)->i4", + ], + ["required modifier", [0x1f, 0x09, 0x08], "i4 modreq:9"], + [ + "optional modifier under a pointer", + [0x0f, 0x20, 0x0d, 0x08], + "i4 modopt:13*", + ], + [ + "adjacent modifiers in source order", + [0x1f, 0x05, 0x20, 0x09, 0x08], + "i4 modreq:5 modopt:9", + ], + [ + "modifier runs separated by a pointer", + [0x1f, 0x05, 0x0f, 0x20, 0x09, 0x08], + "i4 modopt:9* modreq:5", + ], + [ + "unmanaged cdecl function pointer", + [0x1b, 0x01, 0x01, 0x01, 0x18], + "fnptr[c](native-int)->void", + ], + [ + "array of generic instantiations", + [0x1d, 0x15, 0x12, 0x05, 0x01, 0x14, 0x08, 0x01, 0x01, 0x03, 0x00], + "class:5[]", + ], +])("decodes a %s field signature", (_name, type, expected) => { + expect(signature(Buffer.from([0x06, ...type]))).toMatchObject({ + kind: "field", + parse_status: "decoded", + field_type: expected, + issue: null, + }); +}); + +it.each([ + ["generic instantiation without arguments", [0x15, 0x12, 0x05, 0x00]], + ["generic instantiation of a primitive", [0x15, 0x08, 0x05, 0x01, 0x08]], + ["zero-rank array", [0x14, 0x08, 0x00, 0x00, 0x00]], + [ + "array with more sizes than dimensions", + [0x14, 0x08, 0x01, 0x02, 0x01, 0x01, 0x00], + ], + ["truncated generic argument list", [0x15, 0x12, 0x05, 0x02, 0x08]], + ["truncated function pointer", [0x1b, 0x00, 0x01, 0x08]], + ["function pointer with a field header", [0x1b, 0x06, 0x00, 0x01]], + ["function pointer with a reserved flag", [0x1b, 0x80, 0x00, 0x01]], + [ + "function pointer with explicit-this but no has-this", + [0x1b, 0x40, 0x00, 0x01], + ], + ["typed reference field", [0x16]], + ["modified typed reference field", [0x20, 0x09, 0x16]], + ["typed reference generic argument", [0x15, 0x12, 0x05, 0x01, 0x16]], + ["typed reference array element", [0x1d, 0x16]], + ["by-reference generic argument", [0x15, 0x12, 0x05, 0x01, 0x10, 0x08]], + ["void generic argument", [0x15, 0x12, 0x05, 0x01, 0x01]], + ["void function pointer parameter", [0x1b, 0x00, 0x01, 0x01, 0x01]], + ["by-reference pointee", [0x0f, 0x10, 0x08]], + ["nested by-reference", [0x10, 0x10, 0x08]], + ["void field", [0x01]], + ["void array element", [0x1d, 0x01]], +])("reports a %s as malformed", (_name, type) => { + expect(signature(Buffer.from([0x06, ...type]))).toMatchObject({ + kind: "unknown", + parse_status: "malformed", + }); +}); + +it("rejects array ranks beyond the CLR limit before rendering dimensions", () => { + expect( + signature( + Buffer.from([0x06, 0x14, 0x08, 0xdf, 0xff, 0xff, 0xff, 0x00, 0x00]), + ), + ).toMatchObject({ parse_status: "unsupported", - issue: "unsupported element type 0x14", + issue: "array rank 536870911 exceeds the 32 dimensions the CLR supports", + }); + expect( + signature(Buffer.from([0x06, 0x14, 0x08, 0x20, 0x00, 0x00])), + ).toMatchObject({ + parse_status: "decoded", + field_type: `i4[${",".repeat(31)}]`, + }); +}); + +it("decodes typed references in parameter and return positions", () => { + expect( + signature(Buffer.from([0x00, 0x02, 0x16, 0x20, 0x09, 0x16, 0x08])), + ).toMatchObject({ + kind: "method", + parse_status: "decoded", + return_type: "typedbyref", + parameter_types: ["typedbyref modopt:9", "i4"], + }); + expect( + signature(Buffer.from([0x06, 0x1b, 0x20, 0x01, 0x01, 0x16])), + ).toMatchObject({ + parse_status: "decoded", + field_type: "fnptr[default has-this](typedbyref)->void", + }); +}); + +it.each([ + ["by-reference field", [0x06, 0x10, 0x08], { field_type: "i4&" }], + ["void pointer field", [0x06, 0x0f, 0x01], { field_type: "void*" }], + [ + "by-reference function pointer return and parameter", + [0x06, 0x1b, 0x00, 0x01, 0x10, 0x08, 0x10, 0x0e], + { field_type: "fnptr[default](string&)->i4&" }, + ], + [ + "by-reference method return and parameter", + [0x00, 0x01, 0x10, 0x08, 0x10, 0x15, 0x12, 0x05, 0x01, 0x0f, 0x01], + { return_type: "i4&", parameter_types: ["class:5&"] }, + ], +])("decodes a %s in its grammar position", (_name, blob, expected) => { + expect(signature(Buffer.from(blob))).toMatchObject({ + parse_status: "decoded", + ...expected, + }); +}); + +it("keeps element types that member signatures do not assign unsupported", () => { + expect(signature(Buffer.from([0x06, 0x45, 0x08]))).toMatchObject({ + parse_status: "unsupported", + issue: "unsupported element type 0x45", + }); +}); + +it("decodes deeply nested generic instantiations without recursion", () => { + const depth = 4_000; + const decoded = signature( + Buffer.concat([ + Buffer.from([0x06]), + Buffer.from( + Array.from({ length: depth }, () => [0x15, 0x12, 0x05, 0x01]).flat(), + ), + Buffer.from([0x08]), + ]), + ); + + expect(decoded).toMatchObject({ + parse_status: "decoded", + field_type: `${"class:5<".repeat(depth)}i4${">".repeat(depth)}`, }); }); diff --git a/src/dotnet/ManagedMemberInspector.ts b/src/dotnet/ManagedMemberInspector.ts index 3a55932a7f..6c27ac8024 100644 --- a/src/dotnet/ManagedMemberInspector.ts +++ b/src/dotnet/ManagedMemberInspector.ts @@ -186,7 +186,7 @@ export const inspectManagedMembersBytes = ( "Metadata tokens are build-local coordinates and are only meaningful with the reported artifact SHA-256 and MVID.", "CIL instruction anchors are decoded from file-backed method bodies only; no target assembly is loaded or executed.", "Exported types are reported from ExportedType rows as declared; forwarded types are not resolved in their implementing assemblies.", - "Signatures are decoded for common ECMA-335 primitive, class, valuetype, pointer, byref, array, and generic variable forms; unsupported forms retain raw signature hashes.", + "Member signatures decode ECMA-335 primitive, class, valuetype, generic instantiation, generic variable, pointer, byref, single-dimension and general array, function pointer, custom modifier, and typed reference forms; class and valuetype operands keep their encoded TypeDefOrRefOrSpec values, and unsupported forms such as vararg sentinels retain raw signature hashes.", ], }, managedDecodeBudget(layout), diff --git a/src/dotnet/ManagedMemberInspectorCore.ts b/src/dotnet/ManagedMemberInspectorCore.ts index 970ce3d78b..7d54d5f075 100644 --- a/src/dotnet/ManagedMemberInspectorCore.ts +++ b/src/dotnet/ManagedMemberInspectorCore.ts @@ -346,50 +346,297 @@ const ELEMENT_TYPES = new Map([ [0x1c, "object"], ]); -const readTypeSignature = ( +const readSignedCompressed = ( + bytes: Buffer, + offset: number, +): { readonly value: number; readonly next: number } => { + const encoded = readCompressed(bytes, offset); + const width = + encoded.next - offset === 1 ? 7 : encoded.next - offset === 2 ? 14 : 29; + const magnitude = Math.floor(encoded.value / 2); + return { + value: encoded.value % 2 === 0 ? magnitude : magnitude - 2 ** (width - 1), + next: encoded.next, + }; +}; + +const malformedSignature = (detail: string, offset: number): never => { + throw managedFailure("invalid-blob", "signature", detail, offset); +}; + +// Pending work while decoding one Type. Signatures nest arbitrarily deep, so +// decoding keeps an explicit stack instead of recursing. +type TypeContinuation = + | { readonly kind: "suffix"; readonly suffix: "*" | "&" | "[]" } + | { readonly kind: "modifiers"; readonly modifiers: string[] } + | { readonly kind: "array" } + | { + readonly kind: "list"; + remaining: number; + readonly items: string[]; + // Function pointer items are a RetType then Params; generic + // arguments are Types. + readonly signatureItems: boolean; + readonly render: (items: readonly string[]) => string; + }; + +interface MethodSignatureHeader { + readonly first: number; + readonly genericParameterCount: number | null; + readonly parameterCount: number; + readonly next: number; +} + +const readMethodSignatureHeader = ( + blob: Buffer, + offset: number, +): MethodSignatureHeader => { + const first = blob[offset]; + if (first === undefined) + return malformedSignature("truncated method signature", offset); + let next = offset + 1; + let genericParameterCount: number | null = null; + if ((first & 0x10) !== 0) { + const generic = readCompressed(blob, next); + genericParameterCount = generic.value; + next = generic.next; + } + const parameterCount = readCompressed(blob, next); + return { + first, + genericParameterCount, + parameterCount: parameterCount.value, + next: parameterCount.next, + }; +}; + +const MAX_ARRAY_RANK = 32; + +// FNPTR carries a method signature: managed DEFAULT/VARARG, or the unmanaged +// C, STDCALL, THISCALL, FASTCALL, and UNMANAGED conventions that compilers +// emit for unmanaged function pointers. FIELD, LOCAL_SIG, PROPERTY, and +// GENERICINST headers are not method signatures. +const FUNCTION_POINTER_CONVENTIONS = new Set([0, 1, 2, 3, 4, 5, 9]); + +const arrayShape = ( blob: Buffer, offset: number, ): { readonly value: string; readonly next: number } => { - const suffixes: string[] = []; - let cursor = offset; - let kind = blob[cursor]; - while (kind === 0x0f || kind === 0x10 || kind === 0x1d) { - suffixes.push(kind === 0x0f ? "*" : kind === 0x10 ? "&" : "[]"); - cursor += 1; - kind = blob[cursor]; - } - if (kind === undefined) - throw managedFailure( - "invalid-blob", - "signature", - "truncated type signature", - cursor, - ); - const named = ELEMENT_TYPES.get(kind); - let value: string; - let next: number; - if (named !== undefined) { - value = named; - next = cursor + 1; - } else if (kind === 0x11 || kind === 0x12) { - const token = readCompressed(blob, cursor + 1); - value = `${kind === 0x11 ? "valuetype" : "class"}:${String(token.value)}`; - next = token.next; - } else if (kind === 0x1e || kind === 0x13) { - const variable = readCompressed(blob, cursor + 1); - value = `${kind === 0x1e ? "mvar" : "var"}:${String(variable.value)}`; - next = variable.next; - } else { + const rank = readCompressed(blob, offset); + if (rank.value === 0) return malformedSignature("array rank is zero", offset); + // ECMA-335 leaves rank unbounded, but the CLR loads at most 32 dimensions; + // larger ranks would expand a few bytes into an unbounded rendering. + if (rank.value > MAX_ARRAY_RANK) throw managedFailure( "unsupported-signature", "signature", - `unsupported element type 0x${kind.toString(16)}`, - cursor, + `array rank ${String(rank.value)} exceeds the ${String(MAX_ARRAY_RANK)} dimensions the CLR supports`, + offset, ); + const sizeCount = readCompressed(blob, rank.next); + if (sizeCount.value > rank.value) + return malformedSignature("array has more sizes than dimensions", offset); + const sizes: number[] = []; + let next = sizeCount.next; + for (let index = 0; index < sizeCount.value; index += 1) { + const size = readCompressed(blob, next); + sizes.push(size.value); + next = size.next; } - return { value: value + suffixes.reverse().join(""), next }; + const boundCount = readCompressed(blob, next); + if (boundCount.value > rank.value) + return malformedSignature( + "array has more lower bounds than dimensions", + offset, + ); + const lowerBounds: number[] = []; + next = boundCount.next; + for (let index = 0; index < boundCount.value; index += 1) { + const bound = readSignedCompressed(blob, next); + lowerBounds.push(bound.value); + next = bound.next; + } + const dimensions = Array.from({ length: rank.value }, (_, index) => { + const size = sizes[index]; + const lower = lowerBounds[index]; + if (lower !== undefined) + return size === undefined + ? `${String(lower)}...` + : `${String(lower)}...${String(lower + size - 1)}`; + if (size !== undefined) return String(size); + // ILAsm spelling: a bare rank-1 `[]` is reserved for SZARRAY. + return rank.value === 1 ? "..." : ""; + }); + return { value: `[${dimensions.join(",")}]`, next }; }; +/** Grammar position of the element being decoded (ECMA-335 II.23.2). */ +type SignaturePosition = "field" | "return" | "parameter" | "type" | "pointee"; + +// Custom modifiers do not change the position they precede. +const currentPosition = ( + stack: readonly TypeContinuation[], + base: SignaturePosition, +): SignaturePosition => { + const frame = stack.findLast(({ kind }) => kind !== "modifiers"); + if (frame === undefined) return base; + if (frame.kind === "suffix") return frame.suffix === "*" ? "pointee" : "type"; + if (frame.kind === "list" && frame.signatureItems) + return frame.items.length === 0 ? "return" : "parameter"; + return "type"; +}; + +// VOID is a RetType or a pointee; TYPEDBYREF is a RetType or Param; BYREF +// prefixes a RetType, Param, or (since ref fields) a field type. +const POSITION_ELEMENTS: ReadonlyMap = + new Map([ + [0x01, ["return", "pointee"]], + [0x16, ["return", "parameter"]], + [0x10, ["return", "parameter", "field"]], + ]); + +const readTypeSignature = ( + blob: Buffer, + offset: number, + base: SignaturePosition, +): { readonly value: string; readonly next: number } => { + const stack: TypeContinuation[] = []; + let cursor = offset; + for (;;) { + let value: string | undefined; + const kind = blob[cursor]; + if (kind === undefined) + return malformedSignature("truncated type signature", cursor); + const positions = POSITION_ELEMENTS.get(kind); + if ( + positions !== undefined && + !positions.includes(currentPosition(stack, base)) + ) + return malformedSignature( + `element type 0x${kind.toString(16)} is not valid in a ${currentPosition(stack, base)} position`, + cursor, + ); + const named = ELEMENT_TYPES.get(kind); + if (named !== undefined) { + value = named; + cursor += 1; + } else if (kind === 0x0f || kind === 0x10 || kind === 0x1d) { + const suffix = kind === 0x0f ? "*" : kind === 0x10 ? "&" : "[]"; + stack.push({ kind: "suffix", suffix }); + cursor += 1; + } else if (kind === 0x1f || kind === 0x20) { + const token = readCompressed(blob, cursor + 1); + const modifier = `${kind === 0x1f ? "modreq" : "modopt"}:${String(token.value)}`; + // A modifier frame on top was pushed by the directly preceding byte; + // keep an adjacent run together and in source order. + const top = stack.at(-1); + if (top?.kind === "modifiers") top.modifiers.push(modifier); + else stack.push({ kind: "modifiers", modifiers: [modifier] }); + cursor = token.next; + } else if (kind === 0x11 || kind === 0x12) { + const token = readCompressed(blob, cursor + 1); + value = `${kind === 0x11 ? "valuetype" : "class"}:${String(token.value)}`; + cursor = token.next; + } else if (kind === 0x1e || kind === 0x13) { + const variable = readCompressed(blob, cursor + 1); + value = `${kind === 0x1e ? "mvar" : "var"}:${String(variable.value)}`; + cursor = variable.next; + } else if (kind === 0x16) { + value = "typedbyref"; + cursor += 1; + } else if (kind === 0x14) { + stack.push({ kind: "array" }); + cursor += 1; + } else if (kind === 0x15) { + const generic = blob[cursor + 1]; + if (generic !== 0x11 && generic !== 0x12) + return malformedSignature( + "generic instantiation does not name a class or value type", + cursor + 1, + ); + const token = readCompressed(blob, cursor + 2); + const argumentCount = readCompressed(blob, token.next); + if (argumentCount.value === 0) + return malformedSignature( + "generic instantiation has no type arguments", + token.next, + ); + const head = `${generic === 0x11 ? "valuetype" : "class"}:${String(token.value)}`; + stack.push({ + kind: "list", + remaining: argumentCount.value, + items: [], + signatureItems: false, + render: (items) => `${head}<${items.join(",")}>`, + }); + cursor = argumentCount.next; + } else if (kind === 0x1b) { + const header = readMethodSignatureHeader(blob, cursor + 1); + if ( + !FUNCTION_POINTER_CONVENTIONS.has(header.first & 0x0f) || + (header.first & 0x80) !== 0 || + ((header.first & 0x40) !== 0 && (header.first & 0x20) === 0) + ) + return malformedSignature( + `function pointer calling convention 0x${header.first.toString(16)} does not head a method signature`, + cursor + 1, + ); + const convention = callingConvention(header.first); + const head = + header.genericParameterCount === null + ? convention + : `${convention};${String(header.genericParameterCount)}`; + stack.push({ + kind: "list", + remaining: header.parameterCount + 1, + items: [], + signatureItems: true, + render: ([returnType, ...parameters]) => + `fnptr[${head}](${parameters.join(",")})->${returnType ?? ""}`, + }); + cursor = header.next; + } else { + throw managedFailure( + "unsupported-signature", + "signature", + `unsupported element type 0x${kind.toString(16)}`, + cursor, + ); + } + while (value !== undefined) { + const top = stack.pop(); + if (top === undefined) return { value, next: cursor }; + if (top.kind === "suffix") value += top.suffix; + else if (top.kind === "modifiers") + value = `${value} ${top.modifiers.join(" ")}`; + else if (top.kind === "array") { + const shape = arrayShape(blob, cursor); + value += shape.value; + cursor = shape.next; + } else { + top.items.push(value); + top.remaining -= 1; + if (top.remaining === 0) value = top.render(top.items); + else { + value = undefined; + stack.push(top); + } + } + } + } +}; + +const CALLING_CONVENTION_NAMES = new Map([ + [0, "default"], + [1, "c"], + [2, "stdcall"], + [3, "thiscall"], + [4, "fastcall"], + [5, "vararg"], + [6, "field"], + [9, "unmanaged"], +]); + const callingConvention = (value: number): string => { const base = value & 0x0f; const flags = [ @@ -397,14 +644,7 @@ const callingConvention = (value: number): string => { (value & 0x40) === 0 ? null : "explicit-this", (value & 0x10) === 0 ? null : "generic", ].filter((flag) => flag !== null); - const name = - base === 0 - ? "default" - : base === 5 - ? "vararg" - : base === 6 - ? "field" - : `unknown:${String(base)}`; + const name = CALLING_CONVENTION_NAMES.get(base) ?? `unknown:${String(base)}`; return flags.length === 0 ? name : `${name} ${flags.join(" ")}`; }; @@ -421,7 +661,7 @@ export const signature = ( throw managedFailure("invalid-blob", "signature", "empty signature", 0); const first = blob[0] ?? 0; if ((first & 0x0f) === 6) { - const fieldType = readTypeSignature(blob, 1); + const fieldType = readTypeSignature(blob, 1, "field"); return { ...raw, kind: "field", @@ -436,20 +676,12 @@ export const signature = ( fieldType.next === blob.length ? null : "Trailing signature data", }; } - let offset = 1; - let genericParameterCount: number | null = null; - if ((first & 0x10) !== 0) { - const generic = readCompressed(blob, offset); - genericParameterCount = generic.value; - offset = generic.next; - } - const parameterCount = readCompressed(blob, offset); - offset = parameterCount.next; - const returnType = readTypeSignature(blob, offset); - offset = returnType.next; + const header = readMethodSignatureHeader(blob, 0); + const returnType = readTypeSignature(blob, header.next, "return"); + let offset = returnType.next; const parameters: string[] = []; - for (let index = 0; index < parameterCount.value; index += 1) { - const parameter = readTypeSignature(blob, offset); + for (let index = 0; index < header.parameterCount; index += 1) { + const parameter = readTypeSignature(blob, offset, "parameter"); parameters.push(parameter.value); offset = parameter.next; } @@ -458,8 +690,8 @@ export const signature = ( kind: "method", parse_status: offset === blob.length ? "decoded" : "partial", calling_convention: callingConvention(first), - generic_parameter_count: genericParameterCount, - parameter_count: parameterCount.value, + generic_parameter_count: header.genericParameterCount, + parameter_count: header.parameterCount, return_type: returnType.value, parameter_types: parameters, field_type: null, diff --git a/tests/boundary/providers/managed/managedMemberComparisonFromMetadata.test.ts b/tests/boundary/providers/managed/managedMemberComparisonFromMetadata.test.ts index 813889ca69..7849cfb827 100644 --- a/tests/boundary/providers/managed/managedMemberComparisonFromMetadata.test.ts +++ b/tests/boundary/providers/managed/managedMemberComparisonFromMetadata.test.ts @@ -311,7 +311,7 @@ describe("managed member comparison of undecoded signatures", () => { it("pairs undecoded signatures by exact declared type, name, and raw bytes", () => { const undecoded = { methodSignature: Buffer.from([0x00, 0x00, 0x7f]), - fieldSignature: Buffer.from([0x06, 0x15]), + fieldSignature: Buffer.from([0x06, 0x7f]), }; const left = inspect( buildManagedPeFixture(undecoded), @@ -346,7 +346,7 @@ describe("managed member comparison of undecoded signatures", () => { const left = inspect( buildManagedPeFixture({ methodSignature: Buffer.from([0x00, 0x00, 0x7f]), - fieldSignature: Buffer.from([0x06, 0x15]), + fieldSignature: Buffer.from([0x06, 0x7f]), }), "/tmp/undecoded-before.dll", ); @@ -374,7 +374,7 @@ describe("managed member comparison of undecoded signatures", () => { const right = inspect( buildManagedPeFixture({ methodSignature: Buffer.from([0x00, 0x00, 0x7f]), - fieldSignature: Buffer.from([0x06, 0x15]), + fieldSignature: Buffer.from([0x06, 0x7f]), }), "/tmp/undecoded-after.dll", );