From 7ca978fdb95cfc27cae47e7eadbad5db3b2a1c70 Mon Sep 17 00:00:00 2001 From: grovebit <337789029+grovebit@users.noreply.github.com> Date: Mon, 12 Oct 2026 15:36:00 +0100 Subject: [PATCH] Cut names from files to a length that can be drawn A MIDI file whose track name was a million characters long imported fine, and the next frame crashed the app: drawing that name needed a 320 MB vertex buffer, past wgpu's 256 MB limit, and the panic comes from painting, outside anything that catches it. Saved, the session crashed again on every open. A 4 MB status line in ui.json did the same at every launch. Names read from files are now cut to 256 characters and comments to 4096: everything Session::sanitize repairs on load (session, track, playlist, clip, marker, bus, output, group, source, video and key names), MIDI track names on import and clip names from clip group files. The status bar draws at most 1024 characters. --- crates/engine/src/clip_group_file.rs | 1 + crates/engine/src/io.rs | 14 ++++++++- crates/model/src/lib.rs | 5 ++- crates/model/src/session.rs | 46 ++++++++++++++++++++++++++++ crates/ui-egui/src/edit_window.rs | 3 +- 5 files changed, 66 insertions(+), 3 deletions(-) diff --git a/crates/engine/src/clip_group_file.rs b/crates/engine/src/clip_group_file.rs index 66735f9b..e60beedd 100644 --- a/crates/engine/src/clip_group_file.rs +++ b/crates/engine/src/clip_group_file.rs @@ -144,6 +144,7 @@ pub fn import(e: &mut Engine, path: &str, first: Option, at: Samples) - continue; } c.id = s.new_clip_id(); + soundcraft_model::truncate_chars(&mut c.name, soundcraft_model::MAX_NAME_CHARS); c.start = at.saturating_add(scale(c.start.max(0))); c.length = scale(c.length).max(1); c.group = Some(group_id); diff --git a/crates/engine/src/io.rs b/crates/engine/src/io.rs index f1bf6a49..a99f7cc6 100644 --- a/crates/engine/src/io.rs +++ b/crates/engine/src/io.rs @@ -203,7 +203,8 @@ pub fn import_midi_bytes(e: &mut Engine, bytes: &[u8], at: Samples, tempo_map: b if tr.sequence.notes.is_empty() { continue; } - let name = if tr.name.trim().is_empty() { format!("MIDI {}", i + 1) } else { tr.name.clone() }; + let mut name = if tr.name.trim().is_empty() { format!("MIDI {}", i + 1) } else { tr.name.clone() }; + soundcraft_model::truncate_chars(&mut name, soundcraft_model::MAX_NAME_CHARS); let id = s.add_track(TrackKind::Instrument, ChannelFormat::Stereo, Some(&name)); let drums = tr.sequence.notes.iter().all(|n| n.channel == 9); if let Some(t) = s.track_mut(id) { @@ -1265,6 +1266,17 @@ mod rate_tests { mod name_tests { use super::*; + #[test] + fn a_midi_track_name_is_cut_to_a_length_that_can_be_drawn() { + let note = soundcraft_midi::Note { pitch: 60, velocity: 100, release_velocity: 64, channel: 0, start: 0, length: 960 }; + let sequence = soundcraft_midi::Sequence { notes: vec![note], ..Default::default() }; + let smf = soundcraft_midi::Smf { tracks: vec![soundcraft_midi::SmfTrack { name: "n".repeat(1_000_000), sequence }], ..Default::default() }; + let mut e = Engine::default(); + let ids = import_midi_bytes(&mut e, &soundcraft_midi::write_smf(&smf).unwrap(), 0, false).unwrap(); + let track = e.session().track(ids[0]).unwrap(); + assert_eq!((track.name.len(), track.clips()[0].name.len()), (soundcraft_model::MAX_NAME_CHARS, soundcraft_model::MAX_NAME_CHARS)); + } + #[test] fn new_sources_take_the_first_free_file_name() { let mut s = Session::default(); diff --git a/crates/model/src/lib.rs b/crates/model/src/lib.rs index 55d605d0..ec8aaa2c 100644 --- a/crates/model/src/lib.rs +++ b/crates/model/src/lib.rs @@ -22,7 +22,10 @@ pub use markers::{Group, MarkerKind, MemoryLocation}; pub use mixer::{ Bus, ChannelFormat, INSERT_SLOTS, Insert, Mixer, OutputPath, Route, SEND_SLOTS, SendSlot, Speaker, SurroundPan, fader_db_to_pos, fader_pos_to_db, }; -pub use session::{BitDepthSetting, EditMode, EditState, SESSION_EXTENSION, Session, SessionError, Tool, TrackView, ZoomState}; +pub use session::{ + BitDepthSetting, EditMode, EditState, MAX_COMMENT_CHARS, MAX_NAME_CHARS, SESSION_EXTENSION, Session, SessionError, Tool, TrackView, ZoomState, + truncate_chars, +}; pub use source::{Source, SourceAudio, SourcePool, VideoSource}; pub use track::{Playlist, TRACK_COLORS, Track, TrackHeight, TrackKind}; diff --git a/crates/model/src/session.rs b/crates/model/src/session.rs index 9d384f5b..a3f906c3 100644 --- a/crates/model/src/session.rs +++ b/crates/model/src/session.rs @@ -575,6 +575,39 @@ impl Session { self.sample_rate = SampleRate::default(); } self.tempo.sanitize(); + truncate_chars(&mut self.name, MAX_NAME_CHARS); + truncate_chars(&mut self.comments, MAX_COMMENT_CHARS); + for t in &mut self.tracks { + truncate_chars(&mut t.name, MAX_NAME_CHARS); + truncate_chars(&mut t.comments, MAX_COMMENT_CHARS); + for p in &mut t.playlists { + truncate_chars(&mut p.name, MAX_NAME_CHARS); + p.clips.iter_mut().for_each(|c| truncate_chars(&mut c.name, MAX_NAME_CHARS)); + } + } + for m in &mut self.markers { + truncate_chars(&mut m.name, MAX_NAME_CHARS); + truncate_chars(&mut m.comments, MAX_COMMENT_CHARS); + } + self.key_signatures.iter_mut().for_each(|(_, k)| truncate_chars(k, MAX_NAME_CHARS)); + self.busses.iter_mut().for_each(|b| truncate_chars(&mut b.name, MAX_NAME_CHARS)); + self.outputs.iter_mut().for_each(|o| truncate_chars(&mut o.name, MAX_NAME_CHARS)); + self.groups.iter_mut().for_each(|g| truncate_chars(&mut g.name, MAX_NAME_CHARS)); + self.sources.iter_mut().for_each(|s| truncate_chars(&mut s.name, MAX_NAME_CHARS)); + self.videos.iter_mut().for_each(|v| truncate_chars(&mut v.name, MAX_NAME_CHARS)); + } +} + +/// The most characters kept of a name read from a file: longer ones can't be shown, and drawing +/// a name of a million characters overflows the renderer's buffers. +pub const MAX_NAME_CHARS: usize = 256; +/// The most characters kept of a comment read from a file. +pub const MAX_COMMENT_CHARS: usize = 4096; + +/// Cut `s` to at most `max` characters. +pub fn truncate_chars(s: &mut String, max: usize) { + if let Some((end, _)) = s.char_indices().nth(max) { + s.truncate(end); } } @@ -657,6 +690,19 @@ mod tests { assert_eq!(s.unique_track_name("Audio"), "Audio 1"); } + #[test] + fn names_from_a_file_are_cut_to_a_length_that_can_be_drawn() { + let mut s = Session::default(); + let t = s.add_track(TrackKind::Audio, ChannelFormat::Mono, None); + if let Some(tr) = s.track_mut(t) { + tr.name = "é".repeat(1_000); + } + s.comments = "c".repeat(10_000); + let back = Session::from_json(&s.to_json().unwrap()).unwrap(); + assert_eq!(back.track(t).map(|tr| tr.name.chars().count()), Some(MAX_NAME_CHARS)); + assert_eq!(back.comments.len(), MAX_COMMENT_CHARS); + } + #[test] fn a_bad_sample_rate_or_tempo_is_repaired_on_load() { let mut v = serde_json::to_value(Session::default()).unwrap(); diff --git a/crates/ui-egui/src/edit_window.rs b/crates/ui-egui/src/edit_window.rs index f13bc78a..dc2453b5 100644 --- a/crates/ui-egui/src/edit_window.rs +++ b/crates/ui-egui/src/edit_window.rs @@ -157,7 +157,7 @@ fn status_bar(app: &mut SoundApp, ui: &mut Ui) { let t = Tokens::current(); let r = ui.max_rect(); let s = app.engine.session(); - let left = format!( + let mut left = format!( "{} · {} Hz · {} · {} tracks · {}", s.name, s.sample_rate.hz(), @@ -169,6 +169,7 @@ fn status_bar(app: &mut SoundApp, ui: &mut Ui) { s.tracks.len(), app.ui.status ); + soundcraft_model::truncate_chars(&mut left, 4 * soundcraft_model::MAX_NAME_CHARS); ui.painter().text(pos2(r.min.x + 10.0, r.center().y), Align2::LEFT_CENTER, left, regular(11.0), t.text_dim); let undo = app.engine.undo_label().map_or(String::new(), |l| format!("Undo {l}")); ui.painter().text(