Skip to content

Treat SSH host paths as POSIX paths on every client - #115

Open
huntharo wants to merge 7 commits into
tobi:mainfrom
huntharo:fix/remote-path-separators
Open

huntharo wants to merge 7 commits into
tobi:mainfrom
huntharo:fix/remote-path-separators

Conversation

@huntharo

@huntharo huntharo commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #114. Only the last commit (ssh: treat remote paths as POSIX paths on every client) is new; the earlier four disappear from this diff once #114 merges.

Problem

SSH hosts are Unix: the remote command, the embedded scanners and the delete script are POSIX sh and Linux tools. But the client joined, split and compared remote paths with std::path, which follows the client's rules. On a Windows client:

  • path_of joined /srv/data and logs into /srv/data\logs. Marks, the trail, the agent prompt and the exported delete script then named files that don't exist on the host. With root /, realpath decides whether the script deletes a file literally named \logs.
  • A Unix name containing \ or : (common on Linux) was rejected, which failed the whole scan.
  • PathBuf equality on Windows treats \ as a separator, so marks on /a/b\c and /a/b/c collided.
  • ~ shortening, breadcrumbs, the trail separator and the indexing line all split remote paths by Windows rules.

Change

  • disktree_core::path_style::PathStyle: Native is std::path, used for local scans. Posix is /-separated, used for SSH hosts. It provides join, parent, file name, ancestors, relative names, names and is-below. The app picks one from remote.is_some(). On Unix clients the two agree, so local and Unix-client behaviour is unchanged.
  • Through PathStyle: tree::path_of, marks::find/refresh/is_hidden/display_path, crumbs_for_path, breadcrumbs and crumb labels, mark containment (toggle_mark, marked_ancestor, the remote plan filter, marks_ancestor), short_name, the trail separator, and the indexing progress line.
  • Marks are indexed by exact path text, not PathBuf equality.
  • Protocol: a host reporting platform: "windows" is refused by name, instead of being half-read with the wrong separator. The header's home and disk_root are now checked as POSIX-absolute, like root. The Windows-only rejection of \ and : in names is gone, because those names are now joined correctly.
  • CLI: disktree ssh HOST PATH refuses a PATH that doesn't start with /.
  • AGENTS.md: the invariant and the "where changes belong" table name PathStyle.

Tests

  • path_style unit tests for both styles: \ and : in names, name boundaries (/srv vs /srvx), the root, trailing slashes, and C:\/\x not being POSIX-absolute.
  • Hand-built Unix-host streams in remote.rs that run on every client. One checks that names with \ and : arrive whole. The other checks that a Windows host, and a non-POSIX home or volume, are refused.
  • A window-harness test that loads a Unix host's tree with logs\old/a:b, checks crumbs, the mark, the trail and ~ display, and copies the delete script. On a Windows runner this is the case that used to break.
  • Four round-trip tests that run the helper on the test machine are now cfg(unix), because that machine is the host. (Fix the macOS and Windows CI failures on main #114 already made the live preview dedup test Unix-only.)
  • For the same reason, the three disktree-scan executable tests that read a snapshot or a discovery result are cfg(unix). The disconnect test reads neither and still runs everywhere.
  • Window tests that pretend a local fixture is an SSH session now root it at a / path (remote_view_over): the temp directory on Unix, /srv/fixture on Windows.

Verified locally on macOS: cargo xtask lint and cargo xtask test pass, and cargo clippy -p disktree-core --all-targets --target x86_64-pc-windows-msvc -- -D warnings is clean. The app crate can't be cross-checked for Windows from macOS, so CI is the first run of these tests on a Windows client.

Note for local runs: gpui-omarchy's theme watcher watches $HOME. While other processes write there, app tests can abort with "Detected activity on thread notify-rs fsevents loop". Running with HOME set to an empty directory avoids it. This is unrelated to this change.

Windows clients rejected every path an SSH host sent. The helper is
Unix-only, so remote roots are /-rooted, but Path::is_absolute also
wants a drive letter on Windows. Root discovery, the scan header and
the picker now check remote paths with is_remote_absolute, which
agrees with is_absolute on Unix.

The volume picker test compared against the unresolved temporary
path. Accepting a local root canonicalizes it, and the temporary
directory is /var -> /private/var on macOS and an 8.3 short name on
Windows runners.

spawn_command and wait in remote.rs are only used by the Unix tests,
so clippy -D warnings failed on Windows. Gate them on unix.
Path::has_root also accepts \x and C:\x on Windows, which name nothing
on the Unix host the helper runs on. Check for a leading / instead.
serve also runs on Windows, and the round-trip tests there send native
C:\ roots. Accept a path that is absolute on this platform or
/-rooted; \x stays rejected.
The preview comes from the Unix helper's streaming walk. On Windows
serve sends a finished tree with no preview, so the test's unwrap
failed there. It was hidden on main because cargo test stops at the
first failing test binary, which was the app's.
SSH hosts are Unix: the remote command, the embedded scanners and the
delete script are POSIX shell and Linux tools. But remote paths were
joined, split and compared with std::path, which follows the client. On
a Windows client that joined /home/x and child into /home/x\child, so
marks, the trail and the exported delete script named files that do not
exist on the host, and a Unix name containing \ or : failed the whole
scan.

PathStyle (core) holds both rules: Native is std::path for local scans,
Posix is /-separated for SSH hosts. path_of, mark lookup and display,
crumbs, the trail, mark containment and the progress line now go through
it. On Unix clients the two agree, so nothing changes there.

Marks are keyed by exact path text, since PathBuf equality on Windows
treats \ as a separator. The client refuses a Windows host by name, and
checks the header's home and disk root as well as its root. The SSH CLI
refuses a root that is not /-rooted. The round-trip tests that run the
helper locally are Unix-only, since the local machine is the host; new
hand-built streams and a window test cover a Unix host on every client.
@huntharo
huntharo force-pushed the fix/remote-path-separators branch from 022f781 to 329e872 Compare October 24, 2026 02:10
Four window tests built a local fixture, set app.remote, then looked up
the fixture's own paths. On Windows those are C:\ paths, which a Unix
host never sends and POSIX rules do not resolve. remote_view_over roots
the scanned fixture at the temporary directory on Unix, keeping the
nothing-deleted checks real, and at /srv/fixture on Windows.
These run disktree-scan on the test machine and read what it sends, so
the machine is the host. Clients now refuse Windows hosts, so on
Windows they failed with that refusal. The disconnect test reads no
snapshot and still runs everywhere.
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