diff --git a/crates/midi/src/smf.rs b/crates/midi/src/smf.rs index 442e27f0f..fb6683a30 100644 --- a/crates/midi/src/smf.rs +++ b/crates/midi/src/smf.rs @@ -189,13 +189,14 @@ pub const MAX_WARNINGS: usize = 32; /// short is not believed when the track's End of Track says otherwise: the next chunk is looked /// for where the track's events ended; /// - bytes that are no chunk are skipped up to the next `MTrk`, and trailing bytes are ignored; -/// - past [`MAX_TRACKS`] tracks the rest is left out; +/// - past [`MAX_TRACKS`] tracks the rest is left out; this and the event limit set +/// [`LenientRead::over_limit`]; /// - lyrics are counted in a warning (they are not kept); a format-2 file gets a warning that its /// tracks are read side by side. pub fn read_smf_lenient(bytes: &[u8]) -> Result { let mut warnings = Vec::new(); let (smf, extra) = read(bytes, Some(&mut warnings))?; - Ok(LenientRead { smf, warnings, instruments: extra.instruments, end: extra.end }) + Ok(LenientRead { smf, warnings, instruments: extra.instruments, end: extra.end, over_limit: extra.over_limit }) } /// What [`read_smf_lenient`] makes of a file. @@ -209,12 +210,18 @@ pub struct LenientRead { pub instruments: Vec, /// The last tick any track reaches: its End of Track, or where its events stop. pub end: i64, + /// Whether the file goes past [`MAX_EVENTS`] events or [`MAX_TRACKS`] tracks, so what lies past + /// the limit was left out. It is set even when the warnings are already full, so a caller that + /// refuses such files (as [`read_smf`] does) can tell without reading the warnings. + pub over_limit: bool, } -/// What the reader finds beyond [`Smf`]: instrument names per kept track, and the end tick. +/// What the reader finds beyond [`Smf`]: instrument names per kept track, the end tick, and +/// whether a limit left part of the file out. struct Extra { instruments: Vec, end: i64, + over_limit: bool, } fn warn(warnings: &mut Option<&mut Vec>, what: String) { @@ -262,6 +269,7 @@ fn read(bytes: &[u8], mut warnings: Option<&mut Vec>) -> Result<(Smf, Ex let (mut tracks, mut instruments) = (Vec::new(), Vec::new()); let mut event_budget = MAX_EVENTS; let (mut end, mut seen, mut lyrics) = (0i64, 0usize, 0usize); + let mut over_limit = false; while !cur.is_empty() { let chunk_at = cur.at(); @@ -306,6 +314,7 @@ fn read(bytes: &[u8], mut warnings: Option<&mut Vec>) -> Result<(Smf, Ex return Err(MidiError::TooManyTracks(MAX_TRACKS)); } warn(&mut warnings, format!("more than {MAX_TRACKS} tracks: the rest was left out")); + over_limit = true; break; } let (body_at, body_start) = (cur.at(), cur.pos); @@ -319,7 +328,10 @@ fn read(bytes: &[u8], mut warnings: Option<&mut Vec>) -> Result<(Smf, Ex end = end.max(read.end); match read.stop { Some(e) if !lenient => return Err(e), - Some(e) => warn(&mut warnings, format!("track {seen} {}: what came before is kept", stop_reason(&e))), + Some(e) => { + over_limit |= matches!(e, MidiError::TooManyEvents(_)); + warn(&mut warnings, format!("track {seen} {}: what came before is kept", stop_reason(&e))); + } None if !read.ended && lenient => warn(&mut warnings, format!("track {seen} has no End of Track: what came before is kept")), None => {} } @@ -363,7 +375,7 @@ fn read(bytes: &[u8], mut warnings: Option<&mut Vec>) -> Result<(Smf, Ex Ok(( Smf { format, tracks, tempos: globals.tempos, meters: globals.meters, markers: globals.markers, key_sigs: globals.key_sigs }, - Extra { instruments, end }, + Extra { instruments, end, over_limit }, )) } diff --git a/crates/midi/tests/it/lenient.rs b/crates/midi/tests/it/lenient.rs index 3d02995bc..0556c3859 100644 --- a/crates/midi/tests/it/lenient.rs +++ b/crates/midi/tests/it/lenient.rs @@ -33,8 +33,8 @@ const TWO: &[u8] = &[0x00, 0x91, 62, 100, 0x60, 0x81, 62, 0]; #[test] fn a_sound_file_reads_the_same_either_way_without_warnings() { let good = file(1, 96, &[ONE, TWO]); - let LenientRead { smf, warnings, end, .. } = read_smf_lenient(&good).unwrap(); - assert_eq!((keys(&smf), warnings.len()), (vec![vec![60], vec![62]], 0)); + let LenientRead { smf, warnings, end, over_limit, .. } = read_smf_lenient(&good).unwrap(); + assert_eq!((keys(&smf), warnings.len(), over_limit), (vec![vec![60], vec![62]], 0, false)); assert_eq!(smf, read_smf(&good).unwrap()); assert_eq!(end, 960, "the End of Track tick, at 960 ticks a quarter"); } @@ -122,15 +122,34 @@ fn a_file_of_any_size_is_read_into_a_bounded_number_of_events_and_warnings() { // Three bytes a note-on under running status: more events than are read. let mut body = vec![0x00, 0x90, 60, 100]; body.extend((0..MAX_EVENTS + 1000).flat_map(|i| [0x00, (i % 128) as u8, 1])); - let LenientRead { smf, warnings, .. } = read_smf_lenient(&file(1, 96, &[&body])).unwrap(); + let LenientRead { smf, warnings, over_limit, .. } = read_smf_lenient(&file(1, 96, &[&body])).unwrap(); assert_eq!(smf.tracks[0].sequence.notes.len(), MAX_EVENTS); assert!(warnings.iter().any(|w| w.contains("more events")), "{warnings:?}"); + assert!(over_limit, "the event limit left events out"); // Tracks of nothing, by the thousand: the count stops the reading. - let LenientRead { smf, warnings, .. } = read_smf_lenient(&file(1, 96, &vec![&[][..]; MAX_TRACKS + 5])).unwrap(); + let LenientRead { smf, warnings, over_limit, .. } = read_smf_lenient(&file(1, 96, &vec![&[][..]; MAX_TRACKS + 5])).unwrap(); assert!(smf.tracks.len() < MAX_TRACKS && warnings.iter().any(|w| w.contains("tracks"))); - // Every track damaged: never more than MAX_WARNINGS words. - let LenientRead { warnings, .. } = read_smf_lenient(&file(1, 96, &vec![&[0x00, 60][..]; 100])).unwrap(); + assert!(over_limit, "the track limit left tracks out"); + // Every track damaged: never more than MAX_WARNINGS words, and no limit was reached. + let LenientRead { warnings, over_limit, .. } = read_smf_lenient(&file(1, 96, &vec![&[0x00, 60][..]; 100])).unwrap(); assert_eq!(warnings.len(), MAX_WARNINGS); + assert!(!over_limit); +} + +#[test] +fn a_limit_reached_after_the_warnings_are_full_is_still_reported() { + // MAX_WARNINGS damaged tracks fill the warnings; the track after them holds more events than are + // read, so its warning is dropped, and over_limit is what says the file was cut. + let mut body = vec![0x00, 0x90, 60, 100]; + body.extend((0..MAX_EVENTS + 1000).flat_map(|i| [0x00, (i % 128) as u8, 1])); + let mut tracks = vec![&[0x00, 60][..]; MAX_WARNINGS]; + tracks.push(&body); + let LenientRead { warnings, over_limit, .. } = read_smf_lenient(&file(1, 96, &tracks)).unwrap(); + assert_eq!(warnings.len(), MAX_WARNINGS); + assert!(!warnings.iter().any(|w| w.contains("more events")), "the test needs the limit's warning dropped: {warnings:?}"); + assert!(over_limit); + // The strict reader stops at the first damaged track, before the limit. + assert!(!matches!(read_smf(&file(1, 96, &tracks)), Err(MidiError::TooManyEvents(_)) | Ok(_))); } #[test]