Skip to content

chore: remove the naming machinery v0.10.0 left behind - #116

Merged
otsobide merged 6 commits into
developfrom
feature/remove-the-dead-naming-machinery
Aug 28, 2026
Merged

chore: remove the naming machinery v0.10.0 left behind#116
otsobide merged 6 commits into
developfrom
feature/remove-the-dead-naming-machinery

Conversation

@otsobide

Copy link
Copy Markdown
Owner

Takes out the machinery v0.10.0 left behind when extraction stopped renaming
entries to fit the host. Four commits, each one green on its own, because the
first is a pure deletion and the rest change what users see.

What went

Layer Removed
core NamePlan, the three extract_*_planned variants, tar's second write path, Substitutions, ExtractOptions::with_replacements, NameRules::rewrite/rewrite_entry/check_replacements, DEVICE_SUFFIX, six of NameError's seven variants
cli adjustments, Adjustment, Outcome::Extracted { adjusted }
desktop the unwritable_names command, src-tauri/src/names.rs, src/names.js, the naming sheet in App.vue, extract_archive's replacements argument, Extraction

The two findings that are not just deletion

The CLI's refusal message was lying, and shipped that way in v0.10.0. It
told users a replacement would unblock the extraction and pointed them at the
desktop dialog as the front end that could ask for one:

Going ahead needs a replacement for '?' (1 entry), and this command cannot
ask for them mid-run without becoming interactive: the Collapse desktop app
asks once per character, checks the answer is writable too, and extracts
with it.

There is no replacement to give and that dialog collects answers that reach
nothing. explain had the same problem in two more lines. The message now says
what is true: nothing was extracted, no option would change that, extract on a
system that can hold the names. A test asserts the dead pointer is absent,
so it cannot come back.

Tar carried two write paths, and the dead one had its own copy of a security
check.
unpack_in derives the destination from the entry's own name, so it
could not write a renamed entry — and unpack_in is also the traversal guard,
the canonicalizing containment check that stops a write following a symlink
already in the output. The renamed branch therefore repeated that check by
hand. Both are gone and every entry goes through unpack_in again.

Tests

Verified locally at every step, not just at the end — cargo is on this machine
now, so each commit was run before the next was started.

  • 595 Rust tests pass (494 root workspace, 101 src-tauri).
  • 88 Vitest cases pass (43 desktop, 45 server-frontend).
  • make test is green end to end.

The root workspace goes 506 → 494 and the desktop suite 18 → 4 naming cases.
Every one of those tested machinery that no longer exists. Two removals are
recorded rather than done silently:

  • apps/desktop/src-tauri/tests/names.rs keeps four of eighteen cases, because
    one guarantee survives and matters more now that it stands alone: a refusal
    must reach the user with its reason attached.
    A NUL prints as nothing, so an
    entry named without its fault looks fine, and "extraction failed" would read
    as a broken archive rather than a foreign one.
  • a_replacement_cannot_carry_an_entry_out_of_the_output_directory is replaced
    by a comment where it stood. It proved an answer like ../../escape could not
    move an entry out of the output directory. That hole is now closed by
    construction instead of by a check — there is no way to supply a replacement,
    so the code that would fail the test does not compile — and the reasoning is
    worth finding where the test used to be.

Docs

architecture.md and desktop.md described this surface as inert and awaiting
removal; they now describe what is there. The desktop's command surface is four,
not five.

`NamePlan` has been the identity since extraction stopped renaming entries, so
every `extract_*_planned` was its plain counterpart with an extra argument and a
lookup that always answered `None`. The type, the three variants and the
wrappers that called them are gone; `extract_zip`, `extract_7z` and
`extract_tar` are now the functions themselves.

The one that mattered is tar. It carried **two** write paths, because
`unpack_in` derives the destination from the entry's own name and so cannot
write a renamed entry at all — and `unpack_in` is also the traversal guard tar
has always used, the canonicalizing containment check that stops a write from
following a symlink already sitting in the output directory. The renamed branch
therefore had to repeat that check by hand. With nothing renamed, that branch
and its hand-rolled containment check are both gone, and every entry goes
through `unpack_in` again.

No behaviour changes and no test changes: 506 passing before, 506 passing after.
That is the point of doing this separately from anything that alters what the
code does.

The explicit `..` refusal before `unpack_in` stays. It existed because the
renamed branch had no other guard, and it is now belt and braces — but it is
also what names the offending entry, which `unpack_in`'s own `Ok(false)` does
not.
`adjustments`, `Adjustment` and `Outcome::Extracted { adjusted }` are gone. They
reported the names extraction had rewritten to fit the host, and extraction
stopped rewriting names in v0.10.0, so the list was provably empty at the only
place it was built.

**More seriously, the refusal message was lying, and shipped that way.** Two of
its sentences described the old scheme:

    Going ahead needs a replacement for '?' (1 entry), and this command cannot
    ask for them mid-run without becoming interactive: the Collapse desktop app
    asks once per character, checks the answer is writable too, and extracts
    with it.
    The names above with nothing to replace are adjusted for you once it can go
    ahead.

