use crate::{Confidence, Detail::Ebml, DocType, Evidence, Outcome};
const EBML_MAGIC: [u8; 4] = [0x1A, 0x45, 0xDF, 0xA3];
const VINT_MAX_WIDTH: usize = 8;
const ID_DOC_TYPE: [u8; 2] = [0x42, 0x82];
const DOC_TYPE_WEBM: &str = "webm";
const DOC_TYPE_MATROSKA: &str = "matroska";
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() < EBML_MAGIC.len() {
return Outcome::Insufficient(EBML_MAGIC.len());
}
if region[..EBML_MAGIC.len()] != EBML_MAGIC {
return Outcome::None;
}
match find_doc_type(region) {
DocTypeResult::Found(doc_type) => Outcome::Match(Evidence {
confidence: Confidence::CERTAIN,
detail: Ebml { doc_type },
}),
DocTypeResult::Truncated(need) => Outcome::Insufficient(need),
DocTypeResult::Malformed => Outcome::None,
DocTypeResult::Absent => Outcome::Match(Evidence {
confidence: Confidence::STRONG,
detail: Ebml {
doc_type: DocType::Other,
},
}),
}
}
#[derive(Debug, PartialEq, Eq)]
enum DocTypeResult {
Found(DocType),
Truncated(usize),
Malformed,
Absent,
}
#[derive(Debug, PartialEq, Eq)]
enum VintRead<T> {
Ok(usize, T),
Truncated,
Malformed,
}
fn find_doc_type(region: &[u8]) -> DocTypeResult {
let cursor = EBML_MAGIC.len(); let (len, size) = match read_size_vint(®ion[cursor..]) {
VintRead::Ok(w, v) => (w, v),
VintRead::Truncated => {
return DocTypeResult::Truncated(cursor.saturating_add(VINT_MAX_WIDTH));
}
VintRead::Malformed => return DocTypeResult::Malformed,
};
let body_start = cursor + len; let (data, bounded) = if let Some(sz) = size {
match body_start.checked_add(sz) {
Some(e) if e <= region.len() => (®ion[body_start..e], true),
Some(e) => return DocTypeResult::Truncated(e),
None => return DocTypeResult::Malformed,
}
} else {
(®ion[body_start..], false)
};
let mut off = 0usize;
while off < data.len() {
let (id_len, id) = match read_id_vint(&data[off..]) {
VintRead::Ok(w, v) => (w, v),
VintRead::Truncated => return cut_short(bounded, body_start, off),
VintRead::Malformed => return DocTypeResult::Malformed,
};
let after_id = off + id_len;
let (esz_len, esz) = match read_size_vint(&data[after_id..]) {
VintRead::Ok(w, v) => (w, v),
VintRead::Truncated => return cut_short(bounded, body_start, after_id),
VintRead::Malformed => return DocTypeResult::Malformed,
};
let value_start = after_id + esz_len;
let value_end = match esz {
Some(sz) => {
match value_start.checked_add(sz) {
Some(end) if end <= data.len() => end,
Some(end) if !bounded => {
return DocTypeResult::Truncated(body_start.saturating_add(end));
}
Some(_) => return DocTypeResult::Malformed,
None => return DocTypeResult::Malformed,
}
}
None => data.len(),
};
if id == ID_DOC_TYPE {
let value = &data[value_start..value_end];
let text = match core::str::from_utf8(value) {
Ok(t) => t,
Err(_) => return DocTypeResult::Absent,
};
return DocTypeResult::Found(match text {
DOC_TYPE_WEBM => DocType::Webm,
DOC_TYPE_MATROSKA => DocType::Matroska,
_ => DocType::Other,
});
}
off = value_end;
}
DocTypeResult::Absent
}
fn cut_short(bounded: bool, body_start: usize, off: usize) -> DocTypeResult {
if bounded {
DocTypeResult::Malformed
} else {
DocTypeResult::Truncated(
body_start
.saturating_add(off)
.saturating_add(VINT_MAX_WIDTH),
)
}
}
fn read_id_vint(b: &[u8]) -> VintRead<&[u8]> {
let Some(&first) = b.first() else {
return VintRead::Truncated;
};
let Some(width) = vint_width(first) else {
return VintRead::Malformed;
};
if b.len() < width {
return VintRead::Truncated;
}
VintRead::Ok(width, &b[..width])
}
fn read_size_vint(b: &[u8]) -> VintRead<Option<usize>> {
let Some(&first) = b.first() else {
return VintRead::Truncated;
};
let Some(width) = vint_width(first) else {
return VintRead::Malformed;
};
if b.len() < width {
return VintRead::Truncated;
}
let marker: u8 = 1u8 << (8 - width);
let bits = width * 7;
let mut v: u64 = u64::from(first & !marker);
for &byte in &b[1..width] {
v = (v << 8) | u64::from(byte);
}
let max: u64 = (1u64 << bits) - 1;
if v == max {
VintRead::Ok(width, None)
} else {
match usize::try_from(v) {
Ok(sz) => VintRead::Ok(width, Some(sz)),
Err(_) => VintRead::Malformed,
}
}
}
fn vint_width(first: u8) -> Option<usize> {
let width = first.leading_zeros() as usize + 1;
if width <= VINT_MAX_WIDTH {
Some(width)
} else {
None
}
}
#[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 width_8_size_vint_does_not_panic() {
let input: [u8; 12] = [
0x1A, 0x45, 0xDF, 0xA3, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
let p = crate::probe(&input);
assert!(matches!(
p,
crate::Probe::Identified {
confidence: crate::Confidence::STRONG,
..
}
));
}
#[test]
fn every_vint_width_decodes_without_panic() {
for width in 1..=8u32 {
let first = 1u8 << (8 - width);
let mut buf = [0u8; 8];
buf[0] = first;
let VintRead::Ok(got_width, size) = read_size_vint(&buf) else {
panic!("width {width} must decode, got {:?}", read_size_vint(&buf))
};
assert_eq!(got_width, width as usize);
assert_eq!(size, Some(0));
}
}
#[test]
fn short_magic_prefix_is_insufficient() {
let data = fixture_bytes("fixtures/mkv/h264_aac.mkv");
let region = &data[..EBML_MAGIC.len() - 1];
match probe(region, region.len()) {
Outcome::Insufficient(need) => assert_eq!(need, EBML_MAGIC.len()),
other => panic!("3-byte EBML prefix must be Insufficient(4), got {other:?}"),
}
}
#[test]
fn a_child_overrunning_a_declared_body_is_ruled_out_not_truncated() {
let mut buf = std::vec![0x1A, 0x45, 0xDF, 0xA3, 0x8A, 0x42, 0x82, 0x40, 0xC8];
buf.resize(300, 0x00);
assert_eq!(
find_doc_type(&buf),
DocTypeResult::Malformed,
"a child declaring 200 bytes inside a declared 10-byte body is illegal; \
more bytes cannot make it legal, so this must not be a truncation"
);
}
#[test]
fn a_truncated_body_reports_its_declared_end_not_the_buffer_length() {
let seed = std::vec![
0x1A, 0x45, 0xDF, 0xA3, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64
];
for len in [12usize, 16, 32, 64, 100, 111] {
let mut buf = seed.clone();
buf.resize(len, 0x00);
assert_eq!(
find_doc_type(&buf),
DocTypeResult::Truncated(112),
"at {len} bytes the need must stay the declared end (112), not track \
the {len} supplied"
);
}
}
#[test]
fn an_illegal_vint_marker_terminates_instead_of_asking_forever() {
for len in [8usize, 64, 1024, 65536] {
let mut buf = std::vec::Vec::with_capacity(len);
buf.extend_from_slice(&EBML_MAGIC);
buf.resize(len, 0x00);
match probe(&buf, buf.len()) {
Outcome::None => {}
Outcome::Insufficient(need) => panic!(
"EBML magic + 0x00 padding can never become a valid header, so it must \
be ruled out; at {len} bytes it instead asked for {need} -- a caller \
obeying that reads forever"
),
other => panic!("expected Outcome::None at {len} bytes, got {other:?}"),
}
}
}
#[test]
fn a_truncated_legal_header_still_asks_for_more() {
let full = std::fs::read(std::format!(
"{}/../fixtures/mkv/h264_aac.mkv",
env!("CARGO_MANIFEST_DIR")
))
.expect("fixture");
let prefix = &full[..12];
match probe(prefix, prefix.len()) {
Outcome::Insufficient(need) => assert!(
need > prefix.len(),
"need_at_least {need} must exceed the {} bytes supplied",
prefix.len()
),
other => panic!("a truncated real MKV header must be Insufficient, got {other:?}"),
}
}
}