diff --git a/src-tauri/src/core/sftp/scp_normal.rs b/src-tauri/src/core/sftp/scp_normal.rs index eaf9a475..9d8c339e 100644 --- a/src-tauri/src/core/sftp/scp_normal.rs +++ b/src-tauri/src/core/sftp/scp_normal.rs @@ -323,6 +323,14 @@ fn split_ls_fields(line: &str) -> Option<(Vec<&str>, &str)> { (cursor < bytes.len()).then(|| (fields, &line[cursor..])) } +fn list_dir_command(path: &str) -> String { + format!("LC_ALL=C ls -la -n -- {}", sh_quote(path)) +} + +fn stat_command(path: &str) -> String { + format!("LC_ALL=C ls -lad -n -- {}", sh_quote(path)) +} + fn parse_ls_line(line: &str) -> Option { let line = line.trim_start(); if line.trim_end().is_empty() || line.starts_with("total ") { @@ -442,11 +450,7 @@ async fn stat_remote_properties( ssh_handle: &Arc, path: &str, ) -> AppResult { - let result = exec_command( - ssh_handle, - &format!("LC_ALL=C ls -lad -- {}", sh_quote(path)), - ) - .await?; + let result = exec_command(ssh_handle, &stat_command(path)).await?; if result.exit_code != Some(0) { let stderr_text = String::from_utf8_lossy(&result.stderr); return Err(AppError::Channel(format!( @@ -888,9 +892,7 @@ impl RemoteFs for ScpNormalBackend { async fn list_dir(&self, path: &str) -> AppResult> { let listing_path = remote_dir_listing_path(path); - let output = self - .exec_ok(&format!("LC_ALL=C ls -la -- {}", sh_quote(&listing_path))) - .await?; + let output = self.exec_ok(&list_dir_command(&listing_path)).await?; let mut entries = Vec::new(); for line in output.lines() { if let Some(mut entry) = parse_ls_line(line) { @@ -908,9 +910,7 @@ impl RemoteFs for ScpNormalBackend { } async fn stat(&self, path: &str) -> AppResult { - let output = self - .exec_ok(&format!("LC_ALL=C ls -lad -- {}", sh_quote(path))) - .await?; + let output = self.exec_ok(&stat_command(path)).await?; let line = output .lines() .find(|l| !l.trim().is_empty() && !l.starts_with("total ")) @@ -1558,7 +1558,60 @@ impl RemoteFs for ScpNormalBackend { #[cfg(test)] mod tests { - use super::parse_ls_line_to_properties; + use super::{list_dir_command, parse_ls_line, parse_ls_line_to_properties, stat_command}; + + #[test] + fn ls_parser_handles_numeric_owner_and_group() { + let entry = parse_ls_line("-rw-r--r-- 1 1000 100513 203 Aug 3 16:37 .zshrc").unwrap(); + assert_eq!(entry.name, ".zshrc"); + assert_eq!(entry.owner, "1000"); + assert_eq!(entry.group, "100513"); + assert_eq!(entry.size, 203); + assert!(!entry.is_dir); + } + + #[test] + fn ls_parser_keeps_hidden_directory_name() { + let entry = parse_ls_line("drwx------ 4 1000 100513 4096 Sep 1 09:31 .copilot").unwrap(); + assert_eq!(entry.name, ".copilot"); + assert!(entry.is_dir); + } + + #[test] + fn ls_parser_keeps_spaces_in_file_name() { + let entry = + parse_ls_line("-rw-r--r-- 1 1000 100513 123 Sep 1 09:31 hello world.txt").unwrap(); + assert_eq!(entry.name, "hello world.txt"); + assert!(!entry.is_dir); + } + + #[test] + fn ls_parser_keeps_spaces_in_directory_name() { + let entry = parse_ls_line("drwxr-xr-x 2 1000 100513 4096 Sep 1 09:31 My Folder").unwrap(); + assert_eq!(entry.name, "My Folder"); + assert!(entry.is_dir); + } + + #[test] + fn ls_parser_strips_symlink_target_from_name() { + let entry = + parse_ls_line("lrwxrwxrwx 1 1000 100513 11 Aug 29 12:00 current -> releases/v2") + .unwrap(); + assert_eq!(entry.name, "current"); + assert!(entry.is_symlink); + } + + #[test] + fn ls_commands_request_numeric_owner_and_group() { + assert_eq!( + list_dir_command("/home/user/My Folder"), + "LC_ALL=C ls -la -n -- '/home/user/My Folder'" + ); + assert_eq!( + stat_command("/home/user/My Folder"), + "LC_ALL=C ls -lad -n -- '/home/user/My Folder'" + ); + } #[test] fn ls_properties_parser_keeps_relative_symlink_target() {