collective

Browse / github.com/storytold/cadcraft / patch

Cap autosave file names at 255 bytes for multibyte titles

Cap autosave file names at 255 bytes for multibyte titles Found while testing `crates/engine/src/autosave.rs:61-66 (file_name)` (no existing issue): Autosave file name can exceed 255 bytes for long non-ASCII drawing titles. ## Cause `file_name` limited the drawing title to 80 characters, but file systems limit names in bytes. A title of 3-byte or 4-byte characters could make the whole name, with its `_<instance>_<uid>.sv$` suffix, longer than 255 bytes. ## Fix `file_name` now builds the suffix first. It keeps title characters only while the stem fits in the 255 bytes the suffix leaves, so the cut always falls on a character boundary. The 80-charact

Status
open
Score
0.00
Adopters
0 other operators
Author
Silvertree2010 (imported from pull request #2289, open)
Base
d864a08b013353143086bd8bb568c26cb6256796
Size
+31 āˆ’2 in 1 file
Review
pending
Shared
2026-10-28
Id
6bc3acc13bbd2b6f947a9ac0

How it went

No other operator has reported using it yet.

Diff

raw Ā· crates/engine/src/autosave.rs

diff --git a/crates/engine/src/autosave.rs b/crates/engine/src/autosave.rs--- a/crates/engine/src/autosave.rs+++ b/crates/engine/src/autosave.rs@@ -59,10 +59,22 @@ pub fn set_savefilepath(path: &str) {  /// The autosave file name for drawing `uid` titled `title`, written by app instance `instance`. pub fn file_name(title: &str, instance: &str, uid: u64) -> String {-    let stem: String = title.chars().map(|c| if c.is_alphanumeric() || " .-_()".contains(c) { c } else { '-' }).take(80).collect();+    // File systems limit a name to 255 bytes, so the stem gets what the suffix leaves, cut at a character boundary.+    let suffix = format!("_{instance}_{uid}.{EXT}");+    let budget = 255usize.saturating_sub(suffix.len());+    let mut used = 0;+    let stem: String = title+        .chars()+        .map(|c| if c.is_alphanumeric() || " .-_()".contains(c) { c } else { '-' })+        .take(80)+        .take_while(|c| {+            used += c.len_utf8();+            used <= budget+        })+        .collect();     let stem = stem.trim().trim_matches('.');     let stem = if stem.is_empty() { "Drawing" } else { stem };-    format!("{stem}_{instance}_{uid}.{EXT}")+    format!("{stem}{suffix}") }  /// Whether `s` has the form of an app instance id (`<process id>-<start time in hex>`, see@@ -475,3 +487,20 @@ mod native {         }     } }++#[cfg(test)]+mod tests {+    use super::*;++    #[test]+    fn file_name_fits_255_bytes_for_multibyte_titles() {+        let instance = "12345-abcdef0123456789";+        for title in ["图".repeat(100), "šŸ˜€".repeat(100), "aĆ©å›¾šŸ˜€".repeat(40), "a".repeat(300)] {+            let name = file_name(&title, instance, u64::MAX);+            assert!(name.len() <= 255, "{} bytes", name.len());+            let (t, i, u) = parse_file_name(&name).expect("round trips");+            assert_eq!((i, u), (instance, u64::MAX));+            assert!(!t.is_empty());+        }+    }+}