diff --git a/crates/audio-io/src/wav.rs b/crates/audio-io/src/wav.rs index ee93cd46..532c2460 100644 --- a/crates/audio-io/src/wav.rs +++ b/crates/audio-io/src/wav.rs @@ -342,7 +342,10 @@ pub(crate) fn read_channel_mask(b: &[u8]) -> Option { fn put_fixed_str(out: &mut Vec, s: &str, len: usize) { let bytes = s.as_bytes(); - let n = bytes.len().min(len); + let mut n = bytes.len().min(len); + while !s.is_char_boundary(n) { + n -= 1; + } out.extend_from_slice(bytes.get(..n).unwrap_or_default()); out.resize(out.len() + (len - n), 0); } diff --git a/crates/audio-io/tests/it/roundtrip.rs b/crates/audio-io/tests/it/roundtrip.rs index 70042966..77cd2b69 100644 --- a/crates/audio-io/tests/it/roundtrip.rs +++ b/crates/audio-io/tests/it/roundtrip.rs @@ -316,3 +316,13 @@ fn float_wav_header_rejects_a_frame_count_whose_total_size_overflows() { let frames = u64::MAX / 4; assert!(matches!(wav_float_header(1, 48_000, frames), Err(soundcraft_audio_io::AudioError::TooLarge(_)))); } + +#[test] +fn bwf_truncation_keeps_utf8_boundaries() { + let src = sine(48_000, 1, 10); + let bwf = BwfInfo { originator: format!("{}é", "a".repeat(31)), description: format!("{}é", "d".repeat(255)), ..BwfInfo::default() }; + let bytes = encode(&src, &EncodeOptions { format: FileFormat::Wav, bwf: Some(bwf), ..EncodeOptions::default() }).unwrap(); + let out = probe(&bytes, None).unwrap().bwf.unwrap(); + assert_eq!(out.originator, "a".repeat(31)); + assert_eq!(out.description, "d".repeat(255)); +}