Skip to content

Name the entry tar could not write, and two CLI answers that were wrong - #109

Merged
otsobide merged 2 commits into
devfrom
feature/three-cli-and-tar-fixes
Aug 27, 2026
Merged

Name the entry tar could not write, and two CLI answers that were wrong#109
otsobide merged 2 commits into
devfrom
feature/three-cli-and-tar-fixes

Conversation

@otsobide

Copy link
Copy Markdown
Owner

Fixes #93, #67 and #75. Three small ones, all about what the tool tells you.

#93 — tar did not say which entry failed

Issue #64 asked a write failure to name the entry at fault. zip and 7z do. tar has two write paths, and the one an archive with nothing renamed takes, which on Unix is nearly every archive, still answered in the dependency's words.

before: failed to unpack `/…/out-tar/a.txt/b.txt`
after:  cannot write entry "a.txt/b.txt" to /…/out-tar/a.txt/b.txt: Not a directory (os error 20)
zip:    cannot write entry "a.txt/b.txt" to /…/out-zip/a.txt/b.txt: File exists (os error 17)

unpack_in itself is untouched: it is the traversal guard tar has always used, and its canonicalizing containment check is what stops a write from following a symlink already in the output directory. Only its error is dressed.

That error needs unwrapping first, because it is a stack of wrappers each adding the destination again:

[0] failed to unpack `/…/out/a.txt/b.txt`
[1] failed to unpack `a.txt/b.txt` into `/…/out/a.txt/b.txt`
[2] Not a directory (os error 20)

Only the last says what went wrong. Using level 0 inside an Entry, which names the destination itself, printed the path three times.

The test that should have caught this passed for the wrong reason. It asserted message.contains("a.txt/b.txt"), which tar satisfied because the entry name is a substring of the destination path, so the guarantee was never checked at all. It now requires the name quoted as an entry.

#67 — it answered with a path you never typed

$ collapse compress ./sub/a.txt -f zip
before: Created /Users/…/scratch/misc.A4Z7/sub/a.txt.zip
after:  Created ./sub/a.txt.zip

The canonicalization is there for good reasons and stays: ., .. and a trailing slash need to resolve to something with a usable name, and an output aliasing the source can only be caught against a resolved path. Both spellings are kept now, and each is used where it belongs. The arcname deliberately keeps taking the resolved name, since that is what is stored inside the archive.

A source with no name of its own falls back to the resolved path, which is also what keeps a directory's archive beside it rather than inside it, where the guards would then refuse it.

#75-f and -o could contradict each other in silence

$ collapse compress notes.txt -f tar -o mixed.zip
Created mixed.zip
$ collapse extract mixed.zip -o out
error: Could not find EOCD

Refused now, before anything is read or written:

error: --format says tar but mixed.zip ends in .zip, which is a different format. Drop one of the two.

A warning was the other candidate and is worse: it goes to a terminal nobody reads in a script, and what is left is still a broken file with a misleading name.

An extension naming no known format is deliberately not a contradiction. -o backup.bin -f 7z is a choice, and a guard that refused it would be overreaching.

Tests

Seven new, each checked against the unfixed code:

mutation tests that fail
tar back to the dependency's wrapper 1
#67 reverted, reports the resolved path 2
#75 reverted, --format wins in silence 1
#75 over-refusing, unknown extension breaks -f 2

One desktop assertion moves with #93: it pinned the old tar message, and the new one is strictly better, so it now requires the entry named and the real cause, and fails if the dependency's wrapper leaks through again.

620 Rust tests and 116 Vitest, 505 offline. architecture.md's compression flow updated, since steps 1 to 3 described the old behaviour.

Issue #64 asked a write failure to name which entry was at fault. zip and 7z
do. tar has two write paths, and the one an archive with nothing renamed takes,
which on Unix is nearly every archive, still answered in the tar crate's own
words: `failed to unpack \`/…/out/a.txt/b.txt\``. No entry named, and the
absolute destination loose in the sentence.

`unpack_in` itself is untouched. It is the traversal guard tar has always used
and its canonicalizing containment check is what stops a write from following
a symlink already in the output directory; only its error is dressed.

Its error needs unwrapping first, because it is a stack of wrappers each
adding the destination again:

  [0] failed to unpack `/…/out/a.txt/b.txt`
  [1] failed to unpack `a.txt/b.txt` into `/…/out/a.txt/b.txt`
  [2] Not a directory (os error 20)

Only the last says what went wrong, and it is the register the other two
formats answer in. Using level 0 inside an `Entry`, which names the
destination itself, printed the path three times.

  before: failed to unpack `/…/out-tar/a.txt/b.txt`
  after:  cannot write entry "a.txt/b.txt" to /…/out-tar/a.txt/b.txt: Not a directory (os error 20)
  zip:    cannot write entry "a.txt/b.txt" to /…/out-zip/a.txt/b.txt: File exists (os error 17)

The test that should have caught this passed for the wrong reason. It asserted
`message.contains("a.txt/b.txt")`, which tar satisfied because the entry name
is a substring of the destination path, so the guarantee was never checked at
all. It now requires the name quoted as an entry, and requires the real cause,
and it fails against the old code.

Closes #93
…icts -o

Two issues in `run_compress`, both decidable long before anything is written.

**#67.** The source is canonicalized, for good reasons: `.`, `..` and a
trailing slash have to resolve to something with a usable name, and an output
that aliases the source can only be spotted against a resolved path. That
resolved path then leaked into what the user was told. Type six characters,
get an absolute path back:

  $ collapse compress ./sub/a.txt -f zip
  before: Created /Users/…/scratch/misc.A4Z7/sub/a.txt.zip
  after:  Created ./sub/a.txt.zip

Both spellings are kept now. The resolved one still drives the guards, the
engine and the arcname, which has to be the file's real name. The typed one is
what gets reported. A source with no name of its own falls back to the
resolved path, which is also what keeps a directory's archive beside it rather
than inside it, where the guards would then refuse it.

**#75.** `--format` won over `-o`'s extension in silence, producing a file
whose name lies about its contents and that this same CLI then rejects:

  $ collapse compress notes.txt -f tar -o mixed.zip
  Created mixed.zip
  $ collapse extract mixed.zip -o out
  error: Could not find EOCD

Refused now, before anything is read or written. A warning was the other
candidate and is worse: it goes to a terminal nobody reads in a script, and
what is left behind is still a broken file with a misleading name.

An extension that names no known format is deliberately **not** a
contradiction. `-o backup.bin -f 7z` is a choice, and a guard that refused it
would be overreaching; a test pins that.

Every new test was checked against the unfixed code. Reverting #67 fails two,
reverting #75 fails one, and making the guard over-refuse fails two more.

Closes #67
Closes #75
@otsobide otsobide added the full-matrix Run the macOS and Windows suites on this PR label Aug 27, 2026
@otsobide
otsobide merged commit 9ee1fce into dev Aug 27, 2026
36 checks passed
This was referenced Aug 27, 2026
@otsobide
otsobide deleted the feature/three-cli-and-tar-fixes branch August 28, 2026 18:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

full-matrix Run the macOS and Windows suites on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant