From 0da6a108986b055754c75b58aea615078091d206 Mon Sep 17 00:00:00 2001 From: Yoshihiro OKUMURA Date: Mon, 20 Apr 2026 12:19:28 +0900 Subject: [PATCH] fix: ensure NFC normalization is applied consistently - api/files.rs: NFC-normalize filename before sending to server in upload_file(). On macOS, local filenames may be NFD-encoded, which would cause the server to store them as NFD instead of NFC. - commands/download.rs: replace direct to_lowercase() subfolder comparison with find_subfolder_by_name() helper, which already applies NFC normalization on both sides. - commands/cp.rs, mv.rs: apply nfc() to s_basename (source path component from user input) for consistency with d_basename, so the no-op identity check and find_*() calls use normalized strings. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/api/files.rs | 6 ++++-- src/commands/cp.rs | 3 ++- src/commands/download.rs | 9 +++------ src/commands/mv.rs | 3 ++- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/api/files.rs b/src/api/files.rs index a15d3d2..3d853e9 100644 --- a/src/api/files.rs +++ b/src/api/files.rs @@ -1,5 +1,6 @@ use crate::connection::MDRSConnection; pub use crate::models::file::File; +use unicode_normalization::UnicodeNormalization; #[derive(serde::Deserialize)] struct FileListResponse { @@ -40,11 +41,12 @@ impl MDRSConnection { file_path: &str, ) -> Result<(), Box> { use reqwest::multipart; - let file_name = std::path::Path::new(file_path) + let file_name: String = std::path::Path::new(file_path) .file_name() .unwrap() .to_string_lossy() - .to_string(); + .nfc() + .collect(); let file_bytes = tokio::fs::read(file_path).await?; let part = multipart::Part::bytes(file_bytes).file_name(file_name.clone()); let form = multipart::Form::new() diff --git a/src/commands/cp.rs b/src/commands/cp.rs index 3872993..bf0d1d3 100644 --- a/src/commands/cp.rs +++ b/src/commands/cp.rs @@ -25,7 +25,8 @@ pub async fn cp( let lab_id = lab.id; // Split source path into parent directory and target name - let (s_dirname, s_basename) = split_path(&s_path); + let (s_dirname, s_basename_raw) = split_path(&s_path); + let s_basename = nfc(&s_basename_raw); // If dest ends with '/', treat it as a directory and preserve src basename let (d_dirname, d_basename_raw) = if dest_ends_with_slash { diff --git a/src/commands/download.rs b/src/commands/download.rs index 706ff76..74817b8 100644 --- a/src/commands/download.rs +++ b/src/commands/download.rs @@ -1,6 +1,6 @@ use crate::commands::shared::{ - create_authenticated_conn, find_file_by_name, find_folder, find_lab_in_cache, load_cache_with_token_refresh, - parse_remote_path, + create_authenticated_conn, find_file_by_name, find_folder, find_lab_in_cache, + find_subfolder_by_name, load_cache_with_token_refresh, parse_remote_path, }; use crate::connection::MDRSConnection; use futures::stream::{FuturesUnordered, StreamExt}; @@ -66,10 +66,7 @@ pub async fn download( } // Case 2: basename matches a sub-folder. - let subfolder = parent_folder - .sub_folders - .iter() - .find(|f| f.name.to_lowercase() == basename.to_lowercase()); + let subfolder = find_subfolder_by_name(&parent_folder.sub_folders, &basename); if let Some(sub) = subfolder { if !recursive { return Err(format!("Cannot download `{}`: Is a folder.", r_path_clean).into()); diff --git a/src/commands/mv.rs b/src/commands/mv.rs index 106c4c8..f39497f 100644 --- a/src/commands/mv.rs +++ b/src/commands/mv.rs @@ -21,7 +21,8 @@ pub async fn mv(src_path: &str, dest_path: &str) -> Result<(), Box