Files
libfreemkv/src/labels/mod.rs
T
MattJackson 99236ffd55 labels: add class_reader, hand-rolled JVM .class file parser
Foundation for label parsers that need structured access to .class
files inside /BDMV/JAR/<x>.jar. Replaces noak (~3KLOC dep) with a
~1000-line std-only reader.

Public API:
- ClassFile::parse(&[u8]) -> Result<ClassFile>
- ConstantPool::{get, utf8, class_name, string, integer, member_ref, iter}
- Member::code(&pool) -> Option<CodeAttribute>
- CodeAttribute::instructions() -> Instructions iterator
- Instruction::{name, operand_u8, operand_u16, cp_index}
- Opcode constants (LDC, AASTORE, NEW, GETSTATIC, INVOKESPECIAL, ...)

Spec coverage:
- Constant pool: all 17 tag types incl. Long/Double 2-slot quirk
- Modified UTF-8 incl. 0xC0 0x80 -> U+0000 special case
- Bytecode iteration with full opcode size table
- Variable-length tableswitch / lookupswitch / wide

12 unit tests cover the opcode table edge cases (padded switch tables,
wide-iinc 6-byte form), modified-UTF-8 decoder, and iterator
stop-on-truncated behaviour.

Module is currently #![allow(dead_code)] — the public API is staged
for labels::deluxe (Phases A-E bytecode walker) and a labels::dbp
refactor onto the constant-pool iterator. Tests exercise the API
in isolation. The allow comes off as those callers land.

Also fixes two pre-existing clippy lints that 1.86's stricter checks
flagged after I touched the labels module:
- src/mux/disc.rs: while-let-loop in test fixture
- tests/pass_n_size_aware_skip.rs: type_complexity in helper signature

Precommit (cargo +1.86 fmt + clippy + test) green.
2026-05-10 15:06:34 -07:00

415 lines
14 KiB
Rust

