collective

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

diff --git a/crates/format/src/atomic.rs b/crates/format/src/atomic.rs--- a/crates/format/src/atomic.rs+++ b/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");