Neither is true. There is no replacement to give, and the desktop dialog it
points at collects answers that reach nothing. A message that sends someone to a
feature that no longer works is worse than one that says less, so it now says
what is actually the case: nothing was extracted, no option would change that,
extract on a system that can hold the names.

`explain` had the same problem in two lines — a trailing run "would be dropped"
and a device name "would be written under an adjusted name". Both described
adjustments that no longer happen.

The characters are still listed with their counts, and deliberately: there is no
answer to give, but one character across forty entries is an archive that is
awkward here, while forty characters is one that does not belong on this machine
at all, and that is the user's call.

The two tests that pinned the old sentences are rewritten rather than deleted —
one of them now asserts the dead pointer is *absent*, so it cannot come back.
506 passing, unchanged.
The whole conversation is gone: `unwritable_names`, the `names` module that
shaped its answer for the webview, the sheet in `App.vue` with its fields,
validation and adjustment notes, `src/names.js`, and the `replacements`
argument `extract_archive` took.

It had stopped being a conversation. Extraction refuses a name it cannot write
rather than asking what to call it, so the dialog collected answers that reached
nothing and then reported a refusal the user could not act on — the worst shape
a question can have.

`Extraction` goes with it. A naming refusal had a variant of its own so the
sheet could stay open on it while everything else went to the error banner; with
no sheet there is one destination for both, so `extract_archive` returns
`Result<Vec<String>, String>` like every other command.

**Four places had to move together**, which is what `tests/ipc.rs` exists to
police: the `generate_handler!` list, the command itself, the `invoke` call in
`App.vue`, and the stub switch in `tests/App.test.js`. Its `BASELINE` and
signature table are updated too, so the five-command surface is now four and
nothing can quietly become untyped.

Tests: `tests/names.rs` is rewritten rather than deleted, down from eighteen
cases to four, because one guarantee in it survives and is worth more now that
it is the only one — a refusal must reach the user **with its reason attached**.
A NUL prints as nothing, so an entry named without its fault looks perfectly
fine, and a user told "extraction failed" would think the archive is broken when
it is merely foreign. Ten dialog cases went from `App.test.js`, and the two that
remain assert a refusal and an ordinary failure both land in the banner.

101 Rust tests and 43 Vitest cases pass.
With both front ends off it, the rest goes: `Substitutions` and its
`FromIterator`, `ExtractOptions::with_replacements` and `replacements`,
`NameRules::rewrite`, `rewrite_entry`, `check_replacements` and
`check_replacement`, `DEVICE_SUFFIX`, `NameProblem::replaceable`,
`NameRules::offending_characters`, and six of `NameError`'s seven variants with
the `in_entry` helper that re-pointed them.

`NameError` is one variant now, `Unwritable`, which is the only one anything
constructed. The other six described a negotiation that no longer happens: no
replacement to be missing, unwritable or separator-bearing, no rewrite to make
two entries collide, no adjustment to empty a component, no answer key to be
more than one character.

`NameReport` stays, and so does its `characters` half. It is no longer a
questionnaire — nothing can be answered — but it is what the CLI groups its
refusal by, and one character across forty entries is a different problem from
forty characters.

Tests: eleven cases in `apps/core/tests/names.rs` exercised the removed
machinery and go with it. `an_entry_splits_the_same_way_on_every_host` goes too,
for a different reason — it used `rewrite_entry` only as a way to observe where
a name splits, and that guarantee is already held by
`the_report_sees_a_colon_in_the_first_component_too` and
`only_windows_refuses_a_backslash_inside_a_component`, both of which survive.
`the_report_separates_the_questions_from_the_stated_adjustments` is rewritten
around what `characters` still means.

In `security.rs`, `a_replacement_cannot_carry_an_entry_out_of_the_output_
directory` is replaced by a comment in its place rather than removed silently.
It proved a hostile answer like `../../escape` could not move an entry out of
the output directory. That hole is now closed by construction instead of by a
check — there is no way to supply a replacement, so the code that would fail
the test does not compile — and the guarantee is still one this crate makes,
so it is worth someone finding the reasoning where the test used to be.

494 passing across the root workspace, down from 506 by exactly the twelve that
tested machinery that no longer exists.
`architecture.md` and `desktop.md` described `Substitutions`, `NamePlan`, the
`extract_*_planned` variants and the desktop's `unwritable_names` command as
inert surface awaiting removal. They have been removed, so the documents now
describe what is there.

Two facts worth keeping rather than just deleting: `NameError` is down to one
variant because the other six described a negotiation that no longer happens,
and tar went from two write paths to one — the renamed branch had to repeat by
hand the containment check `unpack_in` performs, and both went together.

The desktop's command surface is four, not five.
@otsobide
otsobide merged commit 017b86a into develop Aug 28, 2026
18 checks passed
@otsobide otsobide mentioned this pull request Aug 28, 2026
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.

1 participant