Skip to content

feat: add CLI for extracting ELF build ID#3

Open
bobbyg603 wants to merge 1 commit intomainfrom
bobby/cli-support
Open

feat: add CLI for extracting ELF build ID#3
bobbyg603 wants to merge 1 commit intomainfrom
bobby/cli-support

Conversation

@bobbyg603
Copy link
Copy Markdown
Member

Description

Adds a elfy CLI command that reads an ELF file and prints its .note.gnu.build-id section as a hex UUID string.

Checklist

  • Tested manually
  • Unit tests pass with no errors or warnings
  • Documentation updated (if applicable)
  • Reviewed by at least 1 other contributor

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>
Copilot AI review requested due to automatic review settings April 9, 2026 15:32
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.ts entrypoint that opens an ELF file and prints the extracted build-id.
  • Exposed the CLI via package.json bin mapping (elfy -> dist/cli.js).
  • Updated tsconfig.json to compile cli.ts into dist/.

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.

Comment on lines +13 to +14
const fileHandle = await open(filePath, 'r');
try {
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +7 to +37
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();
}
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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();

Copilot uses AI. Check for mistakes.
Comment on lines +30 to +32
const uuid = Array.from(section.slice(16, 36))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +30 to +34
const uuid = Array.from(section.slice(16, 36))
.map(b => b.toString(16).padStart(2, '0'))
.join('');

console.log(uuid);
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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);

Copilot uses AI. Check for mistakes.
Comment on lines +7 to +37
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();
}
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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);
}

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants