Skip to content

fix(cache): never produce PAX / AppleDouble entries in cache tars - #99

Merged
Exelord merged 1 commit into
mainfrom
claude/filter-pax-applefiles
May 17, 2026
Merged

fix(cache): never produce PAX / AppleDouble entries in cache tars#99
Exelord merged 1 commit into
mainfrom
claude/filter-pax-applefiles

Conversation

@Exelord

@Exelord Exelord commented May 17, 2026

Copy link
Copy Markdown
Member

Bug

Cache restores were landing files like outputs/._dist and PaxHeaders/<name> alongside real outputs in the project tree. Two root causes:

  1. BSD tar (macOS default) emits PAX extended-header records (typeflag x / g) per entry by default — for xattrs, mtime-nanos, SCHILY metadata. They look like regular entries to a naïve parser.
  2. AppleDouble files (._<name>) are macOS resource-fork siblings that Finder / cp -p leave around. If they happen to match a project's output glob, they get staged and ride into the tar.

Per the user's direction ("just never create them"), the fix is at the source — keep the input-side ALWAYS_IGNORE filter list unchanged.

Two-layer fix

1. At save (src/cache/cache.ts:save())

- ['tar', '-cf', '-', '-C', stage, ...topLevel]
+ ['tar', '--format=ustar', '-cf', '-', '-C', stage, ...topLevel]
  // + env: { ...process.env, COPYFILE_DISABLE: '1' }

--format=ustar forces strict POSIX ustar; BSD tar then emits no PAX records. GNU tar accepts the flag too (no-op since GNU format already avoids PAX). COPYFILE_DISABLE=1 blocks Apple's copyfile() from attaching xattrs to staged children — tar has nothing to AppleDouble for.

2. At parse (src/cache/tar.ts:parseTarHeaders) — defense in depth

+ if (typeFlag === 0x78 || typeFlag === 0x67 || typeFlag === 0x58) {
+   // PAX 'x' / 'g' / Solaris 'X' — skip
+   continue
+ }
+ if (basename.startsWith('._')) {
+   // AppleDouble — skip
+   continue
+ }

This protects against:

  • Cache entries created before this PR that already contain junk records.
  • Future tar-binary quirks we haven't seen yet.

Tests

Two new tests in tests/cache-baseline.test.ts construct synthetic tars byte-by-byte (no shell-out — independent of which tar binary the host has) with PAX + AppleDouble records, assert the parser returns only the real file entries.

Asserts
skips PAX extended-header records (typeflag x), AppleDouble entries (._*), and keeps real files parser yields outputs/main.js only — PAX header + ._main.js filtered
skips global PAX records (typeflag g) too global PAX skipped, real file passes

Compatibility

No CACHE_VERSION bump needed. Old contaminated entries in <cacheDir>/<hash>.tar.zst will:

  1. On next restore — parser skips junk, only real files extracted (clean tree)
  2. But output_files rows from the old save still reference ._dist/... paths → isOutputsCurrent mismatches → triggers full restore (which is now clean)
  3. Subsequent saves use --format=ustar and don't write junk rows

So old cache entries self-heal on next cache miss → new save. Pre-alpha; acceptable.

Test plan

  • 487/487 tests pass (added 2)
  • Lint + format clean
  • No schema change
  • Parser tests are host-independent (no real tar involved)

https://claude.ai/code/session_016HXj6HW6bxSn8EYuKcxTD9


Generated by Claude Code

The user reported `._dist` files and `PaxHeaders/<name>` entries
appearing alongside legit files after a cache restore. Root cause:
BSD tar (macOS default) emits PAX extended-header records per entry
by default for xattr / mtime-nanos metadata, and Finder / `cp -p`
leave AppleDouble `._<name>` resource-fork siblings around. Our save
pipeline picked both up via the project's output glob and our tar
parser treated them as regular files on restore.

Two layers of fix:

1. **At save time**: force `tar --format=ustar` so BSD tar produces
   no PAX records. Also pass `COPYFILE_DISABLE=1` env to suppress
   Apple's `copyfile()` from attaching xattrs to staged files.

2. **At parse time (defense in depth)**: skip tar entries with
   typeflag 'x' (per-entry PAX), 'g' (global PAX), or 'X' (Solaris
   extended). Skip entries whose basename starts with `._`
   (AppleDouble). This protects against legacy contaminated cache
   entries written before this PR, and against future tar-binary
   format quirks.

`parseTarHeaders` doc comment updated to document the typeflag
filter rules.

Two new tests in `tests/cache-baseline.test.ts` construct
synthetic tars (no shell-out — bytes built directly with a
ustar-header helper) containing PAX + AppleDouble entries and
assert the parser returns only the real file. Independent of
which `tar` binary the host has.

No CACHE_VERSION bump: extract-side filter handles old contaminated
entries gracefully on the next save. The first cache hit after this
ships will still see junk paths in `output_files` rows for OLD
entries; that triggers `isOutputsCurrent → false → restore`, which
then writes the clean tree. Subsequent saves use the new tar
format and don't write junk rows.

487/487 tests pass.
@Exelord
Exelord merged commit 5018f9b into main May 17, 2026
1 check passed
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