bdnav: warn on dropped MPLS, dedupe be_u16, pin SET taint test

This commit is contained in:
Matthew Jackson
2026-09-29 19:36:34 -07:00
parent 5a6b5617e6
commit 07d1c7aa1e
5 changed files with 41 additions and 14 deletions
+2 -3
View File
@@ -3,6 +3,8 @@
//! documented binary format, never executed: every field is bounds-checked and
//! any malformed input yields `None` (the nav resolver then abstains).
use super::be_u16;
/// One playback/title object in `index.bdmv`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum PlaybackObj {
@@ -29,9 +31,6 @@ const OBJ_LEN: usize = 12;
/// Sanity cap on the title count (real discs have well under this).
const MAX_TITLES: usize = 4096;
fn be_u16(d: &[u8], o: usize) -> Option<u16> {
Some(u16::from_be_bytes([*d.get(o)?, *d.get(o + 1)?]))
}
fn be_u32(d: &[u8], o: usize) -> Option<u32> {
Some(u32::from_be_bytes([
*d.get(o)?,
+2 -4
View File
@@ -7,6 +7,8 @@
//! byte 50. Each object is `flags`(1) + reserved(1) + `num_cmds`(u16) followed
//! by `num_cmds` 12-byte navigation commands.
use super::be_u16;
// One decoded 12-byte navigation command.
#[derive(Debug, Clone, Copy)]
pub(crate) struct Cmd {
@@ -36,10 +38,6 @@ const CMD_LEN: usize = 12;
/// separate cap would be dead code.
const MAX_OBJECTS: usize = 4096;
fn be_u16(d: &[u8], o: usize) -> Option<u16> {
Some(u16::from_be_bytes([*d.get(o)?, *d.get(o + 1)?]))
}
/// Decode one 12-byte command. Caller guarantees `b.len() == 12`.
fn decode_cmd(b: &[u8]) -> Cmd {
let (b0, b1, b2, b3) = (b[0], b[1], b[2], b[3]);
+4
View File
@@ -14,6 +14,10 @@ pub(crate) mod vm;
use crate::sector::SectorSource;
use crate::udf::UdfFs;
pub(super) fn be_u16(d: &[u8], o: usize) -> Option<u16> {
Some(u16::from_be_bytes([*d.get(o)?, *d.get(o + 1)?]))
}
// Resolve the playlist id First-Play navigation plays as the feature, among ids the caller
// marks as feature candidates (`is_feature_candidate`). Returns `None` for BD-J discs,
// malformed nav data, or non-convergence.
+11
View File
@@ -674,6 +674,17 @@ mod tests {
assert_eq!(resolve(&index, &mobjs, &|_| true), None);
}
#[test]
fn set_from_rnd_source_taints_destination() {
// GPR0 = RND; GPR1 (clean) += GPR0 (register src); PlayPL GPR1 must abstain.
let rnd = set_imm(0x08, 0, 10);
let add = cmd((2 << 5) | (2 << 3), 0x00, 0, 0x03, 1, 0);
let d = build(&[&[rnd, add, play_pl_reg(1), play_pl(1)]]);
let mobjs = mobj::parse(&d).unwrap();
let index = idx(PlaybackObj::Hdmv { id_ref: 0 }, vec![]);
assert_eq!(resolve(&index, &mobjs, &|_| true), None);
}
#[test]
fn playpl_operand_over_16_bits_is_not_a_playlist_id() {
// 0x1_0001 must not alias playlist 1; the VM falls through to 800.
+22 -7
View File
@@ -62,8 +62,18 @@ impl Disc {
filename: &str,
data: &[u8],
) -> Result<Option<DiscTitle>> {
let Ok(parsed) = mpls::parse(data) else {
return Ok(None);
let parsed = match mpls::parse(data) {
Ok(p) => p,
Err(e) => {
// Real code, matching the CLPI path: a malformed playlist is
// dropped, but never silently.
tracing::warn!(
target: "freemkv::disc",
playlist = ?filename,
"E{}", e.code()
);
return Ok(None);
}
};
// Calculate duration from play items
@@ -665,11 +675,16 @@ mod tests {
let mut disc = MemDisc::new();
let udf = make_min_fs(&mut disc);
let junk = vec![0u8; 100];
assert!(
Disc::parse_playlist(&mut disc, &udf, "00001.mpls", &junk)
.expect("scan")
.is_none()
);
let (t, events) = crate::testlog::capture(|| {
Disc::parse_playlist(&mut disc, &udf, "00001.mpls", &junk).expect("scan")
});
assert!(t.is_none());
let line = events
.iter()
.find(|e| e.target == "freemkv::disc")
.unwrap_or_else(|| panic!("a dropped playlist must be logged; got {events:?}"));
assert_eq!(line.level, tracing::Level::WARN);
assert_eq!(line.message(), format!("E{}", crate::error::E_MPLS_PARSE));
}
/// Build the minimal fixture: an empty `BDMV/` directory (no STREAM,