Skip to content

Windows receiver: rewrite names Windows would misread, skip what it can't store - #70

Open
op-q wants to merge 4 commits into
chore/remove-browser-clientfrom
fix/windows-receiver
Open

op-q wants to merge 4 commits into
chore/remove-browser-clientfrom
fix/windows-receiver

Conversation

@op-q

@op-q op-q commented Sep 14, 2026

Copy link
Copy Markdown
Owner

Cross-platform plan, phase 1. Behaviour change, waiting for your review. Stacked on #69.

Problems this fixes (found by reading the code; Windows had never run it)

  • A symlink in a folder aborted extraction on Windows at the first link. That covers most real project folders (node_modules/.bin, virtualenvs).
  • Names that are ordinary on Linux and macOS mean something else to Windows:
Name sent Windows would… Now stored as
notes.txt:hidden write an NTFS stream on notes.txt notes.txt_hidden
mine.txt::$DATA open mine.txt's main stream mine.txt__$DATA
CON, nul.txt open a device CON_, nul_.txt
report. / notes silently drop the dot or space report_ / notes_
C:x treat it as a drive-relative path C_x
10:30 standup.md (from a Mac) create a stream 10_30 standup.md

Design

  • Rewrite, not refuse, because most of these names come from honest senders. A rewrite changes one component's spelling and never introduces a separator, so an entry cannot move to another directory. The existence and symlink checks run on the rewritten path, and every rename is shown as a warning. This is open question 1 of the plan, answered as it leaned.
  • Archive paths are split on / by hand instead of going through Path, so every OS judges the same components. A backslash inside a component is refused everywhere.
  • Warn and continue when the filesystem refuses a name (InvalidFilename/InvalidInput), or when a symlink can't be created. A full disk or a permission problem still aborts.
  • On Windows, symlinks aren't attempted at all. With --force, trying would delete the existing file before failing to create the link.
  • The policy is in the new cli/src/names.rs: pure functions tested on every OS. The single-file receive path uses it too (report:v2.pdf is saved as report_v2.pdf, and the receiver is told).

Tests (210 total, up from 193)

  • 13 unit tests for the naming policy, plus 4 portable extractor tests that drive the Windows naming through the real extractor on Linux: rewriting, collisions after a rewrite, backslash refusal, and a filesystem-refused name.
  • Negative control: with the old abort behaviour restored, the refused-name test fails.
  • A Windows-only test with hostile names against a real NTFS destination. It checks that the receiver's existing file is untouched, no stream is created, and nothing is written outside the destination.
  • A Windows-only skipped-symlink test, and a case-insensitive collision test for macOS and Windows.
  • The Windows-only tests run in CI on this PR. They have not run on a real Windows machine; that's phase 5's manual checklist.

fmt, clippy -D warnings, cargo test --workspace --all-targets and check-secrets.sh all pass locally (Linux).

🤖 Generated with Claude Code

https://claude.ai/code/session_01G7Fy45hUvna94cd79WKG8S

Before this, a folder with a symlink in it stopped extracting on Windows at the
first link. Names ordinary on Linux and macOS were worse. `notes.txt:hidden`
would have become an NTFS stream on `notes.txt`, `CON` a device, `report.` a
file quietly named `report`, and `a<b` an error that ended the extraction.

Names are rewritten, not refused, because most of them come from honest
senders: `10:30 standup.md` is a normal name on a Mac. Each character Windows
would interpret becomes `_`, trailing dots and spaces become `_`, and a device
name gets `_` after its stem. A rewrite never introduces a separator, so it
cannot move an entry to another directory. The existence and link checks run on
the rewritten path, and every rename is reported.

Archive paths are now split on `/` by hand rather than handed to `Path`, which
reads `C:x` as a drive on Windows and as a name elsewhere, so every platform
judges the same components.

A name the filesystem refuses, or a link the system cannot create, now skips
that entry with a warning instead of ending the extraction. A full disk or a
permission problem still ends it. Pinned without Windows by a 300-byte name;
with the old behaviour restored, that test fails.

Cross-platform plan, phase 1. The Windows-only tests run in CI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G7Fy45hUvna94cd79WKG8S
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

The first Windows run of the naming tests showed that storing `C:x` as sent
there is refused: PathBuf::push treats it as drive-relative and replaces the
destination, and the final inside-the-destination check catches that. Names
are never stored as sent on Windows, so the refusal is the correct outcome and
the test now asserts it, pinning the safety check that found it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G7Fy45hUvna94cd79WKG8S

This branch has not been deployed

No deployments
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