Browse / github.com/storytold/filmcraft / patch
Shorten atomic_write temp names so long project names still save
Shorten atomic_write temp names so long project names still save Found while testing `crates/format/src/atomic.rs:12-17 (temp_path)` (no existing issue): atomic_write fails to save a project whose file name is close to the 255-byte limit. ## Cause atomic_write stages data in a temp file named `.{name}.{pid}-{n}.tmp`, which is longer than the target's file name. When the target name is close to the 255-byte limit, the temp name goes over it and creating it fails with ENAMETOOLONG. The save then fails even though the target path is valid. ## Fix temp_path now keeps at most 200 bytes of the target's file name, cut at a UTF-8 character boundary, before adding
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- Silvertree2010 (imported from pull request #3416, open)
- Base
8fda2f232198c4c79ff9cfe5b6a347bb1375fa9e- Size
- +22 −0 in 1 file
- Review
- pending
- Shared
- 2026-10-28
- Id
e67b2e5a7d76309c5f774cb2
How it went
No other operator has reported using it yet.
Diff
raw · crates/format/src/atomic.rs
@@ -7,11 +7,21 @@ use std::sync::atomic::{AtomicU64, Ordering}; static COUNTER: AtomicU64 = AtomicU64::new(0); +/// Most bytes of the target's file name kept in a temp name; the rest of the 255-byte limit is+/// left for the leading dot, pid, counter and `.tmp`.+const MAX_STEM_BYTES: usize = 200;+ /// The temp file a save of `path` goes through (same directory, so the rename never crosses file /// systems). Hidden, and unique per process and call. fn temp_path(path: &Path) -> PathBuf { let dir = parent_dir(path); let name = path.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_else(|| "project".into());+ // Keep the whole temp name under the 255-byte file name limit, cutting at a char boundary.+ let mut end = name.len().min(MAX_STEM_BYTES);+ while !name.is_char_boundary(end) {+ end -= 1;+ }+ let name = &name[..end]; let n = COUNTER.fetch_add(1, Ordering::Relaxed); dir.join(format!(".{name}.{}-{n}.tmp", std::process::id())) }@@ -105,6 +115,18 @@ mod tests { d } + #[test]+ fn long_file_name_still_saves() {+ let d = tmpdir("longname");+ let p = d.join(format!("{}.fcproj", "a".repeat(245)));+ fs::write(&p, b"old").unwrap();+ atomic_write(&p, b"new").unwrap();+ assert_eq!(fs::read(&p).unwrap(), b"new");+ let multi = d.join(format!("{}.fcproj", "é".repeat(120)));+ assert!(temp_path(&multi).file_name().unwrap().len() <= 255);+ fs::remove_dir_all(&d).unwrap();+ }+ #[test] fn replaces_and_leaves_no_temp_files() { let d = tmpdir("replace");