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
@@ -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();