Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion crates/disktree-app/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -948,7 +948,12 @@ impl Disktree {
cx.notify();
return;
}
if !path.is_absolute() {
let absolute = if self.volumes_source.is_some() {
disktree_core::remote::is_remote_absolute(&path)
} else {
path.is_absolute()
};
if !absolute {
self.picker_error =
Some("Enter an absolute directory path.".into());
cx.notify();
Expand Down
5 changes: 4 additions & 1 deletion crates/disktree-app/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1014,7 +1014,10 @@ fn enter_in_the_focused_volume_picker_scans_the_selected_root(
) {
cx.update(gpui_omarchy::init);
let temp = fixture();
let target = temp.path().join("junk");
// Accepting a local root canonicalizes it, and the temporary directory
// is behind a link on macOS (/var -> /private/var) and a short 8.3 name
// on Windows runners, so compare against the resolved path.
let target = dunce::canonicalize(temp.path().join("junk")).unwrap();
let (view, cx) = view_over(temp.path(), cx);
update(&view, cx, |app, _| {
app.volumes = vec![disktree_core::space::Volume {
Expand Down
44 changes: 39 additions & 5 deletions crates/disktree-core/src/remote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
//! and stream without making a second serialized copy of the whole tree.

use std::io::{self, BufRead, BufReader, Read as _, Write};
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::sync::{Arc, Mutex, mpsc};

Expand All @@ -23,6 +23,19 @@ const VERSION: u32 = 1;
const MAX_LINE: u64 = 1 << 20;
const MAX_DIAGNOSTICS: usize = 16 << 10;

/// Whether `path` names a directory from the remote host's root.
///
/// `serve` runs on Windows as well as Unix, so a remote path is either
/// `/`-rooted or absolute the Windows way. On Windows
/// `Path::is_absolute` wants a drive letter, which rejects every Unix path,
/// and `Path::has_root` also accepts `\x`, which is neither. On Unix this
/// agrees with `is_absolute`.
#[must_use]
pub fn is_remote_absolute(path: &Path) -> bool {
path.is_absolute()
|| path.as_os_str().as_encoded_bytes().first() == Some(&b'/')
}

// Non-interactive shells often omit ~/.local/bin. Do not require users to
// edit the server's startup files after the documented user-local install.
const REMOTE_COMMAND: &str = r#"if command -v disktree-scan >/dev/null 2>&1; then
Expand Down Expand Up @@ -84,8 +97,11 @@ pub fn read_roots(input: &mut impl BufRead) -> io::Result<Roots> {
|| roots
.volumes
.iter()
.any(|volume| !volume.point.is_absolute())
|| roots.home.as_ref().is_some_and(|home| !home.is_absolute())
.any(|volume| !is_remote_absolute(&volume.point))
|| roots
.home
.as_ref()
.is_some_and(|home| !is_remote_absolute(home))
{
return Err(io::Error::other("incompatible remote root discovery"));
}
Expand Down Expand Up @@ -312,7 +328,7 @@ fn read_snapshot_with_progress(
shared: &Shared,
) -> io::Result<Snapshot> {
let header: Header = receive(input)?;
if header.version != VERSION || !header.root.is_absolute() {
if header.version != VERSION || !is_remote_absolute(&header.root) {
return Err(io::Error::other("incompatible remote helper protocol"));
}
let mut progress = Progress {
Expand Down Expand Up @@ -577,7 +593,8 @@ impl RemoteScan {
Self::spawn_with_archive(command, host, request, archive)
}

#[cfg(test)]
// Only the Unix tests run a fake ssh, so Windows has no caller.
#[cfg(all(test, unix))]
fn spawn_command(
command: Command,
host: &str,
Expand Down Expand Up @@ -744,6 +761,7 @@ mod tests {
}
}

#[cfg(unix)]
fn wait<T: Send + 'static>(scan: &RemoteJob<T>) -> io::Result<T> {
wait_for(scan, std::time::Duration::from_secs(10))
}
Expand Down Expand Up @@ -1086,6 +1104,19 @@ mod tests {
);
}

/// A Windows client must accept a Unix host's paths without a drive
/// letter, and every client accepts its own platform's absolute paths.
#[test]
fn remote_paths_are_absolute_without_a_drive_letter() {
assert!(is_remote_absolute(Path::new("/")));
assert!(is_remote_absolute(Path::new("/home/someone")));
assert!(!is_remote_absolute(Path::new("home/someone")));
assert!(!is_remote_absolute(Path::new("")));
// Rooted without a drive is absolute nowhere.
assert!(!is_remote_absolute(Path::new(r"\home\someone")));
assert_eq!(is_remote_absolute(Path::new(r"C:\Users")), cfg!(windows));
}

#[test]
fn rejects_ssh_options_and_shell_syntax() {
for host in ["-oProxyCommand=oops", "dev; touch x", "dev\nother", ""] {
Expand Down Expand Up @@ -1174,6 +1205,9 @@ mod tests {
assert_eq!(shared.lock().unwrap().stage, Stage::Finalizing);
}

// The live preview this checks comes from the Unix helper's streaming
// walk; on other systems `serve` sends a finished tree with no preview.
#[cfg(unix)]
#[test]
fn desktop_dedup_counts_links_once_without_requiring_external_names() {
let dir = tempfile::tempdir().unwrap();
Expand Down
Loading