Files
libfreemkv/src/labels/deluxe.rs
T

1623 lines
62 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 starting `Disney, Marvel, Pixar, ...` |
//! | Codec | many `new` instructions, 0 ldcs in `<clinit>` (codec strings live in subclasses) |
//!
//! Matching on the shape rather than the class name keeps the parser
//! working across obfuscation variants.
//!
//! ## 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 B** — codec enum subclass walk (`decode_codec_enum`).
//! The codec enum's `<clinit>` has many `new` instructions and zero
//! string ldcs — codec name strings live in the subclasses each
//! `new` constructs. Walks every referenced subclass's constant
//! pool, extracts the codec name string, following the standard Java
//! enum compilation convention (each enum value's `<init>` is called
//! with its name string as the first arg).
//!
//! - **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(udf: &UdfFs) -> bool {
// Cheap pre-check at the dir level; the real signal is
// `com/bydeluxe/` inside any top-level jar's central directory,
// which `parse()` confirms when given a `SectorSource`.
jar::has_any_top_level_jar(udf)
}
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 B — codec enum (structural + subclass walk).
let codec_shape = find_codec_enum(archive);
let codec_table = match codec_shape.as_ref() {
Some(shape) => decode_codec_enum(archive, shape),
None => CodecTable::default(),
};
if let Some(shape) = &codec_shape {
tracing::info!(
jar = %entry_name,
class = %shape.class_name,
count = codec_table.codecs.len(),
"deluxe codec enum decoded",
);
}
// 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 B (structural): identify the codec enum class. The codec
/// enum's `<clinit>` has many `new` instructions (one per codec value)
/// and zero string ldcs — codec name strings live in the subclasses
/// each `new` constructs, not in the enum class itself. This function
/// returns the candidate enum's class name + the ordered list of
/// subclass class names; [`decode_codec_enum`] walks those subclasses
/// to extract the codec strings.
pub(crate) fn find_codec_enum(archive: &mut jar::Jar) -> Option<CodecEnumShape> {
let mut best: Option<(String, Vec<String>)> = None;
jar::for_each_class(archive, |class_name, class| {
let Some((news, ldcs)) = clinit_news_and_ldcs(class) else {
return;
};
// Codec enum's <clinit> has many `new` ops, 0 string ldcs.
if news.len() < 20 || !ldcs.is_empty() {
return;
}
match &best {
None => best = Some((class_name.to_string(), news)),
Some((_, prev)) => {
if news.len() > prev.len() {
best = Some((class_name.to_string(), news));
}
}
}
});
best.map(|(class_name, subclass_news)| CodecEnumShape {
class_name,
subclass_news,
})
}
#[derive(Debug)]
pub(crate) struct CodecEnumShape {
pub class_name: String,
/// Ordered list of class names referenced by `new` in <clinit>.
/// One entry per codec enum value; subclass walking resolves
/// each to a codec string.
pub subclass_news: Vec<String>,
}
/// Phase B (subclass walk): given the codec enum's structural shape,
/// walk each referenced subclass's constant pool to extract its
/// codec name string. Output is ordinal-indexed: `codecs[i]` is the
/// codec name for the i-th `new` instruction in the enum's `<clinit>`.
///
/// The codec name extraction heuristic: each subclass's constant
/// pool typically contains a small number of Utf8 entries; the
/// codec-name-shaped one is uppercase, ≥4 chars, optionally with
/// underscores or digits. We pick the first matching Utf8 entry that
/// isn't a method-descriptor sigil, class-name fragment, or attribute
/// name. Empty string when no candidate is found — the parser can
/// surface "unknown codec at ordinal N" via tracing.
pub(crate) fn decode_codec_enum(archive: &mut jar::Jar, shape: &CodecEnumShape) -> CodecTable {
// Two-pass: first pass extracts the codec-name candidate from
// every class in the jar (cheap to do all at once, cache for the
// ordinal-ordered second pass).
let mut name_by_class: HashMap<String, String> = HashMap::new();
let wanted: HashSet<&str> = shape.subclass_news.iter().map(String::as_str).collect();
jar::for_each_class(archive, |class_name, class| {
if !wanted.contains(class_name) {
return;
}
if let Some(name) = extract_codec_name(class) {
name_by_class.insert(class_name.to_string(), name);
}
});
let codecs: Vec<String> = shape
.subclass_news
.iter()
.map(|c| name_by_class.get(c).cloned().unwrap_or_default())
.collect();
CodecTable { codecs }
}
/// Per-codec name table — `codecs[ordinal]` is the codec string for
/// that enum value. Empty string for ordinals where Phase B couldn't
/// extract a name (rare; logged via tracing).
#[derive(Debug, Default, Clone)]
pub(crate) struct CodecTable {
pub codecs: Vec<String>,
}
impl CodecTable {
/// Resolve a codec enum ordinal to its name string. Returns None
/// for out-of-range ordinals or for entries Phase B couldn't
/// extract (those slots are stored as empty strings, which this
/// helper normalizes to None).
#[allow(dead_code)] // surface for callers; interpret_streams uses
// binding_type substring match for now (codec-ordinal wiring
// deferred until corpus bytecode confirms the codec arg position).
pub fn get(&self, ordinal: u16) -> Option<&str> {
let s = self.codecs.get(ordinal as usize)?;
if s.is_empty() { None } else { Some(s.as_str()) }
}
}
/// Heuristic: extract the codec-name string from a codec-enum
/// subclass's constant pool. Codec names are uppercase tokens with
/// optional underscores/digits, ≥4 chars (e.g. "ATMOS_HD_AUDIO",
/// "DOLBY_AC3_AUDIO", "DTS_HD_MA", "PCM_5_1"). We scan the pool's
/// Utf8 entries and pick the first that:
/// - is ≥4 chars
/// - contains only A-Z, 0-9, and _
/// - contains at least one underscore OR is a known codec token
/// (the underscore signal is what separates "ATMOS_HD_AUDIO"
/// from "Utf8" / "Code" / "Object" attribute names).
///
/// Returns `None` when no candidate matches — the caller's `codecs[i]`
/// will be empty for that ordinal.
fn extract_codec_name(class: &ClassFile) -> Option<String> {
for (_, entry) in class.constant_pool.iter() {
let CpInfo::Utf8(s) = entry else {
continue;
};
if s.len() < 4 {
continue;
}
if !s
.chars()
.all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')
{
continue;
}
if !s.contains('_') {
// Single-token all-caps strings might still be valid
// (e.g. "ATMOS", "DTS"). Require at least one of the
// known codec token roots to avoid false positives like
// attribute names that happen to be uppercase. For now
// we only accept these as a fallback.
let is_known_root = [
"ATMOS", "DOLBY", "DTS", "TRUEHD", "MLP", "AC3", "EAC3", "PCM",
]
.iter()
.any(|root| s == *root);
if !is_known_root {
continue;
}
}
return Some(s.clone());
}
None
}
/// Walk `<clinit>` and return `(new_class_names, ldc_strings)`. Used
/// for the codec-enum shape match where we care about both counts.
#[allow(dead_code)]
fn clinit_news_and_ldcs(
class: &super::class_reader::ClassFile,
) -> Option<(Vec<String>, Vec<String>)> {
let mut news = Vec::new();
let mut ldcs = Vec::new();
let mut found = false;
let mut _aastore = 0u32;
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() {
match insn.opcode {
NEW => {
if let Some(idx) = insn.cp_index() {
if let Some(n) = class.constant_pool.class_name(idx) {
news.push(n.to_string());
}
}
}
LDC | LDC_W => {
if let Some(idx) = insn.cp_index() {
let s = 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) = s {
ldcs.push(s);
}
}
}
AASTORE => _aastore += 1,
_ => {}
}
}
}
if found { Some((news, ldcs)) } else { None }
}
// ── 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 mut out: Vec<Construction> = Vec::new();
let target_name = binding_class_name.to_string();
jar::for_each_class(archive, |class_name, class| {
if class_name != target_name {
return;
}
out = decode_binding_class(class, master);
});
out
}
/// 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 extract_codec_name_picks_uppercase_with_underscore() {
// Synthetic class file built via ClassFile::parse would be
// overkill; here we directly invoke extract_codec_name via a
// minimal hand-built ClassFile. Skip — covered indirectly by
// the end-to-end Phase B tests at corpus runtime. Tested
// signal: the matcher logic itself.
// (Helper inlined for clarity rather than spinning up a fake
// class.)
let candidate_strings = ["Code", "Utf8", "ATMOS_HD_AUDIO", "MyVar"];
let result = candidate_strings.iter().find(|s| {
s.len() >= 4
&& s.chars()
.all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')
&& s.contains('_')
});
assert_eq!(result, Some(&"ATMOS_HD_AUDIO"));
}
#[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);
}
}