bdnav: BC compare per libbluray; labels: camelCase split, deluxe master enums and Rc stack names; ifo: warn on multi-PGC titles; h264/hevc: param sets lead the AU in hierarchy order

This commit is contained in:
Matthew Jackson
2026-10-01 14:27:14 -07:00
parent 08aece29ff
commit 62d56dff5b
6 changed files with 226 additions and 48 deletions
+7 -6
View File
@@ -226,7 +226,7 @@ fn run(vm: &mut Vm, mut obj_id: usize, is_feature: &dyn Fn(u16) -> bool) -> Opti
// CMP — skip the next command when the compare is false.
1 => {
let truth = match c.cmp_opt {
0x01 => (src & !dst) == 0, // BC: every src bit set in dst
0x01 => (dst & !src) == 0, // BC: every dst bit set in src (hdmv_vm.c)
0x02 => dst == src,
0x03 => dst != src,
0x04 => dst >= src,
@@ -625,12 +625,13 @@ mod tests {
}
}
// libbluray INSN_BC: true iff every bit of src is set in dst (src & ~dst == 0).
// libbluray INSN_BC skips the next command when `dst & ~src` is non-zero:
// true iff every bit of dst is set in src.
#[test]
fn cmp_bc_tests_src_bits_within_dst() {
assert!(cmp_picks_true(0x01, 0b1110, 0b0110));
assert!(!cmp_picks_true(0x01, 0b0100, 0b0110));
assert!(!cmp_picks_true(0x01, 0b0110, 0b1110));
fn cmp_bc_tests_dst_bits_within_src() {
assert!(!cmp_picks_true(0x01, 0b1110, 0b0110));
assert!(cmp_picks_true(0x01, 0b0100, 0b0110));
assert!(cmp_picks_true(0x01, 0b0110, 0b1110));
}
#[test]
+69
View File
@@ -815,6 +815,43 @@ fn ptt_srpt_pgc_index(data: &[u8], ptt_offset: usize, ttn: u8) -> Option<usize>
pgcn.checked_sub(1)
}
// Distinct PGCs title `ttn`'s part-of-title entries point at. A title split across
// PGCs (one per chapter) is read from its first PGC only; the caller warns on > 1.
fn ptt_srpt_pgc_count(data: &[u8], ptt_offset: usize, ttn: u8) -> usize {
let entries = || -> Option<Vec<u16>> {
let count = be_u16(data, ptt_offset).ok()? as usize;
let last_byte = be_u32(data, ptt_offset + 4).ok()? as usize;
if ttn as usize > count {
return None;
}
let at = |t: usize| {
be_u32(data, ptt_offset + 8 + (t - 1) * 4)
.ok()
.map(|r| r as usize)
};
let start = at(ttn as usize)?;
let end = if (ttn as usize) < count {
at(ttn as usize + 1)?
} else {
last_byte.checked_add(1)?
};
let mut pgcns = Vec::new();
let mut rel = start;
while rel.checked_add(4)? <= end {
pgcns.push(be_u16(data, ptt_offset.checked_add(rel)?).ok()?);
rel += 4;
}
Some(pgcns)
};
if ptt_offset == 0 || ttn == 0 {
return 0;
}
let mut pgcns = entries().unwrap_or_default();
pgcns.sort_unstable();
pgcns.dedup();
pgcns.len()
}
/// Parse VTS_PGCIT (Program Chain Information Table) to extract titles.
/// `ptt_offset` is the VTS_PTT_SRPT byte offset (0 = absent).
pub(crate) fn parse_pgcit(
@@ -855,6 +892,16 @@ pub(crate) fn parse_pgcit(
continue;
}
let pgcs = ptt_srpt_pgc_count(data, ptt_offset, vts_title_num);
if pgcs > 1 {
tracing::warn!(
target: "freemkv::scan",
vts_title_num,
pgcs,
"title spans several PGCs; only its first PGC is read, the rest of the title is missing"
);
}
let entry_offset = entries_start + pgc_index * 8;
if entry_offset + 8 > data.len() {
skipped += 1;
@@ -2705,6 +2752,28 @@ mod tests {
d
}
#[test]
fn ptt_srpt_counts_the_pgcs_a_title_spans() {
// Title 1: one PTT per PGC (1, 2, 3); title 2: two PTTs in PGC 4.
let entries: [(u16, u16); 5] = [(1, 1), (2, 1), (3, 1), (4, 1), (4, 2)];
let total = 8 + 2 * 4 + entries.len() * 4;
let mut d = vec![0u8; total];
d[0..2].copy_from_slice(&2u16.to_be_bytes());
d[4..8].copy_from_slice(&((total - 1) as u32).to_be_bytes());
d[8..12].copy_from_slice(&16u32.to_be_bytes());
d[12..16].copy_from_slice(&28u32.to_be_bytes());
for (i, (pgcn, pgn)) in entries.iter().enumerate() {
d[16 + i * 4..18 + i * 4].copy_from_slice(&pgcn.to_be_bytes());
d[18 + i * 4..20 + i * 4].copy_from_slice(&pgn.to_be_bytes());
}
assert_eq!(ptt_srpt_pgc_count(&d, 0, 1), 0);
let mut buf = vec![0u8; 4];
buf.extend_from_slice(&d);
assert_eq!(ptt_srpt_pgc_count(&buf, 4, 1), 3);
assert_eq!(ptt_srpt_pgc_count(&buf, 4, 2), 1);
assert_eq!(ptt_srpt_pgc_count(&buf, 4, 3), 0);
}
/// TTN -> PGC goes through VTS_PTT_SRPT, not "TTN N is PGCIT entry N"; the PGCIT
/// sits at a NON-ZERO offset so the pgcit base term is exercised, and the real TTN
/// is stamped on each title (disc/dvd.rs joins on it).
+38 -11
View File
@@ -22,6 +22,7 @@ use super::{LabelPurpose, LabelQualifier, ParseResult, StreamLabel, StreamLabelT
use crate::sector::SectorSource;
use crate::udf::UdfFs;
use std::collections::{BTreeMap, HashMap, HashSet};
use std::rc::Rc;
pub fn detect(reader: &mut dyn SectorSource, udf: &UdfFs) -> bool {
// Real signal is `com/bydeluxe/` in a jar's central directory: a cheap
@@ -548,6 +549,13 @@ fn find_binding_classes(
const MIN_GETSTATIC: usize = 4;
let mut candidates: Vec<(String, usize)> = Vec::new();
jar::for_each_class_budgeted(archive, budget, |class_name, class| {
// A master enum's own <clinit> reads every constant into $VALUES; it is not a binding.
if class
.this_class_name()
.is_some_and(|n| master_enum_classes.contains(n))
{
return;
}
let count = count_master_enum_getstatic(class, master_enum_classes);
if count >= MIN_GETSTATIC {
candidates.push((class_name.to_string(), count));
@@ -618,10 +626,10 @@ pub(crate) enum StackVal {
// Reference to org.bluray.ti.CodingType; field name (e.g.
// DOLBY_AC3_AUDIO) is the codec id — NOT a Deluxe-internal enum, read
// straight from the binding constructor's getstatic operand.
CodingType(String),
CodingType(Rc<str>),
/// An uninitialized `new` object — popped by the matching
/// invokespecial.
NewObj(String),
NewObj(Rc<str>),
/// Anything we can't model — stack effect tracked but content
/// opaque. Lets the walker stay in sync past loads/computed
/// values it doesn't understand.
@@ -898,9 +906,9 @@ impl<'a> BindingDecoder<'a> {
let class_name = insn
.cp_index()
.and_then(|i| self.pool.class_name(i))
.unwrap_or("")
.to_string();
self.push(StackVal::NewObj(class_name));
.unwrap_or("");
// Rc: `dup` copies the handle, not a name of up to 64 KiB.
self.push(StackVal::NewObj(class_name.into()));
}
// Word-level stack ops (a long/double is one two-word value).
POP => drop(self.take_words(insn, 1)),
@@ -943,7 +951,7 @@ impl<'a> BindingDecoder<'a> {
return;
};
let val = if m.class_name == BD_CODING_TYPE_CLASS {
StackVal::CodingType(m.name.to_string())
StackVal::CodingType(m.name.into())
} else if let Some((kind, ordinal)) = self.master.resolve(m.class_name, m.name) {
StackVal::EnumRef { kind, ordinal }
} else {
@@ -988,12 +996,12 @@ impl<'a> BindingDecoder<'a> {
// Underneath the args: the object the constructor
// operates on. For our pattern it's NewObj(X).
match self.stack.pop() {
Some(StackVal::NewObj(name)) if is_init && name == member.class_name => {
Some(StackVal::NewObj(name)) if is_init && *name == *member.class_name => {
// Bounded by MAX_CONSTRUCTIONS: an unbounded push here
// is ~1 GiB reachable from a crafted `<clinit>`.
if !is_container && self.constructions.len() < MAX_CONSTRUCTIONS {
self.constructions.push(Construction {
binding_type: name,
binding_type: name.to_string(),
args,
});
}
@@ -1173,9 +1181,9 @@ fn interpret_streams(constructions: &[Construction], master: &MasterEnumTable) -
StackVal::CodingType(name) if slot.is_none() => {
slot = coding_slot(name);
if slot == Some(CodingSlot::Audio) {
coding_type = Some(name.clone());
coding_type = Some(name.to_string());
} else if slot.is_none() {
unknown_coding = Some(name.clone());
unknown_coding = Some(name.to_string());
}
}
StackVal::Int(n) => {
@@ -1997,6 +2005,25 @@ mod tests {
);
}
// A master enum's own <clinit> getstatics every constant (old-javac $VALUES):
// counting it would set the 40% bar and drop the real binding classes.
#[test]
fn find_binding_classes_skips_the_master_enum_itself() {
let master_classes: HashSet<&str> = ["LanguageEnum"].into_iter().collect();
let lang = class_with_getstatic_refs("LanguageEnum", "LanguageEnum", 70);
let audio = class_with_getstatic_refs("Audio", "LanguageEnum", 6);
let subs = class_with_getstatic_refs("Subs", "LanguageEnum", 8);
let zip = build_zip(&[
("LanguageEnum.class", lang),
("Audio.class", audio),
("Subs.class", subs),
]);
let mut archive = open_jar(zip);
let candidates = find_binding_classes(&mut archive, &master_classes, &mut unbounded());
let names: Vec<&str> = candidates.iter().map(|(n, _)| n.as_str()).collect();
assert_eq!(names, vec!["Subs.class", "Audio.class"]);
}
#[test]
fn find_binding_classes_empty_master_set_yields_no_candidates() {
let master_classes: HashSet<&str> = HashSet::new();
@@ -2623,7 +2650,7 @@ mod tests {
);
for v in &decoder.stack {
match v {
StackVal::NewObj(name) => assert_eq!(name, "AudioSlot"),
StackVal::NewObj(name) => assert_eq!(&**name, "AudioSlot"),
other => panic!("expected NewObj(\"AudioSlot\") x2, got {other:?}"),
}
}
+13 -2
View File
@@ -188,11 +188,13 @@ pub(crate) fn name_words(s: &str) -> Vec<String> {
let mut words = Vec::new();
for part in s.split(|c: char| !c.is_ascii_alphabetic()) {
let mut word = String::new();
// The boundary test reads the source char: `word` holds it lower-cased.
let mut prev_lower = false;
for c in part.chars() {
if c.is_ascii_uppercase() && word.chars().last().is_some_and(|l| l.is_ascii_lowercase())
{
if c.is_ascii_uppercase() && prev_lower {
words.push(std::mem::take(&mut word));
}
prev_lower = c.is_ascii_lowercase();
word.push(c.to_ascii_lowercase());
}
if !word.is_empty() {
@@ -1259,6 +1261,15 @@ pub(crate) fn read_jar_file(
mod registry_tests {
use super::*;
#[test]
fn name_words_splits_camel_case_but_not_all_caps() {
assert_eq!(name_words("MAIN_FEATURE"), ["main", "feature"]);
assert_eq!(name_words("FEATURE_A"), ["feature", "a"]);
assert_eq!(name_words("Feature_TRAILER"), ["feature", "trailer"]);
assert_eq!(name_words("MainFeature_A"), ["main", "feature", "a"]);
assert_eq!(name_words("FeatureHD"), ["feature", "hd"]);
}
fn dir_entry(
name: &str,
is_dir: bool,
+34 -12
View File
@@ -212,9 +212,11 @@ impl CodecParser for H264Parser {
let mut all_vcl_intra = true;
let mut saw_vcl = false;
let mut first_pic_done = false;
// Did this access unit already carry each param-set type in-band?
let mut emitted_sps = false;
let mut emitted_pps = false;
// In-band redefinitions, held aside so they lead the AU in SPS, PPS order:
// a PPS is parsed against the SPS before it, so a re-asserted PPS must
// never precede a redefined SPS.
let mut inband_sps = Vec::new();
let mut inband_pps = Vec::new();
// Pre-sized to input length plus PARAM_REASSERT_HEADROOM so the mux hot
// path avoids repeated reallocs and the keyframe param-set re-assert
// below can be spliced in front without reallocating (see that site).
@@ -232,12 +234,10 @@ impl CodecParser for H264Parser {
// in-band on any change. In MVC passthrough these fall through to
// the default arm so subset SPS/PPS stay in-band (self-contained AU).
NAL_SPS if !mvc => {
emitted_sps |=
handle_param_set(&mut self.sps, &mut self.cur_sps, nal, &mut frame_data)
handle_param_set(&mut self.sps, &mut self.cur_sps, nal, &mut inband_sps);
}
NAL_PPS if !mvc => {
emitted_pps |=
handle_param_set(&mut self.pps, &mut self.cur_pps, nal, &mut frame_data)
handle_param_set(&mut self.pps, &mut self.cur_pps, nal, &mut inband_pps);
}
// Access unit delimiters: drop. Matroska H.264 frame data omits
// AUDs (the container delimits access units), so keeping them
@@ -278,7 +278,7 @@ impl CodecParser for H264Parser {
}
}
if frame_data.is_empty() {
if frame_data.is_empty() && inband_sps.is_empty() && inband_pps.is_empty() {
return Vec::new();
}
@@ -291,11 +291,17 @@ impl CodecParser for H264Parser {
// Every keyframe is self-contained: re-assert active SPS/PPS in-band so a
// decoder that dropped the set recovers, and a stale avcC re-apply can't
// revert it. Skipped per-type only when this AU already carried it.
if keyframe && !mvc {
// revert it. A type this AU redefined goes in-band instead, in its place.
{
let reassert = keyframe && !mvc;
let mut prefix = Vec::with_capacity(PARAM_REASSERT_HEADROOM);
reassert_active(&mut prefix, &self.cur_sps, emitted_sps);
reassert_active(&mut prefix, &self.cur_pps, emitted_pps);
for (inband, cur) in [(&inband_sps, &self.cur_sps), (&inband_pps, &self.cur_pps)] {
if !inband.is_empty() {
prefix.extend_from_slice(inband);
} else if reassert {
reassert_active(&mut prefix, cur, false);
}
}
if !prefix.is_empty() {
// Splice prefix into frame_data in place, sized via
// PARAM_REASSERT_HEADROOM to avoid the extra whole-frame alloc+copy
@@ -1744,6 +1750,22 @@ mod tests {
types
}
// An IDR whose SPS changed but whose PPS did not: the re-asserted PPS must follow
// the new SPS (§7.3.2.2: a PPS is parsed against its SPS).
#[test]
fn reasserted_pps_follows_a_redefined_sps() {
let mut parser = H264Parser::new();
let au = |sps: u8| {
let mut d = vec![0x00, 0x00, 0x01, 0x67, 0x42, 0x00, 0x1E, sps];
d.extend_from_slice(&[0x00, 0x00, 0x01, 0x68, 0x11]);
d.extend_from_slice(&[0x00, 0x00, 0x01, 0x65, 0x10, 0x20]);
d
};
parser.parse(&make_pes(au(0xAA), Some(0)));
let f = parser.parse(&make_pes(au(0xAB), Some(90000)));
assert_eq!(frame_nal_types(&f[0].data), vec![7, 8, 5]);
}
#[test]
fn keyframes_self_contained_and_redefinition_emitted() {
let mut parser = H264Parser::new();
+65 -17
View File
@@ -449,9 +449,11 @@ impl CodecParser for HevcParser {
// Track whether THIS access unit already carried each param-set type
// in-band (a redefinition vs codecPrivate). Used after the scan to
// re-assert the active set at a keyframe the source left bare.
let mut emitted_vps = false;
let mut emitted_sps = false;
let mut emitted_pps = false;
// Redefinitions are held aside so they lead the AU in VPS, SPS, PPS order: a
// re-asserted PPS ahead of a redefined SPS is dropped by the decoder.
let mut inband_vps = Vec::new();
let mut inband_sps = Vec::new();
let mut inband_pps = Vec::new();
// Pre-sized to input length plus PARAM_REASSERT_HEADROOM: UHD frames are
// 150-300 KB and an unsized Vec otherwise reallocs 5-7x per frame; the
// headroom also lets the keyframe param-set re-assert splice in without reallocating.
@@ -493,20 +495,20 @@ impl CodecParser for HevcParser {
match nal_type {
NAL_VPS => {
emitted_vps |= handle_param_set(
handle_param_set(
&mut self.vps,
&mut self.cur_vps,
&data[nal_start..end],
&mut frame_data,
)
&mut inband_vps,
);
}
NAL_SPS => {
emitted_sps |= handle_param_set(
handle_param_set(
&mut self.sps,
&mut self.cur_sps,
&data[nal_start..end],
&mut frame_data,
)
&mut inband_sps,
);
}
NAL_PPS => {
// Store the PPS under its own id so a later slice's
@@ -519,12 +521,12 @@ impl CodecParser for HevcParser {
{
*slot = hevc_num_extra_slice_header_bits(pps);
}
emitted_pps |= handle_param_set(
handle_param_set(
&mut self.pps,
&mut self.cur_pps,
&data[nal_start..end],
&mut frame_data,
)
&mut inband_pps,
);
}
// Drop Access Unit Delimiters: Matroska HEVC frame data
// omits AUDs (the container delimits access units), so
@@ -569,18 +571,31 @@ impl CodecParser for HevcParser {
}
}
if frame_data.is_empty() {
if frame_data.is_empty()
&& inband_vps.is_empty()
&& inband_sps.is_empty()
&& inband_pps.is_empty()
{
return Vec::new();
}
// A player re-applies hvcC param sets at every keyframe; if the active
// set was redefined mid-title and the source stopped repeating it, the
// reversion desyncs CABAC. Re-assert in-band at every keyframe (self-heals).
if keyframe {
// A type this AU redefined goes in-band in its place instead.
{
let mut prefix = Vec::with_capacity(PARAM_REASSERT_HEADROOM);
reassert_active(&mut prefix, &self.cur_vps, emitted_vps);
reassert_active(&mut prefix, &self.cur_sps, emitted_sps);
reassert_active(&mut prefix, &self.cur_pps, emitted_pps);
for (inband, cur) in [
(&inband_vps, &self.cur_vps),
(&inband_sps, &self.cur_sps),
(&inband_pps, &self.cur_pps),
] {
if !inband.is_empty() {
prefix.extend_from_slice(inband);
} else if keyframe {
reassert_active(&mut prefix, cur, false);
}
}
if !prefix.is_empty() {
// Splice prefix into frame_data in place via PARAM_REASSERT_HEADROOM,
// avoiding the extra whole-frame alloc+copy per keyframe the old
@@ -2657,6 +2672,39 @@ mod tests {
// --- parameter-set redefinition (mid-title redefinition bug) ---
// An IRAP whose SPS changed but whose PPS did not: the re-asserted PPS must follow
// the new SPS, or the decoder parses it against the old one and drops it.
#[test]
fn reasserted_pps_follows_a_redefined_sps() {
let mut parser = HevcParser::new();
let nal = |t: u8, body: u8| {
let mut v = vec![0x00, 0x00, 0x01];
v.extend_from_slice(&hevc_nal_header(t));
v.extend_from_slice(&[body, body]);
v
};
let au = |sps: u8| {
let mut d = nal(32, 0x11); // VPS
d.extend(nal(33, sps)); // SPS
d.extend(nal(34, 0x33)); // PPS
d.extend(nal(19, 0x44)); // IDR_W_RADL
d
};
let types = |fd: &[u8]| {
let (mut t, mut o) = (Vec::new(), 0usize);
while o + 4 <= fd.len() {
let len = u32::from_be_bytes([fd[o], fd[o + 1], fd[o + 2], fd[o + 3]]) as usize;
o += 4;
t.push((fd[o] >> 1) & 0x3F);
o += len;
}
t
};
parser.parse(&make_pes(au(0x22), Some(0)));
let f = parser.parse(&make_pes(au(0x23), Some(1)));
assert_eq!(types(&f[0].data), vec![32, 33, 34, 19]);
}
// A parameter set REDEFINED mid-stream must be emitted INLINE so the decoder re-activates
// it.
#[test]