diff --git a/src/native/parsers/otool.ts b/src/native/parsers/otool.ts index 8fd62a06b3..b504c6390a 100644 --- a/src/native/parsers/otool.ts +++ b/src/native/parsers/otool.ts @@ -308,7 +308,7 @@ const parseLoadCommand = (block: string, state: ParsedLoadCommands): void => { fields, }); collectEntrypoint(kind, fields, state); - collectBuild(kind, block, fields, state); + collectBuild(kind, parsed, state); collectDependency(kind, fields, state); if (PAYLOAD_DEPENDENCY_COMMANDS.has(kind)) state.omittedDependencyCommands.push(kind); @@ -387,12 +387,12 @@ const collectEntrypoint = ( const collectBuild = ( kind: string, - block: string, - fields: Readonly>, + parsed: ParsedFields, state: ParsedLoadCommands, ): void => { + const fields = parsed.values; if (kind === "LC_BUILD_VERSION") { - const tools = parseBuildTools(block); + const tools = parseBuildTools(parsed.rows); if (numberField(fields, "ntools") !== tools.length) throw new TypeError("otool build-tool count does not match ntools"); state.builds.push({ @@ -517,8 +517,6 @@ const parseField = (rawLine: string): readonly [string, string] | undefined => { // exactly, including surrounding spaces, under otool's ordinal label. const option = /^\s*(string #[1-9]\d*)(?: (.*))?$/u.exec(rawLine); if (option?.[1] !== undefined) return [option[1], option[2] ?? ""]; - const fixedName = /^\s*(segname|sectname) (.*)$/u.exec(rawLine); - if (fixedName?.[1] !== undefined) return [fixedName[1], fixedName[2] ?? ""]; const line = rawLine.trim(); // These are the compound field labels used by otool. Keeping this list // explicit prevents a one-word field value such as `sectname version` from @@ -586,16 +584,23 @@ const parseSections = (block: string): ParsedSection[] => }; }); +/** Project tool/version pairs from the same ordered facts as command fields. */ const parseBuildTools = ( - block: string, + rows: ParsedFields["rows"], ): Array<{ name: string; version: string }> => { const tools: Array<{ name: string; version: string }> = []; - const lines = block.split(/\r?\n/u).map((line) => line.trim()); - for (let index = 0; index < lines.length - 1; index += 1) { - const tool = /^tool\s+(.+)$/u.exec(lines[index] ?? "")?.[1]; - const version = /^version\s+(.+)$/u.exec(lines[index + 1] ?? "")?.[1]; - if (tool !== undefined && version !== undefined) - tools.push({ name: BUILD_TOOL_NAMES.get(tool) ?? tool, version }); + for (let index = 0; index < rows.length; index++) { + const field = rows[index]; + if (field?.[0] === "version") + throw new TypeError("otool build-tool version has no preceding tool"); + if (field?.[0] !== "tool") continue; + const version = rows[++index]; + if (field[1] === "" || version?.[0] !== "version" || version[1] === "") + throw new TypeError("otool build tool is missing its name or version"); + tools.push({ + name: BUILD_TOOL_NAMES.get(field[1]) ?? field[1], + version: version[1], + }); } return tools; }; diff --git a/tests/boundary/providers/native/nativeLoadKinds.golden.test.ts b/tests/boundary/providers/native/nativeLoadKinds.golden.test.ts index da2dfd2829..6379a914bf 100644 --- a/tests/boundary/providers/native/nativeLoadKinds.golden.test.ts +++ b/tests/boundary/providers/native/nativeLoadKinds.golden.test.ts @@ -595,3 +595,61 @@ it.each([ ); expect(load.builds[0]?.tools).toEqual([{ name, version: "1.0" }]); }); + +it("projects build tools from the retained ordered field facts", () => { + const load = parseOtoolLoadCommands( + withOtoolMachHeader( + [ + "Load command 0", + "cmd LC_BUILD_VERSION", + "cmdsize 40", + "platform 1", + "minos 1.0", + "sdk 1.0", + "ntools 2", + "tool 1", + "", + "version 001", + "tool 99", + "version 0x10", + "", + ].join("\n"), + ), + ); + expect(load.builds[0]?.tools).toEqual([ + { name: "CLANG", version: "001" }, + { name: "99", version: "0x10" }, + ]); + expect(load.commands[0]?.fields).toMatchObject({ + "tool #1": 1, + "tool #2": 99, + "version #1": 1, + "version #2": 16, + }); +}); + +it.each([ + ["version 9.0", "tool 1", "version 1.0"], + ["tool 1", "version 1.0", "tool 2"], + ["tool 1", "version 1.0", "version 2.0"], + ["tool 1", "unexpected 2", "version 1.0"], + ["tool 1", "version"], +])("rejects unpaired build tool/version facts: %j", (...toolRows) => { + expect(() => + parseOtoolLoadCommands( + withOtoolMachHeader( + [ + "Load command 0", + "cmd LC_BUILD_VERSION", + "cmdsize 32", + "platform 1", + "minos 1.0", + "sdk 1.0", + "ntools 1", + ...toolRows, + "", + ].join("\n"), + ), + ), + ).toThrow(/otool build.tool/u); +});