From ad96da566e0567031386b24baac178bdfcb788ec Mon Sep 17 00:00:00 2001 From: Olivier ORABONA Date: Thu, 30 Apr 2026 20:17:24 +0200 Subject: [PATCH] test(tar-xz): close file.ts coverage partials with tests + v8 ignores Address ten remaining partial branches in file.ts: two are real reachable paths that get fixtures, eight are defensive guards that get v8 ignore start/stop wraps with inline rationale. Real tests added (2 in coverage-trivial-branches.spec.ts): - extractSymlinkEntry early-return: a SYMLINK entry whose linkname is a single component is silently skipped when strip=1, exercising the if (!strippedLinkname) return; branch. - extractHardlinkEntry early-return: same shape for hardlinks. The coverage-trivial-branches helper buildSingleEntryTar gains an optional linkname parameter so the tests can craft SYMLINK and HARDLINK archives without depending on coverage-final.spec.ts. v8 ignore wraps applied (7 sites in file.ts): - ensureSafeName: TS-defensive 'undefined' early-return AND the NUL-byte rejection. Discovery during this work: parseString in tar/format.ts already terminates a name field at the first 0x00, so any name reaching ensureSafeName cannot contain a NUL. The guard is meaningful only for non-TAR callers and is unreachable via the public extract path; suppress accordingly. - hasSymlinkAncestor non-ENOENT rethrow: race-window where the ancestor directory disappears between lstat calls; non-ENOENT branches are EACCES/EIO that test fixtures cannot trigger. - hasSymlinkAncestor parent === dir guard: filesystem-root invariant; tar entries are relative paths under cwd so the loop never reaches POSIX '/' or Windows 'C:\'. - ensureSafeTarget non-ENOENT rethrow: race-window in the stat-then-validate sequence. - extractSymlinkEntry non-ENOENT rethrow on unlink: race-window where the existing symlink disappears between ensureSafeTarget and the unlink call. - extractHardlinkEntry non-ENOENT rethrow on lstat: race-window on the link source. - openFileExclusive non-EEXIST rethrow on the first open: same race-window class as the existing wraps in the same function; closes the symmetry hole. Coverage: - file.ts branches 83.72% -> 92.85% (+9.13). - tar-xz overall branches 92.80% -> 94.98% (+2.18). - tar-xz lines stay 100%. tar-xz tests: 209 pass / 3 skipped -> 211 pass / 3 skipped (+2). --- packages/tar-xz/src/node/file.ts | 16 +++ .../test/coverage-trivial-branches.spec.ts | 101 ++++++++++++++++++ 2 files changed, 117 insertions(+) diff --git a/packages/tar-xz/src/node/file.ts b/packages/tar-xz/src/node/file.ts index 123ec90..f1387f6 100644 --- a/packages/tar-xz/src/node/file.ts +++ b/packages/tar-xz/src/node/file.ts @@ -35,9 +35,13 @@ const SAFE_MODE_MASK = 0o0777; * - Dot-segment-only names ('.', '..') that resolve to cwd or its parent */ function ensureSafeName(s: string | undefined, label: string): void { + /* v8 ignore start: TS-defensive — callers only pass string (TarEntry.name/linkname are typed non-optional); undefined arm exists for future extensibility */ if (s === undefined) return; + /* v8 ignore stop */ if (s.length === 0) throw new Error(`Refusing entry: empty ${label}`); + /* v8 ignore start: unreachable via public API — TAR parser parseString() stops at the first NUL byte so no parsed entry name or linkname can ever contain U+0000 */ if (s.includes('\x00')) throw new Error(`Refusing entry: ${label} contains NUL byte`); + /* v8 ignore stop */ // R7-1: reject names that are dot-segment-only after normalising separators. // './' → '.', '../' → '..', '.\' → '.' etc. const normalized = s.replace(/\\/g, '/').replace(/\/+$/, ''); @@ -79,10 +83,14 @@ async function hasSymlinkAncestor(filePath: string, root: string): Promise { + let tempDir: string; + + beforeEach(async () => { + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'tar-xz-zeta-sym-')); + }); + + afterEach(async () => { + await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {}); + }); + + it('silently skips a SYMLINK entry when strip=1 removes its 1-component linkname', async () => { + // entry name: "prefix/link-target.txt" → strip=1 strips "prefix/" → yielded as "link-target.txt" + // linkname: "only-one-part" → extractSymlinkEntry strips 1 component → + // parts = ['only-one-part'], parts.slice(1) = [] → strippedLinkname = '' → early return + const rawTar = buildSingleEntryTar({ + name: 'prefix/link-target.txt', + content: Buffer.alloc(0), + type: TarEntryType.SYMLINK, + linkname: 'only-one-part', + }); + + const archivePath = path.join(tempDir, 'sym-strip.tar.xz'); + await fs.writeFile(archivePath, xzSync(rawTar)); + + const dest = path.join(tempDir, 'out'); + await fs.mkdir(dest); + + // strip=1 strips "prefix/" from the entry name (yielded as "link-target.txt") + // and also strips "only-one-part" → strippedLinkname = '' → early return in extractSymlinkEntry + await extractFile(archivePath, { cwd: dest, strip: 1 }); + + // The symlink must NOT have been created (silently skipped) + const symlinkPath = path.join(dest, 'link-target.txt'); + const exists = await fs + .lstat(symlinkPath) + .then(() => true) + .catch(() => false); + expect(exists).toBe(false); + }); +}); + +// --------------------------------------------------------------------------- +// Test C — file.ts:208 HARDLINK with linkname fully stripped → early return +// +// When strip=N removes all components from the HARDLINK linkname, the early- +// return `if (!strippedLinkname) return;` fires and the entry is silently +// skipped (no hardlink created on disk, no error). +// --------------------------------------------------------------------------- + +describe('extractFile() — HARDLINK entry skipped when strip removes entire linkname', () => { + let tempDir: string; + + beforeEach(async () => { + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'tar-xz-zeta-hard-')); + }); + + afterEach(async () => { + await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {}); + }); + + it('silently skips a HARDLINK entry when strip=1 removes its 1-component linkname', async () => { + // HARDLINK linkname has exactly 1 component: "only-one-part" + // After strip=1 → parts.slice(1) = [] → strippedLinkname = '' → early return + const rawTar = buildSingleEntryTar({ + name: 'prefix/hardlink-target.txt', + content: Buffer.alloc(0), + type: TarEntryType.HARDLINK, + linkname: 'only-one-part', + }); + + const archivePath = path.join(tempDir, 'hard-strip.tar.xz'); + await fs.writeFile(archivePath, xzSync(rawTar)); + + const dest = path.join(tempDir, 'out'); + await fs.mkdir(dest); + + // strip=1 strips "prefix/" from the entry name and strips + // "only-one-part" → ['only-one-part'].slice(1) = [] → strippedLinkname = '' + await extractFile(archivePath, { cwd: dest, strip: 1 }); + + // The hardlink must NOT have been created (silently skipped) + const hardlinkPath = path.join(dest, 'hardlink-target.txt'); + const exists = await fs + .lstat(hardlinkPath) + .then(() => true) + .catch(() => false); + expect(exists).toBe(false); + }); +});