use crate::{Confidence, Detail, Evidence, Outcome};
const ID3_SIGNATURE: [u8; 3] = *b"ID3";
const ID3_HEADER_LEN: usize = 10;
const ID3_FOOTER_LEN: usize = 10;
const ID3_FLAG_FOOTER: u8 = 0x10;
const ID3_SYNCSAFE_MASK: u8 = 0x7F;
const MP3_SYNC_B1_MASK: u8 = 0xE0;
const MP3_VERSION_MPEG1: u8 = 3;
const MP3_LAYER_III: u8 = 1;
const FRAME_LENGTH_COEFF: u32 = 144;
const MP3_MIN_FRAME_LEN: usize = 4;
const MP3_HEADER_LEN: usize = 4;
const BITRATE_KILO_FACTOR: u32 = 1000;
const MP3_MIN_CHAIN_WEAK: usize = 4;
const MP3_MIN_CHAIN_STRONG: usize = 16;
const BITRATE_KBPS: [u16; 16] = [
0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0,
];
const SAMPLE_RATE_MPEG1: [u32; 3] = [44_100, 48_000, 32_000];
pub(crate) fn probe(data: &[u8], limit: usize) -> Outcome {
debug_assert!(limit <= data.len(), "harness caps limit at data.len()");
let region = &data[..limit];
if region.len() < MP3_HEADER_LEN {
return Outcome::Insufficient(MP3_HEADER_LEN);
}
let (longest, truncated, anchor, frame_len) = longest_mp3_chain(region);
if longest >= MP3_MIN_CHAIN_STRONG {
return Outcome::Match(Evidence {
confidence: Confidence::LATTICE_STRONG,
detail: Detail::None,
});
}
if longest >= MP3_MIN_CHAIN_WEAK {
return Outcome::Match(Evidence {
confidence: Confidence::LATTICE_WEAK,
detail: Detail::None,
});
}
if longest == 0 {
if let Some(skip) = id3_skip(region)
&& skip > region.len()
{
return Outcome::Insufficient(skip);
}
return Outcome::None;
}
if truncated {
Outcome::Insufficient(need_at_least(anchor, frame_len))
} else {
Outcome::None
}
}
fn need_at_least(anchor: usize, frame_len: usize) -> usize {
anchor.saturating_add(MP3_MIN_CHAIN_WEAK.saturating_mul(frame_len))
}
fn longest_mp3_chain(data: &[u8]) -> (usize, bool, usize, usize) {
let n = data.len();
let mut best = 0usize;
let mut best_truncated = false;
let mut best_anchor = 0usize;
let mut best_frame_len = 0usize;
let mut i = id3_skip(data).unwrap_or(0);
while i < n {
if let Some(first_len) = mp3_frame_len(data, i) {
let mut p = i;
let mut run = 0usize;
let mut truncated = false;
loop {
match mp3_frame_len(data, p) {
Some(l) => {
run += 1;
if run >= MP3_MIN_CHAIN_STRONG {
return (run, false, i, first_len);
}
if p + l > n {
truncated = true;
break;
}
p += l;
}
None => {
if p + MP3_HEADER_LEN > n {
truncated = true;
}
break;
}
}
}
if run > best || (run == best && truncated) {
best = run;
best_truncated = truncated;
best_anchor = i;
best_frame_len = first_len;
}
i += 1;
} else {
i += 1;
}
}
(best, best_truncated, best_anchor, best_frame_len)
}
fn mp3_frame_len(data: &[u8], i: usize) -> Option<usize> {
if i + 3 >= data.len() {
return None;
}
let b0 = data[i];
let b1 = data[i + 1];
let b2 = data[i + 2];
let b3 = data[i + 3];
if b0 != 0xFF || b1 & MP3_SYNC_B1_MASK != MP3_SYNC_B1_MASK {
return None;
}
let version = (b1 >> 3) & 0x03;
let layer = (b1 >> 1) & 0x03;
if version != MP3_VERSION_MPEG1 || layer != MP3_LAYER_III {
return None;
}
let br_idx = (b2 >> 4) & 0x0F;
let sr_idx = (b2 >> 2) & 0x03;
if usize::from(sr_idx) >= SAMPLE_RATE_MPEG1.len() {
return None;
}
let bitrate = BITRATE_KBPS[br_idx as usize];
if bitrate == 0 {
return None; }
let padding = (b3 >> 1) & 0x01;
let len = (FRAME_LENGTH_COEFF * u32::from(bitrate) * BITRATE_KILO_FACTOR)
/ SAMPLE_RATE_MPEG1[sr_idx as usize]
+ u32::from(padding);
let len = len as usize;
if len < MP3_MIN_FRAME_LEN {
return None;
}
Some(len)
}
fn id3_skip(data: &[u8]) -> Option<usize> {
if data.len() < ID3_HEADER_LEN || data[..ID3_SIGNATURE.len()] != ID3_SIGNATURE {
return None;
}
let flags = data[5];
let size = ((u32::from(data[6]) & u32::from(ID3_SYNCSAFE_MASK)) << 21)
| ((u32::from(data[7]) & u32::from(ID3_SYNCSAFE_MASK)) << 14)
| ((u32::from(data[8]) & u32::from(ID3_SYNCSAFE_MASK)) << 7)
| (u32::from(data[9]) & u32::from(ID3_SYNCSAFE_MASK));
let footer = if flags & ID3_FLAG_FOOTER != 0 {
ID3_FOOTER_LEN as u32
} else {
0
};
Some(ID3_HEADER_LEN + size as usize + footer as usize)
}
#[cfg(test)]
mod tests {
use super::*;
fn fixture_bytes(rel: &str) -> std::vec::Vec<u8> {
std::fs::read(std::format!("{}/../{}", env!("CARGO_MANIFEST_DIR"), rel))
.unwrap_or_else(|e| panic!("failed to read {rel}: {e}"))
}
#[test]
fn chain_threshold_keeps_a_container_out() {
let data = fixture_bytes("fixtures/ts/h264_aac.ts");
match probe(&data, data.len()) {
Outcome::None => {}
other => panic!("h264_aac.ts must NOT match MP3 at the real threshold, got {other:?}"),
}
}
#[test]
fn audio_mp3_id3_skip_is_correct() {
let data = fixture_bytes("fixtures/container-probe/audio.mp3");
let skip = id3_skip(&data).expect("audio.mp3 is ID3-prefixed");
assert_eq!(skip, 45);
assert!(
mp3_frame_len(&data, skip).is_some(),
"a frame must follow the tag"
);
match probe(&data, data.len()) {
Outcome::Match(ev) => {
assert_eq!(ev.confidence, Confidence::LATTICE_STRONG);
}
other => panic!("audio.mp3 must identify as Mp3, got {other:?}"),
}
}
#[test]
fn short_prefix_is_insufficient() {
let data = fixture_bytes("fixtures/container-probe/audio.mp3");
let region = &data[..MP3_HEADER_LEN - 1];
match probe(region, region.len()) {
Outcome::Insufficient(need) => assert_eq!(need, MP3_HEADER_LEN),
other => panic!("3-byte MP3 prefix must be Insufficient(4), got {other:?}"),
}
}
#[test]
fn id3_tag_longer_than_the_region_is_insufficient() {
let data = [
b'I', b'D', b'3', 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, ];
match probe(&data, data.len()) {
Outcome::Insufficient(need) => assert_eq!(need, 137),
other => panic!("ID3 tag longer than the region must be Insufficient, got {other:?}"),
}
}
}