Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ deps/*

# Claude Code local settings
.claude/
.claude-recovery.md
.workflow-state.json

# Serena MCP cache
.serena/
Expand Down
6 changes: 4 additions & 2 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## In Progress

_None_
- [ ] 🟡 [Refactor] REFACTOR-BIOME-2026-04-29 (branch `fix/biome-warnings-sweep`, 8 commits) — 63→1 biome warnings (-98.4%). Phases 1-5b-2 all green: pre-push opus senior-review pending then push + PR + Copilot. Started 2026-04-29.

## Pending - HIGH

Expand All @@ -14,7 +14,9 @@ _None._

## Pending - LOW (Nice to Have)

- [ ] [Lint] Biome warnings sweep (6 total, all `pnpm exec biome lint` exits 0 — warnings only, ran via `rtk proxy biome lint` workaround on 2026-04-28). Two from PR #111: `test/wasm/decompress-memlimit.test.ts:30` (`useTemplate` — string concat → template literal, biome FIXABLE) + `test/wasm/decompress-memlimit.test.ts:141` (`noNonNullAssertion` on `result!` in callback success test, replace with assertion guard). Four pre-existing on master: `src/errors.ts:176` `noNonNullAssertion` (`messages[errno]!`), `src/lzma.ts:63` + `src/pool.ts:20` `noImportCycles` (lzma↔pool re-export cycle, already noted in MEMORY.md as "benign — ESM resolves at runtime"), `src/pool.ts:166` `noNonNullAssertion` (`this.queue.shift()!` after empty-check). Priority: L (cosmetic; can batch with another pass). Note: lint pipeline is silently broken until RTK biome bug fixes (workaround: `rtk proxy biome ...`).
- [ ] [Lint] Single residual biome warning: `test/node-api.spec.ts:249` (`suppressions/unused` — pre-existing biome-ignore that no longer suppresses anything). Cosmetic 1-line cleanup for a future PR.
<!-- F-002 (HARDLINK + undefined linkname → TypeError) DROPPED 2026-04-29 by Copilot round-2 review on PR #115: TarEntry.linkname is typed as required string (parser returns '' for empty fields), and ensureSafeLinkname → ensureSafeName already rejects '' with "empty linkname" before reaching resolve(). The original concern was mischaracterized — there is no path where resolve(cwd, undefined) gets called with undefined. -->


## Completed

Expand Down
8 changes: 6 additions & 2 deletions docs/plans/WIN32-TOCTOU-2026-04-29.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,17 @@ doc-meta:
## §1 Scope

**What changes:** Replace the by-path Win32 fallback in
`packages/tar-xz/src/node/file.ts:328-346` with a fail-closed
`packages/tar-xz/src/node/file.ts` (originally lines 328-346 at PR #114
merge `b24040d`; post-REFACTOR-BIOME-2026-04-29 the fail-closed path
lives in private helpers `openFileExclusive` and `writeFileEntryWin32`,
delegated by `writeFileEntry` from `extractFile`) with a fail-closed
JS-pure handle-based path that uses Node's `'wx'` flag (atomic
exclusive create) plus an unlink-then-retry pattern for legitimate
overwrite. fd-based `chmod` and `utimes` replace by-path equivalents.

**What does NOT change:**
- POSIX path (lines 292-327) — unchanged. Already uses `O_NOFOLLOW`.
- POSIX path (originally lines 292-327; now in `writeFileEntryPosix`)
— unchanged. Already uses `O_NOFOLLOW`.
- Leaf-symlink lstat check upstream — unchanged. Still rejects
pre-existing symlink targets before this code runs.
- Public API of `extract()` / `extractFile()` — unchanged.
Expand Down
4 changes: 3 additions & 1 deletion examples/browser/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import {

// --- Logging helpers ---

const logEl = document.getElementById('log')!;
const logElRaw = document.getElementById('log');
if (!logElRaw) throw new Error('Missing required DOM element: #log');
const logEl = logElRaw;

function log(msg: string, cls: string = '') {
const span = document.createElement('span');
Expand Down
73 changes: 57 additions & 16 deletions packages/nxz/src/nxz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ function parseCliArgs(args: string[]): CliOptions {
for (const arg of args) {
const presetMatch = arg.match(/^-(\d)$/);
if (presetMatch) {
presetLevel = Number.parseInt(presetMatch[1]!, 10);
const digit = presetMatch[1];
presetLevel = digit !== undefined ? Number.parseInt(digit, 10) : 6;
} else {
filteredArgs.push(arg);
}
Expand Down Expand Up @@ -707,14 +708,19 @@ async function listTarFile(filename: string, options: CliOptions): Promise<numbe
*/
function findCommonParent(paths: string[]): string {
if (paths.length === 0) return process.cwd();
if (paths.length === 1) return paths[0]!;
if (paths.length === 1) {
const p = paths[0];
if (p === undefined) return process.cwd();
return p;
}
const parts = paths.map((p) => p.split('/'));
const common: string[] = [];
const first = parts[0]!;
const first = parts[0];
if (first === undefined) return process.cwd();
for (let i = 0; i < first.length; i++) {
const segment = first[i];
if (parts.every((p) => p[i] === segment)) {
common.push(segment!);
if (segment !== undefined && parts.every((p) => p[i] === segment)) {
common.push(segment);
} else {
break;
}
Expand All @@ -733,7 +739,17 @@ function resolveTarOutput(
): string | null {
let outputFile = options.output;
if (!outputFile) {
const base = pathModule.basename(files[0]!).replace(/\/$/, '');
// Invariant violation must fail-fast: an empty `files` array would
// otherwise silently produce an output named `.tar.xz` (just the
// suffix). Throw with a descriptive message so callers learn the
// contract instead of inheriting a degenerate output name.
const firstFile = files[0];
if (firstFile === undefined) {
throw new Error(
'resolveTarOutput requires at least one input file when no output path is provided'
);
}
const base = pathModule.basename(firstFile).replace(/\/$/, '');
outputFile = `${base}.tar.xz`;
}

Expand All @@ -752,16 +768,37 @@ function resolveArchiveCwd(
resolvedFiles: string[],
pathModule: typeof import('node:path')
): string {
if (resolvedFiles.length === 1 && statSync(resolvedFiles[0]!).isDirectory()) {
return resolvedFiles[0]!;
const firstFile = resolvedFiles[0];
if (resolvedFiles.length === 1 && firstFile !== undefined && statSync(firstFile).isDirectory()) {
return firstFile;
}
const parents = resolvedFiles.map((f) => (statSync(f).isDirectory() ? f : pathModule.dirname(f)));
return parents.length === 1 ? parents[0]! : findCommonParent(parents);
if (parents.length === 1) {
const parent = parents[0];
return parent !== undefined ? parent : findCommonParent(resolvedFiles);
}
return findCommonParent(parents);
}

/**
* Collect all files to include in a tar archive, relative to cwd.
*/

/**
* Build the relative archive path for a directory entry.
*/
function buildEntryPath(
entry: { parentPath: string; name: string },
dirAbsPath: string,
dirRelative: string
): string {
const entryPath =
entry.parentPath === dirAbsPath
? entry.name
: `${entry.parentPath.slice(dirAbsPath.length + 1)}/${entry.name}`;
return dirRelative ? `${dirRelative}/${entryPath}` : entryPath;
}

async function collectArchiveFiles(
resolvedFiles: string[],
cwd: string,
Expand All @@ -775,11 +812,7 @@ async function collectArchiveFiles(
const dirRelative = pathModule.relative(cwd, file);
for (const entry of entries) {
if (entry.isFile()) {
const entryPath =
entry.parentPath === file
? entry.name
: `${entry.parentPath.slice(file.length + 1)}/${entry.name}`;
filesToArchive.push(dirRelative ? `${dirRelative}/${entryPath}` : entryPath);
filesToArchive.push(buildEntryPath(entry, file, dirRelative));
}
}
} else {
Expand Down Expand Up @@ -991,8 +1024,16 @@ async function main(): Promise<void> {
process.exit(exitCode);
}

// Check for tar-create mode: -T with files that aren't .tar.xz archives
const mode = determineMode(options, options.files[0]!);
// Check for tar-create mode: -T with files that aren't .tar.xz archives.
// The stdin check above already exits when files.length === 0, so reaching
// here means files[0] is defined. Fail-fast on a future invariant breach
// (e.g. argument-parsing changes) instead of silently dispatching with an
// empty filename.
const firstFile = options.files[0];
if (firstFile === undefined) {
throw new Error('Internal error: expected at least one input file after stdin handling');
}
const mode = determineMode(options, firstFile);
if (mode === 'tar-create') {
const exitCode = await createTarFile(options.files, options);
process.exit(exitCode);
Expand Down
40 changes: 32 additions & 8 deletions packages/tar-xz/src/browser/extract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,35 @@ import {
/**
* Parse TAR data into entries
*/

/**
* Handle a PAX_HEADER block: parse its attributes and advance the offset.
* Returns the updated offset and the parsed PAX attributes.
*/
function parsePaxHeaderBlock(
data: Uint8Array,
offset: number,
size: number
): { offset: number; paxAttrs: PaxAttributes } {
const paxData = data.subarray(offset, offset + size);
const newOffset = offset + size + calculatePadding(size);
return { offset: newOffset, paxAttrs: parsePaxData(paxData) };
}

/**
* Extract entry content bytes and advance the offset past the content + padding.
* Returns the updated offset and the content Uint8Array.
*/
function extractEntryContent(
data: Uint8Array,
offset: number,
size: number
): { offset: number; contentData: Uint8Array } {
const contentData = size > 0 ? data.subarray(offset, offset + size) : new Uint8Array(0);
const newOffset = offset + size + calculatePadding(size);
return { offset: newOffset, contentData };
}

function parseTar(data: Uint8Array): Array<TarEntry & { data: Uint8Array }> {
const entries: Array<TarEntry & { data: Uint8Array }> = [];
let offset = 0;
Expand Down Expand Up @@ -54,10 +83,7 @@ function parseTar(data: Uint8Array): Array<TarEntry & { data: Uint8Array }> {

// Handle PAX headers
if (entry.type === TarEntryType.PAX_HEADER) {
const paxSize = entry.size;
const paxData = data.subarray(offset, offset + paxSize);
offset += paxSize + calculatePadding(paxSize);
paxAttrs = parsePaxData(paxData);
({ offset, paxAttrs } = parsePaxHeaderBlock(data, offset, entry.size));
continue;
}

Expand All @@ -73,10 +99,8 @@ function parseTar(data: Uint8Array): Array<TarEntry & { data: Uint8Array }> {
}

// Extract content
const contentData =
entry.size > 0 ? data.subarray(offset, offset + entry.size) : new Uint8Array(0);

offset += entry.size + calculatePadding(entry.size);
let contentData: Uint8Array;
({ offset, contentData } = extractEntryContent(data, offset, entry.size));

entries.push({ ...entry, data: contentData });
}
Expand Down
Loading
Loading