Conversation
Adds a `elfy` CLI command that reads an ELF file and prints its `.note.gnu.build-id` section as a hex UUID string. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a Node.js CLI (elfy) to extract and print the .note.gnu.build-id section from an ELF file, integrating it into the package build/publish output.
Changes:
- Added a new
cli.tsentrypoint that opens an ELF file and prints the extracted build-id. - Exposed the CLI via
package.jsonbinmapping (elfy -> dist/cli.js). - Updated
tsconfig.jsonto compilecli.tsintodist/.
Reviewed changes
Copilot reviewed 2 out of 3 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| tsconfig.json | Includes cli.ts so it’s emitted to dist/ during tsc builds. |
| package.json | Adds bin entry for installing/running the elfy CLI. |
| cli.ts | Implements the CLI: file open, section read, and hex formatting of the build-id. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const fileHandle = await open(filePath, 'r'); | ||
| try { |
There was a problem hiding this comment.
open(filePath, 'r') is outside any error handling, so a missing/unreadable file will throw an unhandled rejection and print a stack trace. Catch the error and print a concise message (e.g., include err.message) with a non-zero exit code.
| const filePath = process.argv[2]; | ||
| if (!filePath) { | ||
| console.error('Usage: elfy <path-to-elf-file>'); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| const fileHandle = await open(filePath, 'r'); | ||
| try { | ||
| const dataSource: DataSource = { | ||
| async read(offset: number, length: number): Promise<Uint8Array> { | ||
| const buffer = new Uint8Array(length); | ||
| const { bytesRead } = await fileHandle.read(buffer, 0, length, offset); | ||
| return buffer.slice(0, bytesRead); | ||
| }, | ||
| }; | ||
|
|
||
| const elf = new ElfFile(dataSource); | ||
| const { success, section } = await elf.tryReadSection('.note.gnu.build-id'); | ||
| if (!success || !section) { | ||
| console.error('Could not find .note.gnu.build-id section'); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| const uuid = Array.from(section.slice(16, 36)) | ||
| .map(b => b.toString(16).padStart(2, '0')) | ||
| .join(''); | ||
|
|
||
| console.log(uuid); | ||
| } finally { | ||
| await fileHandle.close(); | ||
| } |
There was a problem hiding this comment.
Avoid calling process.exit(1) inside this try block: it can terminate the process before the finally runs, so the file handle may not be closed. Prefer setting process.exitCode = 1 and returning from a main() function (or otherwise ensuring cleanup happens before exiting).
| const filePath = process.argv[2]; | |
| if (!filePath) { | |
| console.error('Usage: elfy <path-to-elf-file>'); | |
| process.exit(1); | |
| } | |
| const fileHandle = await open(filePath, 'r'); | |
| try { | |
| const dataSource: DataSource = { | |
| async read(offset: number, length: number): Promise<Uint8Array> { | |
| const buffer = new Uint8Array(length); | |
| const { bytesRead } = await fileHandle.read(buffer, 0, length, offset); | |
| return buffer.slice(0, bytesRead); | |
| }, | |
| }; | |
| const elf = new ElfFile(dataSource); | |
| const { success, section } = await elf.tryReadSection('.note.gnu.build-id'); | |
| if (!success || !section) { | |
| console.error('Could not find .note.gnu.build-id section'); | |
| process.exit(1); | |
| } | |
| const uuid = Array.from(section.slice(16, 36)) | |
| .map(b => b.toString(16).padStart(2, '0')) | |
| .join(''); | |
| console.log(uuid); | |
| } finally { | |
| await fileHandle.close(); | |
| } | |
| async function main(): Promise<void> { | |
| const filePath = process.argv[2]; | |
| if (!filePath) { | |
| console.error('Usage: elfy <path-to-elf-file>'); | |
| process.exitCode = 1; | |
| return; | |
| } | |
| const fileHandle = await open(filePath, 'r'); | |
| try { | |
| const dataSource: DataSource = { | |
| async read(offset: number, length: number): Promise<Uint8Array> { | |
| const buffer = new Uint8Array(length); | |
| const { bytesRead } = await fileHandle.read(buffer, 0, length, offset); | |
| return buffer.slice(0, bytesRead); | |
| }, | |
| }; | |
| const elf = new ElfFile(dataSource); | |
| const { success, section } = await elf.tryReadSection('.note.gnu.build-id'); | |
| if (!success || !section) { | |
| console.error('Could not find .note.gnu.build-id section'); | |
| process.exitCode = 1; | |
| return; | |
| } | |
| const uuid = Array.from(section.slice(16, 36)) | |
| .map(b => b.toString(16).padStart(2, '0')) | |
| .join(''); | |
| console.log(uuid); | |
| } finally { | |
| await fileHandle.close(); | |
| } | |
| } | |
| await main(); |
| const uuid = Array.from(section.slice(16, 36)) | ||
| .map(b => b.toString(16).padStart(2, '0')) | ||
| .join(''); |
There was a problem hiding this comment.
The build-id extraction is hard-coded to section.slice(16, 36) (20 bytes). ELF build IDs can have different descriptor sizes, so this can truncate or misread IDs. Parse the note header to determine the descriptor offset/length (or at least derive the end from the note’s descsz) instead of assuming 20 bytes.
| const uuid = Array.from(section.slice(16, 36)) | ||
| .map(b => b.toString(16).padStart(2, '0')) | ||
| .join(''); | ||
|
|
||
| console.log(uuid); |
There was a problem hiding this comment.
The variable name uuid is misleading here: the GNU build-id is not necessarily a UUID (often it’s a SHA-1-like hex string). Consider renaming to something like buildIdHex/buildId to match what is printed.
| const uuid = Array.from(section.slice(16, 36)) | |
| .map(b => b.toString(16).padStart(2, '0')) | |
| .join(''); | |
| console.log(uuid); | |
| const buildId = Array.from(section.slice(16, 36)) | |
| .map(b => b.toString(16).padStart(2, '0')) | |
| .join(''); | |
| console.log(buildId); |
| const filePath = process.argv[2]; | ||
| if (!filePath) { | ||
| console.error('Usage: elfy <path-to-elf-file>'); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| const fileHandle = await open(filePath, 'r'); | ||
| try { | ||
| const dataSource: DataSource = { | ||
| async read(offset: number, length: number): Promise<Uint8Array> { | ||
| const buffer = new Uint8Array(length); | ||
| const { bytesRead } = await fileHandle.read(buffer, 0, length, offset); | ||
| return buffer.slice(0, bytesRead); | ||
| }, | ||
| }; | ||
|
|
||
| const elf = new ElfFile(dataSource); | ||
| const { success, section } = await elf.tryReadSection('.note.gnu.build-id'); | ||
| if (!success || !section) { | ||
| console.error('Could not find .note.gnu.build-id section'); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| const uuid = Array.from(section.slice(16, 36)) | ||
| .map(b => b.toString(16).padStart(2, '0')) | ||
| .join(''); | ||
|
|
||
| console.log(uuid); | ||
| } finally { | ||
| await fileHandle.close(); | ||
| } |
There was a problem hiding this comment.
This PR introduces a new user-facing CLI entrypoint, but there are no automated tests covering CLI argument handling and output (e.g., missing path, missing section, successful extraction). Consider adding a vitest spec that runs the CLI (or refactors the logic into a testable function) to prevent regressions.
| const filePath = process.argv[2]; | |
| if (!filePath) { | |
| console.error('Usage: elfy <path-to-elf-file>'); | |
| process.exit(1); | |
| } | |
| const fileHandle = await open(filePath, 'r'); | |
| try { | |
| const dataSource: DataSource = { | |
| async read(offset: number, length: number): Promise<Uint8Array> { | |
| const buffer = new Uint8Array(length); | |
| const { bytesRead } = await fileHandle.read(buffer, 0, length, offset); | |
| return buffer.slice(0, bytesRead); | |
| }, | |
| }; | |
| const elf = new ElfFile(dataSource); | |
| const { success, section } = await elf.tryReadSection('.note.gnu.build-id'); | |
| if (!success || !section) { | |
| console.error('Could not find .note.gnu.build-id section'); | |
| process.exit(1); | |
| } | |
| const uuid = Array.from(section.slice(16, 36)) | |
| .map(b => b.toString(16).padStart(2, '0')) | |
| .join(''); | |
| console.log(uuid); | |
| } finally { | |
| await fileHandle.close(); | |
| } | |
| type CliIo = { | |
| stdout?: (message: string) => void; | |
| stderr?: (message: string) => void; | |
| }; | |
| export async function runCli(args: string[], io: CliIo = {}): Promise<number> { | |
| const stdout = io.stdout ?? console.log; | |
| const stderr = io.stderr ?? console.error; | |
| const filePath = args[0]; | |
| if (!filePath) { | |
| stderr('Usage: elfy <path-to-elf-file>'); | |
| return 1; | |
| } | |
| const fileHandle = await open(filePath, 'r'); | |
| try { | |
| const dataSource: DataSource = { | |
| async read(offset: number, length: number): Promise<Uint8Array> { | |
| const buffer = new Uint8Array(length); | |
| const { bytesRead } = await fileHandle.read(buffer, 0, length, offset); | |
| return buffer.slice(0, bytesRead); | |
| }, | |
| }; | |
| const elf = new ElfFile(dataSource); | |
| const { success, section } = await elf.tryReadSection('.note.gnu.build-id'); | |
| if (!success || !section) { | |
| stderr('Could not find .note.gnu.build-id section'); | |
| return 1; | |
| } | |
| const uuid = Array.from(section.slice(16, 36)) | |
| .map(b => b.toString(16).padStart(2, '0')) | |
| .join(''); | |
| stdout(uuid); | |
| return 0; | |
| } finally { | |
| await fileHandle.close(); | |
| } | |
| } | |
| const exitCode = await runCli(process.argv.slice(2)); | |
| if (exitCode !== 0) { | |
| process.exit(exitCode); | |
| } |
Description
Adds a
elfyCLI command that reads an ELF file and prints its.note.gnu.build-idsection as a hex UUID string.Checklist