mirror of
https://github.com/freemkv/libfreemkv.git
synced 2026-10-07 18:13:10 +00:00
disc/bdnav: audit round-1 — VM title/PSR faithfulness + main_title never-panics
From the /codeaudit HEAD..v1.6.10 round (each verified against the libbluray HDMV VM reference, and design-refuted SOUND on two models): - bdnav/vm.rs title_obj: resolve First Play via the spec sentinel 0xFFFF (was num_titles+1). A JumpTitle 0xFFFF now reaches First Play, and an out-of-range JumpTitle(N+1) correctly abstains instead of mis-jumping there. - bdnav/vm.rs wr: refuse stores to PSR-tagged registers (matches _store_reg), so an illegal SET/SWAP to a PSR is a no-op and later compares agree with a real player instead of diverging. - disc/profile.rs main_title(): return Option<&TitleProfile> instead of indexing titles[0], which panicked on a disc that scanned to zero titles. Regression tests added, each shown red-before-green.
This commit is contained in:
+59
-5
@@ -76,9 +76,10 @@ impl Vm<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn wr(&mut self, val: u32, x: u32) {
|
fn wr(&mut self, val: u32, x: u32) {
|
||||||
if val & PSR_FLAG != 0 {
|
// A store to a PSR-tagged register is refused (libbluray `_store_reg`
|
||||||
self.psr[(val & 0x7f) as usize] = x;
|
// returns -1); only GPR writes take effect. PSR values come from the
|
||||||
} else {
|
// system (power-on `psr_init`), never from a nav SET/SWAP operand.
|
||||||
|
if val & PSR_FLAG == 0 {
|
||||||
self.gpr[(val & 0xfff) as usize] = x;
|
self.gpr[(val & 0xfff) as usize] = x;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,14 +87,16 @@ impl Vm<'_> {
|
|||||||
if imm { raw } else { self.rd(raw) }
|
if imm { raw } else { self.rd(raw) }
|
||||||
}
|
}
|
||||||
/// Resolve a JumpTitle target: title 0 = Top Menu, `1..=N` = `titles[i-1]`,
|
/// Resolve a JumpTitle target: title 0 = Top Menu, `1..=N` = `titles[i-1]`,
|
||||||
/// `N+1` = First Play (BD spec title numbering).
|
/// `0xFFFF` = First Play (BD-ROM title numbering, per libbluray
|
||||||
|
/// `_is_valid_title`). Any other value is an invalid title reference and
|
||||||
|
/// abstains (`None`).
|
||||||
fn title_obj(&self, title: u32) -> Option<PlaybackObj> {
|
fn title_obj(&self, title: u32) -> Option<PlaybackObj> {
|
||||||
let n = self.index.titles.len() as u32;
|
let n = self.index.titles.len() as u32;
|
||||||
if title == 0 {
|
if title == 0 {
|
||||||
Some(self.index.top_menu)
|
Some(self.index.top_menu)
|
||||||
} else if title <= n {
|
} else if title <= n {
|
||||||
self.index.titles.get((title - 1) as usize).copied()
|
self.index.titles.get((title - 1) as usize).copied()
|
||||||
} else if title == n + 1 {
|
} else if title == 0xFFFF {
|
||||||
Some(self.index.first_play)
|
Some(self.index.first_play)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -336,6 +339,57 @@ mod tests {
|
|||||||
assert_eq!(resolve(&index, &mobjs, &|_| true), None);
|
assert_eq!(resolve(&index, &mobjs, &|_| true), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn jumptitle_first_play_is_0xffff_not_n_plus_1() {
|
||||||
|
// BD-ROM title numbering (libbluray `_is_valid_title`): 0 = Top Menu,
|
||||||
|
// 0xFFFF = First Play, 1..=N = titles. `N+1` is an INVALID title ref.
|
||||||
|
let index = idx(
|
||||||
|
PlaybackObj::Hdmv { id_ref: 7 },
|
||||||
|
vec![PlaybackObj::Hdmv { id_ref: 1 }],
|
||||||
|
);
|
||||||
|
let vm = Vm {
|
||||||
|
mobjs: &[],
|
||||||
|
index: &index,
|
||||||
|
gpr: [0; 4096],
|
||||||
|
psr: psr_init(),
|
||||||
|
};
|
||||||
|
assert_eq!(vm.title_obj(0), Some(index.top_menu), "title 0 = Top Menu");
|
||||||
|
assert_eq!(
|
||||||
|
vm.title_obj(1),
|
||||||
|
Some(PlaybackObj::Hdmv { id_ref: 1 }),
|
||||||
|
"title 1 = titles[0]"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
vm.title_obj(0xFFFF),
|
||||||
|
Some(PlaybackObj::Hdmv { id_ref: 7 }),
|
||||||
|
"0xFFFF = First Play (abstained wrongly before the fix)"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
vm.title_obj(2),
|
||||||
|
None,
|
||||||
|
"N+1 is an invalid title ref, not First Play (resolved wrongly before the fix)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_to_psr_is_refused() {
|
||||||
|
// A SET MOVE to PSR6 must be a no-op (libbluray `_store_reg` refuses PSR
|
||||||
|
// stores). Program: MOVE PSR6 <- 5; CMP PSR6 == 5; PlayPL 100 else 200.
|
||||||
|
// With the write refused, PSR6 stays at its power-on 0, so the compare is
|
||||||
|
// false and control skips to PlayPL 200. If the illegal PSR write took
|
||||||
|
// effect, the compare would be true and PlayPL 100 would win.
|
||||||
|
let set_psr6 = cmd((2 << 5) | (2 << 3), 0x40, 0, 0x01, 0x8000_0006, 5);
|
||||||
|
let cmp_psr6_eq_5 = cmd((2 << 5) | (1 << 3), 0x40, 0x02, 0, 0x8000_0006, 5);
|
||||||
|
let d = build(&[&[set_psr6, cmp_psr6_eq_5, play_pl(100), play_pl(200)]]);
|
||||||
|
let mobjs = mobj::parse(&d).unwrap();
|
||||||
|
let index = idx(PlaybackObj::Hdmv { id_ref: 0 }, vec![]);
|
||||||
|
assert_eq!(
|
||||||
|
resolve(&index, &mobjs, &|id| id == 100 || id == 200),
|
||||||
|
Some(200),
|
||||||
|
"PSR store must be refused, so the compare is false and 200 is reached"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn register_fold_and_cmp_skip_reach_playpl() {
|
fn register_fold_and_cmp_skip_reach_playpl() {
|
||||||
// SET GPR[3]=7; CMP GPR[3]==7 (true → do NOT skip); PlayPL GPR[3].
|
// SET GPR[3]=7; CMP GPR[3]==7 (true → do NOT skip); PlayPL GPR[3].
|
||||||
|
|||||||
+16
-5
@@ -268,10 +268,12 @@ impl DiscProfile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The selected main-feature title. Panics only on an empty disc (no
|
/// The selected main-feature title, or `None` when the disc scanned to zero
|
||||||
/// titles), which the scan never produces for a real disc.
|
/// titles (a data-only image with no /BDMV, /HVDVD_TS or /VIDEO_TS). Never
|
||||||
pub fn main_title(&self) -> &TitleProfile {
|
/// panics — mirrors the `Option`-returning main-feature accessors elsewhere
|
||||||
&self.titles[self.main_title]
|
/// (`titles.first()`, `dvdnav::resolve_main_title`).
|
||||||
|
pub fn main_title(&self) -> Option<&TitleProfile> {
|
||||||
|
self.titles.get(self.main_title)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,6 +318,15 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn main_title_is_none_on_empty_disc() {
|
||||||
|
// A data-only image (no /BDMV, /HVDVD_TS, /VIDEO_TS) scans to zero
|
||||||
|
// titles; main_title() must return None, not panic on titles[0].
|
||||||
|
let profile = DiscProfile::from_disc(&test_disc());
|
||||||
|
assert!(profile.titles.is_empty());
|
||||||
|
assert!(profile.main_title().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
fn video(codec: Codec, res: Resolution, secondary: bool) -> Stream {
|
fn video(codec: Codec, res: Resolution, secondary: bool) -> Stream {
|
||||||
Stream::Video(VideoStream {
|
Stream::Video(VideoStream {
|
||||||
pid: 0x1011,
|
pid: 0x1011,
|
||||||
@@ -469,7 +480,7 @@ mod tests {
|
|||||||
assert!(profile.titles[0].is_main, "titles[0] is the main feature");
|
assert!(profile.titles[0].is_main, "titles[0] is the main feature");
|
||||||
assert!(!profile.titles[1].is_main);
|
assert!(!profile.titles[1].is_main);
|
||||||
assert_eq!(profile.titles[1].index, 1);
|
assert_eq!(profile.titles[1].index, 1);
|
||||||
assert_eq!(profile.main_title().playlist, "00800.mpls");
|
assert_eq!(profile.main_title().unwrap().playlist, "00800.mpls");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user