fix(cache): never produce PAX / AppleDouble entries in cache tars - #99
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bug
Cache restores were landing files like
outputs/._distandPaxHeaders/<name>alongside real outputs in the project tree. Two root causes:x/g) per entry by default — for xattrs, mtime-nanos, SCHILY metadata. They look like regular entries to a naïve parser.._<name>) are macOS resource-fork siblings that Finder /cp -pleave 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_IGNOREfilter list unchanged.Two-layer fix
1. At save (
src/cache/cache.ts:save())--format=ustarforces 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=1blocks Apple'scopyfile()from attaching xattrs to staged children — tar has nothing to AppleDouble for.2. At parse (
src/cache/tar.ts:parseTarHeaders) — defense in depthThis protects against:
Tests
Two new tests in
tests/cache-baseline.test.tsconstruct synthetic tars byte-by-byte (no shell-out — independent of whichtarbinary the host has) with PAX + AppleDouble records, assert the parser returns only the real file entries.skips PAX extended-header records (typeflag x), AppleDouble entries (._*), and keeps real filesoutputs/main.jsonly — PAX header +._main.jsfilteredskips global PAX records (typeflag g) tooCompatibility
No CACHE_VERSION bump needed. Old contaminated entries in
<cacheDir>/<hash>.tar.zstwill:output_filesrows from the old save still reference._dist/...paths →isOutputsCurrentmismatches → triggers full restore (which is now clean)--format=ustarand don't write junk rowsSo old cache entries self-heal on next cache miss → new save. Pre-alpha; acceptable.
Test plan
tarinvolved)https://claude.ai/code/session_016HXj6HW6bxSn8EYuKcxTD9
Generated by Claude Code