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
6 changes: 4 additions & 2 deletions src/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -605,9 +605,11 @@ fn write_file_atomic(full: &Path, root: &Path, contents: &[u8], executable: bool
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| "file".to_string());
// Truncate suffix if name is long to prevent ENAMETOOLONG
// Truncate suffix if name is long to prevent ENAMETOOLONG. Floor to a
// char boundary: a byte index of 128 can land inside a multi-byte char
// and panic the whole update mid-materialization.
let suffix = if full_name.len() > 128 {
&full_name[..128]
&full_name[..full_name.floor_char_boundary(128)]
} else {
&full_name
};
Expand Down
48 changes: 48 additions & 0 deletions tests/update_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,54 @@ fn test_update_branch_with_slashes() {
let _ = std::fs::remove_dir_all(&tmp);
}

#[test]
fn test_update_long_unicode_filename_does_not_panic() {
let tmp = unique_tmp("unicode-name");
let _ = std::fs::remove_dir_all(&tmp);
let proj = tmp.join("proj");
std::fs::create_dir_all(&proj).unwrap();
assert!(oot(&["init"], &proj).0);

std::fs::write(proj.join("a.txt"), "v1\n").unwrap();
let (ok, msg) = oot(&["record", "--branch", "main", "-m", "first"], &proj);
assert!(ok, "{msg}");

// The long name lives on another branch, so update has to write it.
// 127 ASCII bytes then e-acute at bytes 127..129, so byte index 128
// lands inside the character.
let long_name = format!("{}\u{e9}", "a".repeat(127));
assert!(long_name.len() > 128);
assert!(!long_name.is_char_boundary(128));
std::fs::write(proj.join(&long_name), "payload\n").unwrap();
let (ok, msg) = oot(&["record", "--branch", "feat", "-m", "long name"], &proj);
assert!(ok, "{msg}");
std::fs::remove_file(proj.join(&long_name)).unwrap();

let (ok, out) = oot(&["update", "--branch", "feat"], &proj);
assert!(
ok,
"update must not panic on a long unicode filename: {out}"
);
assert_eq!(
std::fs::read_to_string(proj.join(&long_name)).unwrap_or_default(),
"payload\n",
"the long-named file must land on disk: {out}"
);
assert_eq!(std::fs::read_to_string(proj.join("a.txt")).unwrap(), "v1\n");

// Same path under --force.
let (ok, out) = oot(&["update", "--branch", "main", "--force"], &proj);
assert!(ok, "{out}");
let (ok, out) = oot(&["update", "--branch", "feat", "--force"], &proj);
assert!(ok, "forced update must not panic either: {out}");
assert_eq!(
std::fs::read_to_string(proj.join(&long_name)).unwrap(),
"payload\n"
);

let _ = std::fs::remove_dir_all(&tmp);
}

#[test]
fn test_update_3way_conflict_preserves_work() {
let tmp = unique_tmp("3way-conflict");
Expand Down
Loading