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>
This commit is contained in:
2026-04-20 12:19:28 +09:00
co-authored by Copilot
parent 1d81216c97
commit 0da6a10898
4 changed files with 11 additions and 10 deletions
+4 -2
View File
@@ -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<dyn std::error::Error>> {
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()
+2 -1
View File
@@ -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 {
+3 -6
View File
@@ -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());
+2 -1
View File
@@ -21,7 +21,8 @@ pub async fn mv(src_path: &str, dest_path: &str) -> Result<(), Box<dyn std::erro
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 {