Files
libfreemkv/src/labels/deluxe.rs
T
Matthew Jackson 45c12fc5ce labels: reader-backed detection, Criterion fix, menu-language fallback
- DetectFn now takes a SectorSource so a parser can inspect a jar's
  central directory in detect() instead of firing on "any BD-J jar".
  dbp/deluxe do the real com/<vendor>/ prefix check up front, so each
  claims only its own discs (foundational for scaling the registry).
- criterion: treat a stream-map value of 0 as unmapped and synthesize a
  real 1-based number, so a 0 can't shadow or collide with a genuine
  stream 1 (with regression tests).
- png_filenames: new Low-confidence, last-resort parser reading menu
  language from {title}_UHD01_{LANG}_Composite artwork; sits below the
  MPLS floor so a real framework parser always wins.
- vocab: add menu_lang() for 639-2/B to 639-2/T menu-token normalization.
2026-07-08 14:43:52 -07:00

1393 lines
53 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Deluxe BD-J framework — `com/bydeluxe/bluray/` package signature.
//!
//! Detected on discs whose `/BDMV/JAR/<x>.jar` contains a
//! `com/bydeluxe/` directory entry.
//!
//! ## What this parser reads
//!
//! Deluxe-authored discs store stream labels as ordinal references into
//! enum classes whose names are obfuscated per-disc, so a name-based
//! match won't work. The label data is instead recovered by matching on
//! the **shape of each enum's `<clinit>`**, which is framework-stable:
//!
//! | Enum | Signature |
//! |---|---|
//! | Language | 70 `ldc` operations in `<clinit>`, sequence starts `English, French, Spanish, Dutch, ...` |
//! | Purpose | 8 ldcs starting `Normal, Commentary, PiP, Trivia, ...` |
//! | VideoFormat | 7 ldcs starting `HD, HDR10 Plus, HD Dolby, ...` |
//! | Region | 22 ldcs starting `USA_D1, LIC1, LIC2, LIC3, ...` |
//! | Studio | 6 ldcs in `<clinit>` |
//!
//! Matching on the shape rather than the class name keeps the parser
//! working across obfuscation variants. Codec strings come from the
//! standard BD-J `org/bluray/ti/CodingType` enum referenced directly by
//! the binding constructors (see [`StackVal::CodingType`]), not from a
//! Deluxe-internal enum.
//!
//! ## Implementation phases
//!
//! - **Phase A** — master enum identification (`identify_master_enums`).
//! Walks every `.class`'s `<clinit>` ldc sequence and matches against
//! the framework-stable fingerprints. Output: `Vec<(label, MasterEnum)>`
//! with full ordinal → string-value tables.
//!
//! - **Phase C** — binding-class identification (`find_binding_classes`).
//! The per-stream table is built by some class via repeated
//! `getstatic` references to the master enums identified in A.
//! That class has the highest such `getstatic` count in the jar.
//! Heuristic shape; precise threshold may need tuning.
//!
//! - **Phase D** — binding-class bytecode decoder (`decode_binding`).
//! Walks the binding class's `<clinit>` with a tiny symbolic stack
//! machine. For each `new X / dup / ... / invokespecial X.<init>`
//! sequence, collects the int values and enum-reference operands
//! between the `dup` and the constructor call, then emits a
//! `DecodedStream`. The signal-to-StreamLabel mapping (which arg is
//! stream index? which is language? audio vs subtitle?) uses a
//! heuristic — see `interpret_streams` for the mapping rules.
//!
//! ## Confidence
//!
//! [`parse`] returns `Some(ParseResult::medium(labels))` when Phases A
//! through D produce at least one stream — `Medium` because the
//! signal-to-label mapping is heuristic. `None` when the disc isn't
//! Deluxe-authored or when decoding produces zero streams (a
//! recognized-but-broken state that the analyzer still surfaces via
//! `parsers_detected`).
use super::class_reader::{
AASTORE, BIPUSH, ClassFile, CodeAttribute, ConstantPool, CpInfo, GETSTATIC, ICONST_0, ICONST_1,
ICONST_2, ICONST_3, ICONST_4, ICONST_5, ICONST_M1, INVOKESPECIAL, LDC, LDC_W, NEW, SIPUSH,
};
use super::{LabelPurpose, LabelQualifier, ParseResult, StreamLabel, StreamLabelType, jar, vocab};
use crate::sector::SectorSource;
use crate::udf::UdfFs;
use std::collections::{HashMap, HashSet};
pub fn detect(reader: &mut dyn SectorSource, udf: &UdfFs) -> bool {
// The real signal is `com/bydeluxe/` inside a top-level jar's central
// directory. With a reader in detect we check it directly (cheap
// central-directory scan, no bytecode walk) so this parser claims only
// Deluxe discs; `parse()` repeats the check.
jar::for_each_jar(reader, udf, |_entry, archive| {
jar::has_path_prefix(archive, "com/bydeluxe/").then_some(())
})
.is_some()
}
pub fn parse(reader: &mut dyn SectorSource, udf: &UdfFs) -> Option<ParseResult> {
jar::for_each_jar(reader, udf, |entry_name, archive| {
if !jar::has_path_prefix(archive, "com/bydeluxe/") {
return None;
}
// Phase A — master enums (Language / Purpose / VideoFormat / Region / Studio).
let enums = identify_master_enums(archive);
if enums.is_empty() {
tracing::info!(
jar = %entry_name,
"deluxe: com/bydeluxe/ present but no master enum fingerprint matched"
);
return None;
}
for (label, m) in &enums {
tracing::info!(
jar = %entry_name,
enum = %label,
class = %m.class_name,
count = m.values.len(),
"deluxe master enum identified",
);
}
// Build a fast-lookup table for Phase D's bytecode decoder.
let master_table = MasterEnumTable::from(&enums);
// Phase C — find ALL binding-class candidates (audio + subtitle
// are often split across two classes on Deluxe). Each gets its
// own `<clinit>` walk; constructions union into a single
// stream list for interpret_streams.
let binding_classes = find_binding_classes(archive, &master_table.class_name_set());
if binding_classes.is_empty() {
tracing::info!(
jar = %entry_name,
"deluxe: no binding class found (no class has enough getstatic refs to master enums)"
);
return None;
}
for (name, count) in &binding_classes {
tracing::info!(
jar = %entry_name,
binding_class = %name,
getstatic_count = count,
"deluxe binding class candidate",
);
}
// Phase D — decode each binding class's <clinit>.
let mut streams: Vec<Construction> = Vec::new();
for (name, _) in &binding_classes {
streams.extend(decode_binding(archive, name, &master_table));
}
if streams.is_empty() {
tracing::info!(
jar = %entry_name,
"deluxe: binding classes found but produced 0 decoded streams"
);
return None;
}
let labels = interpret_streams(&streams, &master_table);
if labels.is_empty() {
return None;
}
tracing::info!(
jar = %entry_name,
audio = labels.iter().filter(|l| l.stream_type == StreamLabelType::Audio).count(),
subtitle = labels.iter().filter(|l| l.stream_type == StreamLabelType::Subtitle).count(),
"deluxe emitted labels",
);
// Medium confidence: Phase D's signal-to-label mapping is a
// documented heuristic until corpus-disc bytecode confirms
// the exact binding pattern.
Some(ParseResult::medium(labels))
})
}
/// One identified master enum class.
#[derive(Debug)]
pub(crate) struct MasterEnum {
/// Obfuscated class name (e.g. `be.class`, `aw.class`).
pub class_name: String,
/// Ordinal → string-value mapping, in declaration order.
pub values: Vec<String>,
}
/// Fingerprints we use to identify each master enum class. The
/// matcher walks every class's `<clinit>` ldc sequence; a class
/// matches if its first N ldcs match `prefix` AND the total ldc count
/// equals `expected_count` (allows some slack via tolerance — see
/// `LDC_COUNT_TOLERANCE`). Class names are obfuscated and change per
/// disc; shape is stable.
struct Fingerprint {
label: &'static str,
prefix: &'static [&'static str],
expected_count: usize,
}
const FINGERPRINTS: &[Fingerprint] = &[
Fingerprint {
label: "Language",
prefix: &["English", "French", "Spanish", "Dutch"],
expected_count: 70,
},
Fingerprint {
label: "Purpose",
prefix: &["Normal", "Commentary", "PiP", "Trivia"],
expected_count: 8,
},
Fingerprint {
label: "VideoFormat",
prefix: &["HD", "HDR10 Plus", "HD Dolby"],
expected_count: 7,
},
Fingerprint {
label: "Region",
prefix: &["USA_D1", "LIC1", "LIC2", "LIC3"],
expected_count: 22,
},
Fingerprint {
label: "Studio",
prefix: &["Disney", "Marvel", "Pixar"],
expected_count: 6,
},
];
/// Allow per-version drift in enum size (e.g. one disc had 22 regions,
/// a future build might add one). Matching is still anchored on the
/// prefix, so a count mismatch within tolerance is informative-but-OK.
const LDC_COUNT_TOLERANCE: usize = 4;
/// Phase A. Walk every `.class` in `archive`, identify the master
/// enums by `<clinit>` ldc-sequence fingerprint. Returns a vector of
/// `(label, MasterEnum)` — at most one match per fingerprint label.
pub(crate) fn identify_master_enums(archive: &mut jar::Jar) -> Vec<(&'static str, MasterEnum)> {
use std::collections::HashMap;
// First pass: collect every class's <clinit> ldc string sequence.
let mut candidates: HashMap<String, Vec<String>> = HashMap::new();
jar::for_each_class(archive, |class_name, class| {
let Some(ldcs) = clinit_ldc_strings(class) else {
return;
};
if ldcs.is_empty() {
return;
}
candidates.insert(class_name.to_string(), ldcs);
});
// Second pass: match each fingerprint against the candidate pool.
let mut out = Vec::new();
for fp in FINGERPRINTS {
let mut best: Option<(String, Vec<String>)> = None;
for (name, ldcs) in &candidates {
if !ldcs_match_prefix(ldcs, fp.prefix) {
continue;
}
let count = ldcs.len();
if count.abs_diff(fp.expected_count) > LDC_COUNT_TOLERANCE {
continue;
}
// Prefer exact-count match; otherwise first hit wins.
match &best {
None => best = Some((name.clone(), ldcs.clone())),
Some((_, prev)) => {
if count == fp.expected_count && prev.len() != fp.expected_count {
best = Some((name.clone(), ldcs.clone()));
}
}
}
}
if let Some((class_name, values)) = best {
out.push((fp.label, MasterEnum { class_name, values }));
}
}
out
}
/// Walk `<clinit>` and collect every `ldc` / `ldc_w` operand that
/// resolves to either a `String` constant or a `Utf8` constant, in
/// declaration order. Returns `None` if the class has no `<clinit>`.
fn clinit_ldc_strings(class: &super::class_reader::ClassFile) -> Option<Vec<String>> {
let mut found = false;
let mut out = Vec::new();
for m in &class.methods {
let Some(name) = class.member_name(m) else {
continue;
};
if name != "<clinit>" {
continue;
}
found = true;
let Some(code) = m.code(&class.constant_pool) else {
continue;
};
for insn in code.instructions() {
if insn.opcode != LDC && insn.opcode != LDC_W {
continue;
}
let Some(idx) = insn.cp_index() else {
continue;
};
let resolved = match class.constant_pool.get(idx) {
Some(CpInfo::String { string_index }) => {
class.constant_pool.utf8(*string_index).map(str::to_string)
}
Some(CpInfo::Utf8(s)) => Some(s.clone()),
_ => None,
};
if let Some(s) = resolved {
out.push(s);
}
}
}
if found { Some(out) } else { None }
}
/// True if the first `prefix.len()` entries of `ldcs` match `prefix`
/// exactly. Case-sensitive (enum names are stable strings, not free
/// text).
fn ldcs_match_prefix(ldcs: &[String], prefix: &[&str]) -> bool {
if ldcs.len() < prefix.len() {
return false;
}
ldcs.iter()
.zip(prefix.iter())
.all(|(got, want)| got == want)
}
// ── Phase C: find the binding class ─────────────────────────────────────────
/// Phase C: identify the class that builds the per-stream label table.
/// That class has the highest count of `getstatic` operations whose
/// owning class is one of the master enum classes we identified in
/// Phase A. Returns the class name + the count (useful for the
/// analyzer / corpus regression).
///
/// Threshold: requires at least `MIN_GETSTATIC` matches to consider a
/// class a binding candidate. Empirically the binding class on a
/// typical disc has 50+ such getstatic references (one per slot ×
/// arity); we use a low floor (4) so a small disc with few streams
/// still qualifies, but high enough to filter out classes that just
/// reference the language enum once for a config string.
/// Identify all binding-class candidates by getstatic-count to the
/// master enums. Some Deluxe discs split the per-stream table across
/// two binding classes (one for audio, one for subtitle), so the
/// per-stream decoder needs to walk all of them. Returns top-K
/// candidates ordered by descending getstatic count, filtered to a
/// minimum concentration of master-enum references.
///
/// A disc that splits the table commonly has one audio binding class
/// with the most getstatic refs and a subtitle binding class with
/// somewhat fewer; both share the master Language + Purpose enums.
pub(crate) fn find_binding_classes(
archive: &mut jar::Jar,
master_enum_classes: &HashSet<&str>,
) -> Vec<(String, usize)> {
const MIN_GETSTATIC: usize = 4;
let mut candidates: Vec<(String, usize)> = Vec::new();
jar::for_each_class(archive, |class_name, class| {
let count = count_master_enum_getstatic(class, master_enum_classes);
if count >= MIN_GETSTATIC {
candidates.push((class_name.to_string(), count));
}
});
candidates.sort_by_key(|(_, c)| std::cmp::Reverse(*c));
// Top candidates only — anything significantly below the top one
// is noise. We keep candidates whose count is at least 40% of the
// top, capped at 4 total (audio + subtitle + future use).
if let Some(top_count) = candidates.first().map(|(_, c)| *c) {
let threshold = (top_count * 2) / 5; // 40%
candidates.retain(|(_, c)| *c >= threshold);
candidates.truncate(4);
}
candidates
}
/// Count `getstatic` instructions in this class's `<clinit>` whose
/// owning class is in `master_enum_classes`. Used by Phase C to find
/// the binding class.
fn count_master_enum_getstatic(class: &ClassFile, master_enum_classes: &HashSet<&str>) -> usize {
let mut count = 0usize;
for m in &class.methods {
if class.member_name(m) != Some("<clinit>") {
continue;
}
let Some(code) = m.code(&class.constant_pool) else {
continue;
};
for insn in code.instructions() {
if insn.opcode != GETSTATIC {
continue;
}
let Some(idx) = insn.cp_index() else {
continue;
};
let Some(member) = class.constant_pool.member_ref(idx) else {
continue;
};
if master_enum_classes.contains(member.class_name) {
count += 1;
}
}
}
count
}
// ── Phase D: bytecode-level decoder for the binding class ───────────────────
/// One construction observed in the binding class's `<clinit>`:
/// `new BindingType; dup; ... args ...; invokespecial BindingType.<init>(...)V`.
/// `args` are the symbolic stack values popped at the invokespecial.
#[derive(Debug, Clone)]
pub(crate) struct Construction {
pub binding_type: String,
pub args: Vec<StackVal>,
}
/// Symbolic-stack value during binding `<clinit>` walking.
#[derive(Debug, Clone)]
pub(crate) enum StackVal {
Int(i32),
/// Reference to a master-enum value: (enum kind, ordinal).
EnumRef {
kind: &'static str,
ordinal: u16,
},
/// Reference to a `org.bluray.ti.CodingType` enum value. Field
/// name (e.g. `DOLBY_AC3_AUDIO`, `DOLBY_LOSSLESS_AUDIO`) is the
/// codec identifier. Deluxe binding constructors take a
/// `LCodingType;` arg directly — codecs are NOT a Deluxe-internal
/// enum; the codec source is the standard BD-J API `CodingType`
/// enum, so the binding constructor's codec arg is read straight
/// from that getstatic operand.
CodingType(String),
/// An uninitialized `new` object — popped by the matching
/// invokespecial.
NewObj(String),
/// 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.
Unknown,
}
/// Fully-qualified class name of the BD-J spec codec enum that
/// Deluxe constructors reference directly.
const BD_CODING_TYPE_CLASS: &str = "org/bluray/ti/CodingType";
/// Phase D entry point: find the binding class in `archive`, run the
/// bytecode walker against its `<clinit>`, return one `Construction`
/// per `new X / invokespecial X.<init>` sequence.
pub(crate) fn decode_binding(
archive: &mut jar::Jar,
binding_class_name: &str,
master: &MasterEnumTable,
) -> Vec<Construction> {
let target_name = binding_class_name.to_string();
// Short-circuit on the name match: try_each_class stops iterating
// (and stops decompressing/parsing remaining .class entries) as soon
// as the closure returns Some, instead of walking the whole jar past
// the target.
jar::try_each_class(archive, |class_name, class| {
if class_name != target_name {
return None;
}
Some(decode_binding_class(class, master))
})
.unwrap_or_default()
}
/// Walk every method named `<clinit>` (typically only one) on this
/// class with the symbolic stack machine. Returns each construction
/// emitted.
pub(crate) fn decode_binding_class(
class: &ClassFile,
master: &MasterEnumTable,
) -> Vec<Construction> {
let mut all = Vec::new();
for m in &class.methods {
if class.member_name(m) != Some("<clinit>") {
continue;
}
let Some(code) = m.code(&class.constant_pool) else {
continue;
};
let mut ctx = BindingDecoder::new(&class.constant_pool, master);
ctx.run(&code);
all.extend(ctx.constructions);
}
all
}
/// Tracks the symbolic stack as the walker advances through `<clinit>`.
/// `constructions` accumulates each completed `new X; ... invokespecial X.<init>`.
struct BindingDecoder<'a> {
pool: &'a ConstantPool,
master: &'a MasterEnumTable,
stack: Vec<StackVal>,
constructions: Vec<Construction>,
}
impl<'a> BindingDecoder<'a> {
fn new(pool: &'a ConstantPool, master: &'a MasterEnumTable) -> Self {
Self {
pool,
master,
stack: Vec::new(),
constructions: Vec::new(),
}
}
/// Run the walker over the given Code attribute. On exit the
/// `constructions` field holds the result.
pub(crate) fn run(&mut self, code: &CodeAttribute<'_>) {
for insn in code.instructions() {
self.step(insn);
}
}
fn step(&mut self, insn: super::class_reader::Instruction<'_>) {
match insn.opcode {
// Push small int constants.
ICONST_M1 => self.stack.push(StackVal::Int(-1)),
ICONST_0 => self.stack.push(StackVal::Int(0)),
ICONST_1 => self.stack.push(StackVal::Int(1)),
ICONST_2 => self.stack.push(StackVal::Int(2)),
ICONST_3 => self.stack.push(StackVal::Int(3)),
ICONST_4 => self.stack.push(StackVal::Int(4)),
ICONST_5 => self.stack.push(StackVal::Int(5)),
BIPUSH => {
if let Some(b) = insn.operand_u8() {
self.stack.push(StackVal::Int(b as i8 as i32));
} else {
self.stack.push(StackVal::Unknown);
}
}
SIPUSH => {
if let Some(w) = insn.operand_u16() {
self.stack.push(StackVal::Int(w as i16 as i32));
} else {
self.stack.push(StackVal::Unknown);
}
}
// ldc/ldc_w: push Int when the operand is an Integer
// constant; otherwise push Unknown (we don't care about
// Strings here — labels come via getstatic, not ldc).
LDC | LDC_W => {
let v = insn
.cp_index()
.and_then(|i| match self.pool.get(i) {
Some(CpInfo::Integer(n)) => Some(StackVal::Int(*n)),
_ => None,
})
.unwrap_or(StackVal::Unknown);
self.stack.push(v);
}
// new X — push an uninit-object marker. The matching
// invokespecial will consume this + the args and emit a
// Construction.
NEW => {
let class_name = insn
.cp_index()
.and_then(|i| self.pool.class_name(i))
.unwrap_or("")
.to_string();
self.stack.push(StackVal::NewObj(class_name));
}
// dup — duplicate top of stack.
0x59 /* dup */ => {
if let Some(top) = self.stack.last().cloned() {
self.stack.push(top);
}
}
// getstatic Y.Z — if Y is one of our master enum classes,
// resolve Z to an ordinal and push an EnumRef. Otherwise
// push Unknown so we stay in sync.
GETSTATIC => {
let val = insn
.cp_index()
.and_then(|i| self.pool.member_ref(i))
.map(|m| {
// Three-way resolution:
// 1. org.bluray.ti.CodingType.X → CodingType(X)
// 2. master-enum classname.X → EnumRef(kind, ord)
// 3. anything else → Unknown
if m.class_name == BD_CODING_TYPE_CLASS {
StackVal::CodingType(m.name.to_string())
} else if let Some((kind, ord)) = self.master.resolve(m.class_name, m.name)
{
StackVal::EnumRef { kind, ordinal: ord }
} else {
StackVal::Unknown
}
})
.unwrap_or(StackVal::Unknown);
self.stack.push(val);
}
// invokespecial X.<init>(...) — pop args per descriptor.
// If the object on the stack underneath the args is a
// NewObj of class X (set by an earlier `new X / dup`),
// emit a Construction.
INVOKESPECIAL => {
let Some(idx) = insn.cp_index() else { return };
let Some(member) = self.pool.member_ref(idx) else { return };
let arg_count = parse_method_arg_count(member.descriptor);
// Pop args off the symbolic stack.
if self.stack.len() < arg_count + 1 {
// Stack-machine drift — bail on this construction
// (but don't panic; the walker tolerates malformed
// input by best-effort).
self.stack.clear();
return;
}
let args: Vec<StackVal> = self
.stack
.split_off(self.stack.len() - arg_count);
// Underneath the args: the object the constructor
// operates on. For our pattern it's NewObj(X).
let receiver = self.stack.pop().unwrap_or(StackVal::Unknown);
if let StackVal::NewObj(name) = receiver {
if name == member.class_name {
self.constructions.push(Construction {
binding_type: name,
args,
});
}
}
}
// invokevirtual / invokestatic / invokeinterface — pop
// args per descriptor, push a return placeholder unless
// descriptor returns V (void).
0xB6 /* invokevirtual */ | 0xB8 /* invokestatic */ | 0xB9 /* invokeinterface */ => {
let Some(idx) = insn.cp_index() else { return };
let Some(member) = self.pool.member_ref(idx) else { return };
let arg_count = parse_method_arg_count(member.descriptor);
let extra = if insn.opcode == 0xB6 || insn.opcode == 0xB9 { 1 } else { 0 };
let to_pop = arg_count + extra;
if self.stack.len() < to_pop {
self.stack.clear();
} else {
self.stack.truncate(self.stack.len() - to_pop);
}
// Push return placeholder unless void.
if !member.descriptor.ends_with(")V") {
self.stack.push(StackVal::Unknown);
}
}
// pop / pop2 — drop stack values.
0x57 /* pop */ => {
self.stack.pop();
}
0x58 /* pop2 */ => {
self.stack.pop();
self.stack.pop();
}
// aastore — array store consumes 3 slots (arrayref, index, value).
AASTORE => {
for _ in 0..3 {
self.stack.pop();
}
}
// putstatic / putfield — drop 1 (putstatic) or 2 (putfield).
0xB3 /* putstatic */ => {
self.stack.pop();
}
0xB5 /* putfield */ => {
self.stack.pop();
self.stack.pop();
}
// Branches / returns / unhandled — clear stack as a
// conservative resync. Binding `<clinit>` is straight-
// line code in practice, so we rarely hit these on the
// verified pattern.
0xA7 /* goto */ | 0xB1 /* return */ => {
self.stack.clear();
}
_ => {
// Unknown opcode: best-effort, leave stack untouched.
// The decoder tolerates drift — a final invokespecial
// with mis-aligned stack will just be ignored.
}
}
}
}
/// Count argument slots in a JVMS method descriptor like
/// `(IILjava/lang/String;LFoo;)V`. Each field descriptor is one slot
/// here (we don't track JVM's 2-slot long/double layout — the
/// symbolic stack treats every value as 1 slot, which is what we
/// want for `arg_count` purposes).
fn parse_method_arg_count(descriptor: &str) -> usize {
let bytes = descriptor.as_bytes();
let mut i = 1; // skip leading '('
let mut count = 0;
while i < bytes.len() && bytes[i] != b')' {
match bytes[i] {
b'[' => {
// array — consume the '[' and continue (the element
// descriptor follows).
i += 1;
continue;
}
b'L' => {
// reference type — skip to ';'.
while i < bytes.len() && bytes[i] != b';' {
i += 1;
}
i += 1; // skip the ';'
count += 1;
}
b'B' | b'C' | b'D' | b'F' | b'I' | b'J' | b'S' | b'Z' => {
i += 1;
count += 1;
}
_ => {
// Malformed — best-effort, stop.
break;
}
}
}
count
}
// ── Master enum lookup table ────────────────────────────────────────────────
/// Fast-lookup form of Phase A's master enum identifications. Built
/// once per disc, consumed by Phase D's getstatic resolver.
pub(crate) struct MasterEnumTable {
/// class_name → (kind, field_name → ordinal).
by_class: HashMap<String, (&'static str, HashMap<String, u16>)>,
/// kind → ordinal-indexed string values.
by_kind: HashMap<&'static str, Vec<String>>,
}
impl MasterEnumTable {
pub(crate) fn from(enums: &[(&'static str, MasterEnum)]) -> Self {
let mut by_class = HashMap::new();
let mut by_kind = HashMap::new();
for (kind, m) in enums {
let field_map: HashMap<String, u16> = m
.values
.iter()
.enumerate()
.map(|(i, v)| (v.clone(), i as u16))
.collect();
by_class.insert(m.class_name.clone(), (*kind, field_map));
by_kind.insert(*kind, m.values.clone());
}
MasterEnumTable { by_class, by_kind }
}
pub(crate) fn class_name_set(&self) -> HashSet<&str> {
self.by_class.keys().map(String::as_str).collect()
}
/// Resolve a `getstatic <class>.<field>` to (kind, ordinal). The
/// kind is one of "Language", "Purpose", "VideoFormat", "Region",
/// "Studio" (per the FINGERPRINTS table).
pub(crate) fn resolve(
&self,
class_name: &str,
field_name: &str,
) -> Option<(&'static str, u16)> {
let (kind, fields) = self.by_class.get(class_name)?;
let ordinal = fields.get(field_name).copied()?;
Some((*kind, ordinal))
}
/// Resolve (kind, ordinal) → value string.
pub(crate) fn value(&self, kind: &str, ordinal: u16) -> Option<&str> {
self.by_kind
.get(kind)?
.get(ordinal as usize)
.map(String::as_str)
}
}
// ── interpret_streams: Constructions → StreamLabels ─────────────────────────
/// Convert the per-construction tuples from Phase D into
/// [`StreamLabel`]s. Two binding-constructor shapes are handled:
///
/// 5-arg: `BindingType.<init>(I, Lang;, Lpurpose;, I, LCodingType;)V`
/// 4-arg: `BindingType.<init>(I, Lang;, Lpurpose;, LCodingType;)V`
///
/// Args are identified by **TYPE**, not position:
/// - First `EnumRef{kind: "Language"}` → audio/subtitle language
/// - First `EnumRef{kind: "Purpose"}` → Deluxe purpose ordinal
/// - First `CodingType(name)` → codec field name (translated via
/// [`coding_type_to_codec_hint`])
/// - First `Int(n)` → stream index (preserved as ordering hint;
/// per-type sequential stream_number is what actually goes into
/// the StreamLabel, since BD spec stream-numbering is anchored on
/// MPLS data, not the binding code)
///
/// Stream type inference:
/// - Construction has a `CodingType` arg → audio stream (subtitles
/// on Deluxe don't carry a CodingType; their codec is implicit
/// PGS via the BD spec).
/// - Construction has Language but no CodingType → subtitle stream.
/// - No Language → not a stream (skip).
fn interpret_streams(constructions: &[Construction], master: &MasterEnumTable) -> Vec<StreamLabel> {
let mut audio_idx: u16 = 0;
let mut sub_idx: u16 = 0;
let mut out = Vec::new();
for c in constructions {
let mut lang_ord: Option<u16> = None;
let mut purpose_ord: Option<u16> = None;
let mut coding_type: Option<String> = None;
let mut stream_idx_hint: Option<i32> = None;
for arg in &c.args {
match arg {
StackVal::EnumRef { kind, ordinal } => match *kind {
"Language" => lang_ord = lang_ord.or(Some(*ordinal)),
"Purpose" => purpose_ord = purpose_ord.or(Some(*ordinal)),
_ => {}
},
StackVal::CodingType(name) => {
coding_type = coding_type.or_else(|| Some(name.clone()));
}
StackVal::Int(n) => {
stream_idx_hint = stream_idx_hint.or(Some(*n));
}
_ => {}
}
}
let Some(lang_ord) = lang_ord else { continue };
// Audio when a CodingType is present (audio binding type
// always references org.bluray.ti.CodingType); subtitle
// otherwise.
let codec_hint = coding_type
.as_deref()
.map(coding_type_to_codec_hint)
.map(str::to_string)
.unwrap_or_default();
let (stream_type, stream_number) = if coding_type.is_some() {
audio_idx += 1;
(StreamLabelType::Audio, audio_idx)
} else {
sub_idx += 1;
(StreamLabelType::Subtitle, sub_idx)
};
// Resolve language ordinal → enum value string via master
// table; then route through vocab::lang for ISO code + variant.
let lang_value = master.value("Language", lang_ord).unwrap_or("").to_string();
let (language, variant) = match vocab::lang(&lang_value) {
Some(li) => (li.code.to_string(), li.variant.to_string()),
None if !lang_value.is_empty() => (lang_value.clone(), String::new()),
None => (String::new(), String::new()),
};
let (purpose, qualifier) = match purpose_ord {
Some(o) => deluxe_purpose_to_label(o),
None => (LabelPurpose::Normal, LabelQualifier::None),
};
if let Some(hint) = stream_idx_hint {
tracing::debug!(
disc_stream_idx = hint,
lang = %language,
binding = %c.binding_type,
"deluxe interpret_streams: disc-authored stream index (not used for stream_number; preserved for diagnostic)"
);
}
out.push(StreamLabel {
stream_number,
stream_type,
language,
name: lang_value,
purpose,
qualifier,
codec_hint,
variant,
});
}
out
}
/// Map a `org.bluray.ti.CodingType` field name (as observed in
/// getstatic operands on Deluxe binding classes) to a human-readable
/// codec hint string.
///
/// CodingType is the standard BD-J API enum; values are documented in
/// the BD-J specification. Unknown field names pass through unchanged
/// so unfamiliar codecs still surface something rather than going
/// silent.
fn coding_type_to_codec_hint(field: &str) -> &str {
match field {
// Lossless / hi-res.
"DOLBY_LOSSLESS_AUDIO" => "Dolby TrueHD",
"DTS_HD_LOSSLESS_AUDIO" | "DTS_HD_MA_AUDIO" => "DTS-HD Master Audio",
"LPCM_AUDIO" => "LPCM",
// Dolby family.
"DOLBY_AC3_AUDIO" => "Dolby Digital",
"DOLBY_DIGITAL_PLUS_AUDIO" => "Dolby Digital Plus",
"DOLBY_ATMOS_AUDIO" => "Dolby Atmos",
// DTS family.
"DTS_AUDIO" => "DTS",
"DTS_HD_AUDIO" | "DTS_HD_HR_AUDIO" => "DTS-HD HR",
// MPEG family.
"MPEG1_AUDIO_LAYER2" | "MPEG2_AUDIO_LAYER2" => "MPEG Audio",
// PG-style subtitle codecs (rare to see in Deluxe bindings;
// subtitles usually have NO CodingType arg).
"PG_STREAM" | "PRESENTATION_GRAPHICS_STREAM" => "PGS",
// Unknown / future — pass through verbatim so the operator
// can see what the disc actually authored.
_ => field,
}
}
/// Deluxe Purpose enum ordinal → (LabelPurpose, LabelQualifier). The
/// enum order is fixed per Phase A's verified output:
/// 0=Normal, 1=Commentary, 2=PiP, 3=Trivia, 4=Descriptive, 5=Score,
/// 6=NoForced, 7=NoForcedDescriptive.
fn deluxe_purpose_to_label(ordinal: u16) -> (LabelPurpose, LabelQualifier) {
match ordinal {
0 => (LabelPurpose::Normal, LabelQualifier::None),
1 => (LabelPurpose::Commentary, LabelQualifier::None),
2 => (LabelPurpose::Normal, LabelQualifier::None), // PiP — picture in picture, treated as Normal
3 => (LabelPurpose::Normal, LabelQualifier::None), // Trivia — bonus, treated as Normal
4 => (LabelPurpose::Descriptive, LabelQualifier::None),
5 => (LabelPurpose::Score, LabelQualifier::None),
6 => (LabelPurpose::Normal, LabelQualifier::None), // NoForced — semantic unclear; treat as Normal
7 => (LabelPurpose::Descriptive, LabelQualifier::None), // NoForcedDescriptive
_ => (LabelPurpose::Normal, LabelQualifier::None),
}
}
// ── Tests ────────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ldcs_match_prefix_exact() {
let ldcs = vec![
"English".to_string(),
"French".to_string(),
"Spanish".to_string(),
];
assert!(ldcs_match_prefix(&ldcs, &["English", "French"]));
assert!(ldcs_match_prefix(&ldcs, &["English", "French", "Spanish"]));
assert!(!ldcs_match_prefix(&ldcs, &["English", "German"]));
// Too short — prefix longer than ldcs is a mismatch.
assert!(!ldcs_match_prefix(
&ldcs,
&["English", "French", "Spanish", "Dutch"]
));
}
#[test]
fn ldcs_match_prefix_is_case_sensitive() {
let ldcs = vec!["english".to_string(), "french".to_string()];
assert!(!ldcs_match_prefix(&ldcs, &["English", "French"]));
}
#[test]
fn fingerprint_count_tolerance_lock() {
// Lock the tolerance to a sane value. Too low = brittle to
// framework drift; too high = false positives on unrelated
// classes that happen to match the prefix.
const _: () = assert!(LDC_COUNT_TOLERANCE >= 1 && LDC_COUNT_TOLERANCE <= 10);
}
#[test]
fn fingerprints_cover_documented_enums() {
// Lock the fingerprint roster — if someone adds/removes a
// fingerprint, this test forces them to think about it. The
// 5 documented enums (Language, Purpose, VideoFormat, Region,
// Studio) all need to be here. Codec is structural (separate
// path), not fingerprinted by ldc prefix.
let labels: Vec<&str> = FINGERPRINTS.iter().map(|fp| fp.label).collect();
assert_eq!(
labels,
vec!["Language", "Purpose", "VideoFormat", "Region", "Studio"]
);
}
#[test]
fn fingerprint_prefixes_nonempty_and_under_expected_count() {
// Each prefix must be non-empty and shorter than expected_count
// (so the count gives ADDITIONAL signal beyond the prefix
// match). If a prefix is as long as expected_count there's no
// counting benefit.
for fp in FINGERPRINTS {
assert!(!fp.prefix.is_empty(), "{} has empty prefix", fp.label);
assert!(
fp.prefix.len() < fp.expected_count,
"{} prefix is not shorter than expected_count",
fp.label
);
}
}
// ── Phase D bytecode walker tests ───────────────────────────────────────
use super::super::class_reader::{ConstantPool, CpInfo};
#[test]
fn parse_method_arg_count_basic_types() {
assert_eq!(parse_method_arg_count("()V"), 0);
assert_eq!(parse_method_arg_count("(I)V"), 1);
assert_eq!(parse_method_arg_count("(II)V"), 2);
assert_eq!(parse_method_arg_count("(IIII)V"), 4);
// Long and Double — 1 arg each on our symbolic stack (we
// don't track JVM 2-slot layout).
assert_eq!(parse_method_arg_count("(JD)V"), 2);
assert_eq!(parse_method_arg_count("(BCDFIJSZ)V"), 8);
}
#[test]
fn parse_method_arg_count_reference_types() {
assert_eq!(parse_method_arg_count("(Ljava/lang/String;)V"), 1);
assert_eq!(parse_method_arg_count("(ILjava/lang/String;LFoo;)V"), 3);
// Array types.
assert_eq!(parse_method_arg_count("([I)V"), 1);
assert_eq!(parse_method_arg_count("([[Ljava/lang/Object;)V"), 1);
assert_eq!(
parse_method_arg_count("(I[Ljava/lang/String;Ljava/util/List;)V"),
3
);
}
#[test]
fn parse_method_arg_count_malformed_descriptor() {
// Best-effort: stops on the bad byte, doesn't panic.
assert_eq!(parse_method_arg_count("(Ifoo)V"), 1);
}
/// Construct a minimal ConstantPool that supports the synthetic
/// bytecode in the tests below. Layout:
/// 1: Utf8 "LanguageEnum"
/// 2: Class -> 1 (LanguageEnum)
/// 3: Utf8 "English"
/// 4: Utf8 "LLanguageEnum;"
/// 5: NameAndType { name: 3, descriptor: 4 } (LanguageEnum.English)
/// 6: Fieldref { class: 2, nat: 5 } (getstatic operand)
/// 7: Utf8 "AudioSlot"
/// 8: Class -> 7 (AudioSlot)
/// 9: Utf8 "<init>"
/// 10: Utf8 "(LLanguageEnum;)V"
/// 11: NameAndType { name: 9, descriptor: 10 }
/// 12: Methodref { class: 8, nat: 11 } (invokespecial operand)
fn build_simple_pool() -> ConstantPool {
let entries = vec![
CpInfo::Empty,
CpInfo::Utf8("LanguageEnum".into()),
CpInfo::Class { name_index: 1 },
CpInfo::Utf8("English".into()),
CpInfo::Utf8("LLanguageEnum;".into()),
CpInfo::NameAndType {
name_index: 3,
descriptor_index: 4,
},
CpInfo::Fieldref {
class_index: 2,
name_and_type_index: 5,
},
CpInfo::Utf8("AudioSlot".into()),
CpInfo::Class { name_index: 7 },
CpInfo::Utf8("<init>".into()),
CpInfo::Utf8("(LLanguageEnum;)V".into()),
CpInfo::NameAndType {
name_index: 9,
descriptor_index: 10,
},
CpInfo::Methodref {
class_index: 8,
name_and_type_index: 11,
},
];
ConstantPool::from_entries(entries)
}
fn lang_enum_master() -> MasterEnumTable {
let m = MasterEnum {
class_name: "LanguageEnum".into(),
values: vec!["English".into(), "French".into(), "Spanish".into()],
};
MasterEnumTable::from(&[("Language", m)])
}
#[test]
fn binding_decoder_recognizes_simple_construction() {
// Synthetic <clinit>:
// new AudioSlot (cp idx 8 -> Class -> Utf8 "AudioSlot")
// dup
// getstatic Lang.Eng (cp idx 6 -> Fieldref)
// invokespecial AS.<init>(LLanguageEnum;)V (cp idx 12)
let code: Vec<u8> = vec![
NEW,
0,
8, // new AudioSlot
0x59, // dup
GETSTATIC,
0,
6, // getstatic LanguageEnum.English
INVOKESPECIAL,
0,
12, // invokespecial AudioSlot.<init>(LLanguageEnum;)V
];
let pool = build_simple_pool();
let master = lang_enum_master();
let attr = super::super::class_reader::CodeAttribute {
max_stack: 4,
max_locals: 0,
code: &code,
};
let mut decoder = BindingDecoder::new(&pool, &master);
decoder.run(&attr);
assert_eq!(decoder.constructions.len(), 1);
let c = &decoder.constructions[0];
assert_eq!(c.binding_type, "AudioSlot");
assert_eq!(c.args.len(), 1);
match &c.args[0] {
StackVal::EnumRef { kind, ordinal } => {
assert_eq!(*kind, "Language");
assert_eq!(*ordinal, 0); // English at ordinal 0
}
other => panic!("expected EnumRef, got {:?}", other),
}
}
#[test]
fn binding_decoder_handles_iconst_and_bipush() {
// <clinit> with an int push before the construction:
// iconst_1
// new AudioSlot; dup; getstatic Lang.Eng; invokespecial AS.<init>(LLanguageEnum;)V
// pop (drops the constructed object)
// bipush 42
// pop
let code: Vec<u8> = vec![
ICONST_1,
NEW,
0,
8,
0x59,
GETSTATIC,
0,
6,
INVOKESPECIAL,
0,
12,
0x57, // pop
BIPUSH,
42,
0x57, // pop
];
let pool = build_simple_pool();
let master = lang_enum_master();
let attr = super::super::class_reader::CodeAttribute {
max_stack: 4,
max_locals: 0,
code: &code,
};
let mut decoder = BindingDecoder::new(&pool, &master);
decoder.run(&attr);
// Should still produce one construction, ignoring the
// standalone int pushes that have no construction context.
assert_eq!(decoder.constructions.len(), 1);
}
#[test]
fn binding_decoder_skips_unmatched_invokespecial() {
// invokespecial without a preceding `new X; dup` — should
// produce zero constructions.
let code: Vec<u8> = vec![ICONST_0, GETSTATIC, 0, 6, INVOKESPECIAL, 0, 12];
let pool = build_simple_pool();
let master = lang_enum_master();
let attr = super::super::class_reader::CodeAttribute {
max_stack: 4,
max_locals: 0,
code: &code,
};
let mut decoder = BindingDecoder::new(&pool, &master);
decoder.run(&attr);
assert_eq!(decoder.constructions.len(), 0);
}
#[test]
fn binding_decoder_resolves_master_enum_ordinal() {
// getstatic to a class NOT in MasterEnumTable should push
// Unknown, not an EnumRef.
let mut entries = vec![
CpInfo::Empty,
CpInfo::Utf8("OtherEnum".into()),
CpInfo::Class { name_index: 1 },
CpInfo::Utf8("FOO".into()),
CpInfo::Utf8("LOtherEnum;".into()),
CpInfo::NameAndType {
name_index: 3,
descriptor_index: 4,
},
CpInfo::Fieldref {
class_index: 2,
name_and_type_index: 5,
},
];
entries.extend(vec![
CpInfo::Utf8("AudioSlot".into()),
CpInfo::Class { name_index: 7 },
CpInfo::Utf8("<init>".into()),
CpInfo::Utf8("(LOtherEnum;)V".into()),
CpInfo::NameAndType {
name_index: 9,
descriptor_index: 10,
},
CpInfo::Methodref {
class_index: 8,
name_and_type_index: 11,
},
]);
let pool = ConstantPool::from_entries(entries);
let master = lang_enum_master(); // LanguageEnum, not OtherEnum
let code: Vec<u8> = vec![
NEW,
0,
8, // new AudioSlot
0x59, // dup
GETSTATIC,
0,
6, // getstatic OtherEnum.FOO (not in master table)
INVOKESPECIAL,
0,
12,
];
let attr = super::super::class_reader::CodeAttribute {
max_stack: 4,
max_locals: 0,
code: &code,
};
let mut decoder = BindingDecoder::new(&pool, &master);
decoder.run(&attr);
assert_eq!(decoder.constructions.len(), 1);
// The arg should be Unknown, not EnumRef, because OtherEnum
// isn't in MasterEnumTable.
match &decoder.constructions[0].args[0] {
StackVal::Unknown => {}
other => panic!("expected Unknown, got {:?}", other),
}
}
// ── interpret_streams + deluxe_purpose_to_label tests ───────────────────
#[test]
fn deluxe_purpose_ordinal_maps_correctly() {
// 8-value Purpose enum: Normal/Commentary/PiP/Trivia/
// Descriptive/Score/NoForced/NoForcedDescriptive.
assert_eq!(deluxe_purpose_to_label(0).0, LabelPurpose::Normal);
assert_eq!(deluxe_purpose_to_label(1).0, LabelPurpose::Commentary);
assert_eq!(deluxe_purpose_to_label(4).0, LabelPurpose::Descriptive);
assert_eq!(deluxe_purpose_to_label(5).0, LabelPurpose::Score);
assert_eq!(deluxe_purpose_to_label(7).0, LabelPurpose::Descriptive);
}
#[test]
fn deluxe_purpose_out_of_range_falls_back_to_normal() {
assert_eq!(deluxe_purpose_to_label(99).0, LabelPurpose::Normal);
}
#[test]
fn interpret_streams_emits_subtitle_when_no_codingtype() {
// A Construction with just a language enum ref (no CodingType)
// -> subtitle stream (codec_hint stays empty). Subtitles on
// Deluxe don't carry a CodingType arg.
let constructions = vec![Construction {
binding_type: "SubtitleSlot".into(),
args: vec![StackVal::EnumRef {
kind: "Language",
ordinal: 0,
}],
}];
let out = interpret_streams(&constructions, &lang_enum_master());
assert_eq!(out.len(), 1);
assert_eq!(out[0].stream_type, StreamLabelType::Subtitle);
assert_eq!(out[0].language, "eng");
assert_eq!(out[0].codec_hint, "");
}
#[test]
fn interpret_streams_emits_audio_when_codingtype_present() {
// A Construction with a CodingType arg -> audio stream with
// codec_hint populated by coding_type_to_codec_hint.
let constructions = vec![Construction {
binding_type: "ng".into(),
args: vec![
StackVal::Int(1),
StackVal::EnumRef {
kind: "Language",
ordinal: 0,
},
StackVal::EnumRef {
kind: "Purpose",
ordinal: 0,
},
StackVal::CodingType("DOLBY_LOSSLESS_AUDIO".into()),
],
}];
let out = interpret_streams(&constructions, &lang_enum_master());
assert_eq!(out.len(), 1);
assert_eq!(out[0].stream_type, StreamLabelType::Audio);
assert_eq!(out[0].codec_hint, "Dolby TrueHD");
assert_eq!(out[0].language, "eng");
}
#[test]
fn interpret_streams_purpose_routed_through_deluxe_enum() {
let constructions = vec![Construction {
binding_type: "SubtitleSlot".into(),
args: vec![
StackVal::EnumRef {
kind: "Language",
ordinal: 0,
},
StackVal::EnumRef {
kind: "Purpose",
ordinal: 1, // Commentary
},
],
}];
let out = interpret_streams(&constructions, &lang_enum_master());
assert_eq!(out.len(), 1);
assert_eq!(out[0].purpose, LabelPurpose::Commentary);
}
#[test]
fn interpret_streams_skips_constructions_without_language() {
let constructions = vec![Construction {
binding_type: "SomeOtherType".into(),
args: vec![StackVal::Int(1)],
}];
let out = interpret_streams(&constructions, &lang_enum_master());
assert!(out.is_empty());
}
#[test]
fn coding_type_maps_known_codecs() {
// BD-J spec CodingType field names -> display strings.
assert_eq!(
coding_type_to_codec_hint("DOLBY_LOSSLESS_AUDIO"),
"Dolby TrueHD"
);
assert_eq!(
coding_type_to_codec_hint("DOLBY_AC3_AUDIO"),
"Dolby Digital"
);
assert_eq!(
coding_type_to_codec_hint("DOLBY_DIGITAL_PLUS_AUDIO"),
"Dolby Digital Plus"
);
assert_eq!(coding_type_to_codec_hint("DTS_AUDIO"), "DTS");
assert_eq!(
coding_type_to_codec_hint("DTS_HD_MA_AUDIO"),
"DTS-HD Master Audio"
);
assert_eq!(coding_type_to_codec_hint("LPCM_AUDIO"), "LPCM");
}
#[test]
fn coding_type_passes_through_unknown() {
// Unknown field names pass through verbatim so the operator
// sees what the disc authored.
assert_eq!(
coding_type_to_codec_hint("FUTURE_CODEC_X"),
"FUTURE_CODEC_X"
);
}
#[test]
fn master_enum_table_resolves_field_to_ordinal() {
let table = lang_enum_master();
assert_eq!(
table.resolve("LanguageEnum", "English"),
Some(("Language", 0))
);
assert_eq!(
table.resolve("LanguageEnum", "French"),
Some(("Language", 1))
);
assert_eq!(
table.resolve("LanguageEnum", "Spanish"),
Some(("Language", 2))
);
assert_eq!(table.resolve("LanguageEnum", "Klingon"), None);
assert_eq!(table.resolve("OtherEnum", "English"), None);
}
#[test]
fn master_enum_table_value_resolves_ordinal_to_string() {
let table = lang_enum_master();
assert_eq!(table.value("Language", 0), Some("English"));
assert_eq!(table.value("Language", 2), Some("Spanish"));
assert_eq!(table.value("Language", 99), None);
assert_eq!(table.value("Unknown", 0), None);
}
#[test]
fn master_enum_table_class_name_set_lists_all_classes() {
let table = lang_enum_master();
let set = table.class_name_set();
assert!(set.contains("LanguageEnum"));
assert_eq!(set.len(), 1);
}
}