//! Stream label extraction from BD-J disc files.
//!
//! Each parser module represents one BD-J authoring framework.
//! To add a new format:
//! 1. Create `src/labels/myformat.rs`
//! 2. Implement `pub fn detect(udf: &UdfFs) -> bool`
//! 3. Implement `pub fn parse(reader: &mut dyn SectorReader, udf: &UdfFs) -> Option<Vec<StreamLabel>>`
//! 4. Add `mod myformat;` below and one line to `PARSERS` array
pub(crate) mod class_reader;
mod criterion;
mod ctrm;
mod dbp;
mod paramount;
mod pixelogic;
pub mod vocab;
use crate::disc::{DiscTitle, Stream};
use crate::sector::SectorReader;
use crate::udf::UdfFs;
// Re-exported via crate::disc — the public API surfaces these next to
// AudioStream/SubtitleStream so callers can map purpose/qualifier to display
// text in their own locale.
/// A stream label extracted from disc config files.
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct StreamLabel {
/// STN index (1-based)
pub stream_number: u16,
/// Audio or Subtitle
pub stream_type: StreamLabelType,
/// ISO 639-2 language code
pub language: String,
/// Display name (e.g. "Commentary", "Descriptive Audio")
pub name: String,
/// Stream purpose
pub purpose: LabelPurpose,
/// Additional qualifier
pub qualifier: LabelQualifier,
/// Codec hint from config (e.g. "TrueHD", "Dolby Digital", "Dolby Atmos")
pub codec_hint: String,
/// Regional variant (e.g. "US", "UK", "Castilian", "Canadian")
pub variant: String,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum StreamLabelType {
Audio,
Subtitle,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LabelPurpose {
Normal,
Commentary,
Descriptive,
Score,
Ime,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LabelQualifier {
None,
Sdh,
DescriptiveService,
Forced,
}
// ── Parser registry ────────────────────────────────────────────────────────
//
// Each entry: (name, detect_fn, parse_fn)
// Order = priority. First match wins. Highest quality output first.
type DetectFn = fn(&UdfFs) -> bool;
type ParseFn = fn(&mut dyn SectorReader, &UdfFs) -> Option<Vec<StreamLabel>>;
const PARSERS: &[(&str, DetectFn, ParseFn)] = &[
("paramount", paramount::detect, paramount::parse),
("criterion", criterion::detect, criterion::parse),
("pixelogic", pixelogic::detect, pixelogic::parse),
("ctrm", ctrm::detect, ctrm::parse),
// dbp last: detects on any top-level .jar in /BDMV/JAR/ (every
// BD-J disc has one), so parse() does the real `com/dbp/` check
// and returns None on a mismatch. By placing dbp last, the
// earlier parsers' fast file-presence detects short-circuit and
// dbp only runs on discs that fell through everything else.
("dbp", dbp::detect, dbp::parse),
// ("deluxe", deluxe::detect, deluxe::parse), // TODO: bytecode parser
];
/// Search disc for config files, extract labels, apply to streams.
/// This is 100% optional — if anything fails, streams are untouched.
pub fn apply(reader: &mut dyn SectorReader, udf: &UdfFs, titles: &mut [DiscTitle]) {
let labels = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| extract(reader, udf)))
.unwrap_or_default();
if labels.is_empty() {
return;
}
for title in titles.iter_mut() {
let mut audio_idx: u16 = 0;
let mut sub_idx: u16 = 0;
for stream in &mut title.streams {
match stream {
Stream::Audio(a) => {
audio_idx += 1;
if let Some(label) = labels.iter().find(|l| {
l.stream_type == StreamLabelType::Audio && l.stream_number == audio_idx
}) {
// Structured fields — callers translate purpose to UI text.
a.purpose = label.purpose;
// a.label only carries codec/variant info. NEVER any
// English purpose text — the CLI handles that via i18n.
let mut parts = Vec::new();
if !label.variant.is_empty() {
parts.push(format!("({})", label.variant));
}
if !label.codec_hint.is_empty() {
parts.push(label.codec_hint.clone());
}
if !parts.is_empty() {
a.label = parts.join(" ");
} else if !label.name.is_empty() && label.purpose == LabelPurpose::Normal {
// Only fall back to the parser-supplied display
// name when there's no purpose to flag — the CLI
// handles purpose rendering itself.
a.label = label.name.clone();
}
}
}
Stream::Subtitle(s) => {
sub_idx += 1;
if let Some(label) = labels.iter().find(|l| {
l.stream_type == StreamLabelType::Subtitle && l.stream_number == sub_idx
}) {
s.qualifier = label.qualifier;
if label.qualifier == LabelQualifier::Forced {
s.forced = true;
}
}
}
_ => {}
}
}
}
}
/// Fill in default labels for any streams that don't have one.
/// Runs after BD-J label extraction — fills gaps with codec + channel descriptions.
/// This is the central place for all fallback label generation.
pub fn fill_defaults(titles: &mut [crate::disc::DiscTitle]) {
use crate::disc::Stream;
for title in titles.iter_mut() {
for stream in &mut title.streams {
match stream {
Stream::Audio(a) if a.label.is_empty() => {
a.label = generate_audio_label(&a.codec, &a.channels, a.secondary);
}
Stream::Video(v) if v.label.is_empty() => {
v.label =
generate_video_label(&v.codec, v.resolution.pixels(), &v.hdr, v.secondary);
}
Stream::Subtitle(s) if s.forced => {
// Ensure forced subs are labeled even if BD-J didn't set a name
// (subtitle labels are generally not set — this just marks forced)
}
_ => {}
}
}
}
}
fn generate_video_label(
codec: &crate::disc::Codec,
pixels: (u32, u32),
hdr: &crate::disc::HdrFormat,
secondary: bool,
) -> String {
use crate::disc::HdrFormat;
if secondary {
// "Dolby Vision EL" is a brand identifier, not English prose, so the
// library may emit it. Other "secondary video" wording is a CLI
// concern — the library just leaves the label empty.
return match hdr {
HdrFormat::DolbyVision => "Dolby Vision EL".to_string(),
_ => String::new(),
};
}
let mut parts = Vec::new();
// Codec
parts.push(codec.name().to_string());
// Resolution
let (w, h) = pixels;
let res = if w >= 7680 {
"8K"
} else if w >= 3840 {
"4K"
} else if w >= 1920 {
"1080p"
} else if w >= 1280 {
"720p"
} else if h >= 576 {
"576p"
} else if h >= 480 {
"480p"
} else {
""
};
if !res.is_empty() {
parts.push(res.into());
}
// HDR
match hdr {
HdrFormat::Sdr => {}
_ => parts.push(hdr.name().to_string()),
}
parts.join(" ")
}
fn generate_audio_label(
codec: &crate::disc::Codec,
channels: &crate::disc::AudioChannels,
_secondary: bool,
) -> String {
use crate::disc::{AudioChannels, Codec};
// Full marketing names for disc audio codecs.
// These are codec brand identifiers, not user-facing English prose.
let codec_name = match codec {
Codec::TrueHd => "Dolby TrueHD",
Codec::Ac3 => "Dolby Digital",
Codec::Ac3Plus => "Dolby Digital Plus",
Codec::DtsHdMa => "DTS-HD Master Audio",
Codec::DtsHdHr => "DTS-HD High Resolution",
Codec::Dts => "DTS",
Codec::Lpcm => "LPCM",
Codec::Aac => "AAC",
Codec::Mp2 => "MPEG Audio",
Codec::Mp3 => "MP3",
Codec::Flac => "FLAC",
Codec::Opus => "Opus",
_ => return String::new(),
};
// Channel layout
let channel_str = match channels {
AudioChannels::Mono => "1.0",
AudioChannels::Stereo => "2.0",
AudioChannels::Stereo21 => "2.1",
AudioChannels::Quad => "4.0",
AudioChannels::Surround50 => "5.0",
AudioChannels::Surround51 => "5.1",
AudioChannels::Surround61 => "6.1",
AudioChannels::Surround71 => "7.1",
AudioChannels::Unknown => "",
};
// The "(Secondary)" suffix is a CLI/UI concern — callers display it from
// the AudioStream::secondary bool, not the library.
if channel_str.is_empty() {
codec_name.to_string()
} else {
format!("{} {}", codec_name, channel_str)
}
}
fn extract(reader: &mut dyn SectorReader, udf: &UdfFs) -> Vec<StreamLabel> {
for (name, detect, parse) in PARSERS {
if detect(udf) {
tracing::info!(parser = name, "label parser matched");
if let Some(labels) = parse(reader, udf) {
return labels;
}
}
}
tracing::info!("no label parser matched");
Vec::new()
}
/// Diagnostic introspection — returns the parser that matched, the
/// labels it emitted, and the inventory of files under `/BDMV/JAR/*/`
/// that the discriminators looked at. Intended for `freemkv-tools
/// labels-analyze` and corpus regression tooling, not production code
/// paths. The matching/parsing logic is identical to [`extract`]; only
/// the return shape is richer.
#[doc(hidden)]
pub fn analyze(reader: &mut dyn SectorReader, udf: &UdfFs) -> LabelAnalysis {
let inventory = jar_inventory(udf);
// Record every parser whose discriminator matched — even if its
// parse step then returned None — so the analyzer can distinguish
// "no parser recognized this disc" from "parser recognized it but
// couldn't read the file" (e.g. content past a truncated capture)
// or "parser ran but produced no labels."
let mut parsers_detected: Vec<&'static str> = Vec::new();
for (name, detect, parse) in PARSERS {
if detect(udf) {
tracing::info!(parser = name, "label parser matched");
parsers_detected.push(name);
if let Some(labels) = parse(reader, udf) {
return LabelAnalysis {
parser: Some(name),
parsers_detected,
jar_inventory: inventory,
labels,
};
}
}
}
if parsers_detected.is_empty() {
tracing::info!("no label parser matched");
} else {
tracing::info!(
detected = ?parsers_detected,
"label parsers detected but produced no labels"
);
}
LabelAnalysis {
parser: None,
parsers_detected,
jar_inventory: inventory,
labels: Vec::new(),
}
}
/// Result of [`analyze`].
#[doc(hidden)]
#[derive(Debug, Clone)]
pub struct LabelAnalysis {
/// Which parser matched ("paramount" / "criterion" / "pixelogic" /
/// "ctrm") AND emitted labels. `None` means either no parser
/// recognized the disc, OR a parser recognized it but its parse
/// step returned None (file unreadable, no parseable tokens). Use
/// `parsers_detected` to disambiguate.
pub parser: Option<&'static str>,
/// Every parser whose discriminator matched, in priority order.
/// Distinguishes "we recognized this disc but couldn't extract
/// labels" from "we don't recognize this disc at all" — the
/// former points at a parser bug or a truncated capture, the
/// latter points at a missing parser.
pub parsers_detected: Vec<&'static str>,
/// Filenames found under any `/BDMV/JAR/*/` subdirectory, deduped
/// and sorted. Helps spot unknown authoring formats when no
/// parser detected.
pub jar_inventory: Vec<String>,
/// Raw labels emitted by the matched parser (empty if `parser` is
/// `None`).
pub labels: Vec<StreamLabel>,
}
/// List filenames found under any `/BDMV/JAR/<x>/` subdirectory of
/// the disc. Deduped, sorted. Returns an empty vec if no JAR dir is
/// present.
fn jar_inventory(udf: &UdfFs) -> Vec<String> {
let Some(jar_dir) = udf.find_dir("/BDMV/JAR") else {
return Vec::new();
};
let mut out: Vec<String> = Vec::new();
for entry in &jar_dir.entries {
if entry.is_dir {
for child in &entry.entries {
if !child.is_dir && !out.contains(&child.name) {
out.push(child.name.clone());
}
}
}
}
out.sort();
out
}
// ── Shared helpers ─────────────────────────────────────────────────────────
/// Check if a file exists in any BDMV/JAR subdirectory.
pub(crate) fn jar_file_exists(udf: &UdfFs, filename: &str) -> bool {
find_jar_file(udf, filename).is_some()
}
/// Find a file in any BDMV/JAR subdirectory, return its path.
pub(crate) fn find_jar_file(udf: &UdfFs, filename: &str) -> Option<String> {
let jar_dir = udf.find_dir("/BDMV/JAR")?;
for entry in &jar_dir.entries {
if entry.is_dir {
let path = format!("/BDMV/JAR/{}/{}", entry.name, filename);
// Check if file exists in this subdirectory
for child in &entry.entries {
if !child.is_dir && child.name.eq_ignore_ascii_case(filename) {
return Some(path);
}
}
}
}
None
}
/// Read a file from any BDMV/JAR subdirectory by filename.
pub(crate) fn read_jar_file(
reader: &mut dyn SectorReader,
udf: &UdfFs,
filename: &str,
) -> Option<Vec<u8>> {
let path = find_jar_file(udf, filename)?;
udf.read_file(reader, &path).ok().filter(|d| !d.is_empty())
}