collective

Browse / github.com/storytold/soundcraft / patch

track.delete: keep a folder's input bus when surviving tracks route through it

[Audit] Deleting a folder deletes a bus it does not own Found in a security and robustness audit of `main` at 9b8283b (0.11.0). **Code:** [`crates/engine/src/cmd/track.rs:547-553, 639-697`](https://github.com/storytold/soundcraft/blob/9b8283b90877813a929f5e28b2dc92e5774da933/crates/engine/src/cmd/track.rs#L547-L553) - **Severity:** Low-Medium (the data change is undoable, but easy to miss before saving). **Confidence:** Confirmed by code. - **Evidence:** `crates/engine/src/cmd/track.rs:547-553`: ```rust fn owned_folder_bus(s: &Session, folder: TrackId) -> Option<BusId> { let folder = s.track(folder).filter(|track| track.kind ==

Status
open
Score
0.00
Adopters
0 other operators
Author
CloudCorpRecords (imported from pull request #1026, open)
Base
7dc56a4dbeebf57eacece09536726c57844fc59d
Size
+39 −1 in 1 file
Review
pending
Shared
2026-10-28
Id
5099c274d152ce65c92abbe4

How it went

No other operator has reported using it yet.

Diff

raw · crates/engine/src/cmd/track.rs

diff --git a/crates/engine/src/cmd/track.rs b/crates/engine/src/cmd/track.rs--- a/crates/engine/src/cmd/track.rs+++ b/crates/engine/src/cmd/track.rs@@ -590,6 +590,20 @@ fn owned_folder_bus(s: &Session, folder: TrackId) -> Option<soundcraft_model::Bu     } } +/// Whether a bus is still routed through by a track that survives deleting+/// `deleted`, ignoring the output slots of `folder`'s own members, which the+/// delete reroutes to the parent folder's bus. A folder owns its input bus+/// only when this is false: `track.input` or a session file can point a+/// folder at a bus that other tracks already use (#914).+fn bus_used_beyond_folder(s: &Session, bus: soundcraft_model::BusId, deleted: &[TrackId], folder: TrackId) -> bool {+    let route = Route::Bus(bus);+    s.tracks.iter().filter(|track| !deleted.contains(&track.id)).any(|track| {+        track.mixer.input == route+            || track.mixer.sends.iter().flatten().any(|send| send.target == route)+            || (track.mixer.output == route && !in_folder(s, track.id, folder))+    })+}+ /// Keep only the selected roots so selecting a folder and one of its children preserves the subtree. fn root_tracks(s: &Session, tracks: Vec<TrackId>) -> Vec<TrackId> {     tracks.iter().copied().filter(|track| !tracks.iter().any(|root| root != track && in_folder(s, *track, *root))).collect()@@ -708,7 +722,11 @@ fn delete(e: &mut Engine, p: &Value) -> Result<Value> {         .filter_map(|folder| {             let bus = owned_folder_bus(s, *folder)?;             let parent = surviving_parent(s, &tracks, *folder);-            Some((bus, parent.and_then(|parent| folder_bus(s, parent)).unwrap_or(Route::Main)))+            let replacement = parent.and_then(|parent| folder_bus(s, parent)).unwrap_or(Route::Main);+            // A bus that surviving tracks route through is shared, not the+            // folder's own: keep it, and every route to it, when the folder+            // goes (#914).+            (!bus_used_beyond_folder(s, bus, &tracks, *folder)).then_some((bus, replacement))         })         .collect();     // Promote children of deleted folders and repair only routes that used the deleted@@ -1129,6 +1147,26 @@ mod tests {         assert!(e.session().bus(outer_bus).is_some(), "the surviving parent folder's bus must remain");     } +    #[test]+    fn deleting_a_folder_keeps_a_bus_other_tracks_still_use() {+        let mut e = crate::demo::demo_engine();+        // #914: the demo session's Verb bus is shared: an aux takes it as its input+        // and several tracks send to it. Point a new folder's input at it, the way+        // `track.input` or a session file can, then delete the folder.+        let verb = e.session().bus_by_name("Verb").unwrap().id;+        let folder = e.execute("track.new", &json!({"kind": "folder", "name": "Shared"})).unwrap();+        let folder = TrackId(folder["tracks"][0].as_u64().unwrap());+        e.execute("track.input", &json!({"tracks": [folder.0], "input": "Verb"})).unwrap();+        assert_eq!(e.session().track(folder).unwrap().mixer.input, Route::Bus(verb));+        e.execute("track.delete", &json!({"tracks": [folder.0]})).unwrap();+        assert!(e.session().track(folder).is_none(), "the folder itself is deleted");+        assert!(e.session().bus(verb).is_some(), "a bus other tracks route through must survive");+        let aux = e.session().track_by_name("Verb").unwrap();+        assert_eq!(aux.mixer.input, Route::Bus(verb), "the aux input must keep the bus");+        let snare = e.session().track_by_name("Snare").unwrap();+        assert!(snare.mixer.sends.iter().flatten().any(|send| send.target == Route::Bus(verb)), "sends to the shared bus must survive");+    }+     #[test]     fn heights_take_points_and_step_through_the_presets() {         let mut e = crate::demo::demo_engine();