Skip to content

Commit 771c354

Browse files
committed
xm: add a sanity check
1 parent 6f4da08 commit 771c354

File tree

3 files changed

+39
-6
lines changed

3 files changed

+39
-6
lines changed

src/load/format/xm.rs

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ const FLAG_STEREO: u8 = 1 << 5;
2727
const INSTRUMENT_SIZE: u32 = 263;
2828
const MINIMUM_INSTRUMENT_SIZE: u32 = 29;
2929

30-
const PADDING_LIMIT: u32 = 2 * 1024 * 1024;
30+
const PADDING_LIMIT: u32 = 44100 * 10; // TODO
31+
3132
const ADPCM_COMPRESSION_TABLE_SIZE: u32 = 16;
3233

3334
/// Determine if given bytes could be an Extended Module.
@@ -187,7 +188,7 @@ pub fn load(buffer: Vec<u8>, source: Option<PathBuf>) -> Result<Module, Error> {
187188
// See: Page 16 in "The Unofficial XM File Format Specification"
188189
let length_bytes = match smp.pcm_type == PcmType::ADPCM {
189190
true => ADPCM_COMPRESSION_TABLE_SIZE + ((smp.length + 1) / 2),
190-
_ => smp.length,
191+
false => smp.length,
191192
};
192193

193194
// Apparently, it is common for samples to report their sizes beyond what the file can store.
@@ -198,25 +199,29 @@ pub fn load(buffer: Vec<u8>, source: Option<PathBuf>) -> Result<Module, Error> {
198199
//
199200
// We need to add extra padding as loop points may point to them.
200201
if smp.pointer + length_bytes > buffer.len() as u32 {
201-
extra_padding = buffer.len() as u32 - smp.pointer + length_bytes;
202-
samples.push(smp);
202+
extra_padding = (smp.pointer + length_bytes) - buffer.len() as u32;
203+
204+
if extra_padding < PADDING_LIMIT {
205+
samples.push(smp);
206+
}
203207

204208
break 'parse_instrument;
205209
}
206210

207211
file.skip_bytes(length_bytes as i64)?;
208-
209212
samples.push(smp);
210213
}
211214
}
212215

216+
sanity_check_samples(&mut samples);
217+
213218
samples
214219
};
215220

216221
let mut buffer = buffer;
217222

218223
if extra_padding > 0 {
219-
let new_len = buffer.len() + extra_padding.clamp(0, PADDING_LIMIT) as usize;
224+
let new_len = buffer.len() + extra_padding as usize;
220225
buffer.resize(new_len, 0);
221226
info!("Padded last sample with {} extra bytes", extra_padding);
222227
}
@@ -232,3 +237,19 @@ pub fn load(buffer: Vec<u8>, source: Option<PathBuf>) -> Result<Module, Error> {
232237
samples: samples.into(),
233238
})
234239
}
240+
241+
/// HACK: Removes some really cursed samples. I really need a better heuristic.
242+
/// For "xenia3.xm", xmodits reports last sample to be 5Hz and has a duration of ~34.5 hours.
243+
fn sanity_check_samples(samples: &mut Vec<Sample>) {
244+
use std::time::Duration;
245+
246+
samples.retain(|smp| {
247+
let less_than_15_mins =
248+
Duration::from_secs_f32(smp.length_frames() as f32 / smp.rate as f32)
249+
< Duration::from_secs(60 * 15);
250+
251+
let sensible_sample_rate = (256..=384_000).contains(&smp.rate);
252+
253+
sensible_sample_rate && less_than_15_mins
254+
})
255+
}

tests/modules/xm/xenia3.xm

1.59 MB
Binary file not shown.

tests/test_xm.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,18 @@ check_sample_number! {
2424
with: 26
2525
}
2626

27+
// This module is cursed.
28+
//
29+
// Openmpt reports 7 samples, but the last sample is complete and utter garbage.
30+
// Attempting to save samples with it will fail on the last one.
31+
//
32+
// source: https://modarchive.org/module.php?193712
33+
check_sample_number! {
34+
test_xm_cursed_sample,
35+
path: include_bytes!("modules/xm/xenia3.xm"),
36+
with: 6
37+
}
38+
2739
// An ordinary xm file. Mainly for sanity checks.
2840
//
2941
// source: https://modarchive.org/module.php?191384

0 commit comments

Comments
 (0)