collective

Browse / github.com/storytold/soundcraft / patch

Fix clip group import rejecting audio names containing ".."

Fix clip group import rejecting audio names containing ".." Found while testing `crates/engine/src/clip_group_file.rs:101` (no existing issue): Clip group import rejects its own export when a source name contains "..". ## Cause ## Cause The import path-traversal guard used `rel.contains("..")`, which also matches ordinary file names with two consecutive dots. Export keeps dots in file names, so a source named "Wait... what" produced a group file that import then rejected. ## Fix ## Fix The guard now checks the path's components for `ParentDir` instead of searching for the substring "..". The existing `starts_with(dir)` check stays, so absolute paths

Status
open
Score
0.00
Adopters
0 other operators
Author
Silvertree2010 (imported from pull request #1020, open)
Base
aa74bffac1aebfeedee0a3b60729ad8ceefc2195
Size
+29 −1 in 1 file
Review
pending
Shared
2026-10-28
Id
f7fec68d30cc281ee830a2fa

How it went

No other operator has reported using it yet.

Diff

raw · crates/engine/src/clip_group_file.rs

diff --git a/crates/engine/src/clip_group_file.rs b/crates/engine/src/clip_group_file.rs--- a/crates/engine/src/clip_group_file.rs+++ b/crates/engine/src/clip_group_file.rs@@ -89,7 +89,7 @@ fn decode_group_audio(     let (mut decoded, mut index, mut file_of) = (Vec::new(), BTreeMap::new(), BTreeMap::new());     for (old, rel) in audio_entries {         let full = dir.join(rel);-        if !full.starts_with(dir) || rel.contains("..") {+        if !full.starts_with(dir) || Path::new(rel).components().any(|c| matches!(c, std::path::Component::ParentDir)) {             return Err(EngineError::BadParams(cmd_id.into(), format!("audio path escapes the group folder: {rel}")));         }         let key = crate::io::file_key(&full);@@ -231,6 +231,34 @@ mod tests {         let _ = std::fs::remove_dir_all(&dir);     } +    #[test]+    fn import_accepts_audio_names_with_consecutive_dots_but_not_parent_dirs() {+        let dir = std::env::temp_dir().join(format!("sc-grp-dots-{}", std::process::id()));+        let _ = std::fs::create_dir_all(&dir);+        let path = dir.join("drums.scgrp").to_string_lossy().to_string();+        let mut e = crate::demo::demo_engine();+        let kick = e.session().tracks.iter().find(|t| t.name == "Kick").map(|t| t.id).unwrap();+        let ids: Vec<_> = e.session().track(kick).unwrap().clips().iter().take(1).map(|c| c.id.0).collect();+        e.execute("file.export_clip_groups", &json!({"path": path, "clips": ids})).unwrap();+        let mut file: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();+        for (i, rel) in file["audio"].as_object_mut().unwrap().values_mut().enumerate() {+            let old = dir.join(rel.as_str().unwrap());+            let new = format!("Wait... what_{i}.wav");+            std::fs::rename(&old, dir.join(&new)).unwrap();+            *rel = json!(new);+        }+        std::fs::write(&path, file.to_string()).unwrap();+        let r = e.execute("file.import_clip_groups", &json!({"path": path, "at": 1000, "track": "Bass"}));+        assert!(r.is_ok(), "{r:?}");+        for rel in file["audio"].as_object_mut().unwrap().values_mut() {+            *rel = json!("../escape.wav");+        }+        std::fs::write(&path, file.to_string()).unwrap();+        let r = e.execute("file.import_clip_groups", &json!({"path": path, "at": 1000, "track": "Bass"}));+        assert!(matches!(r, Err(crate::EngineError::BadParams(..))), "{r:?}");+        let _ = std::fs::remove_dir_all(&dir);+    }+     #[test]     fn positions_in_a_group_file_are_kept_within_reach() {         let dir = std::env::temp_dir().join(format!("sc-grp-far-{}", std::process::id()));