disc: first-class FMTS + HD-DVD formats; CPI sample selection

Add DiscFormat::Fmts (AACS 2.1) and DiscFormat::HdDvd as first-class peers. Format derives from the AACS MKB generation (mkb_type().generation(): V10=BD, V20=UHD, V21=FMTS), reusing existing AACS code, and from the on-disc tree for HD-DVD/DVD. One detector (detect_disc_format) shared by the coarse DiscId probe and the full scan — no more 'default BluRay, defer to full scan'.

FMTS is a BD-tree stream variant: parse_playlist resolves the clip stream via CLIP_STREAM_EXTS (.m2ts -> .fmts -> .ssif), so the .fmts main feature yields real extents (previously silently empty). HD-DVD is a tree-level peer with its own enumerator (disc/hddvd.rs): HVDVD_TS/*.evo -> MpegPs titles with real extents (playlist/stream parsing honestly stubbed).

Sample selection for key resolution now uses the authoritative AACS CPI flag (aacs_unit_encrypted, byte-0 & 0xC0) not the ts_sync_destroyed heuristic — container-agnostic (M2TS/FMTS/EVO; TS-sync is meaningless on HD-DVD program streams) and stops the decode-server '0 encrypted units' rejection.

Tests live with each format (bluray/hddvd/mod); generic UDF fixture builders extracted to a shared udf::fixture module.
This commit is contained in:
Matthew Jackson
2026-07-07 18:16:04 -07:00
parent bc04ee7bd2
commit 85347597cc
5 changed files with 715 additions and 276 deletions
+157 -12
View File
@@ -13,6 +13,7 @@ mod dvd;
pub mod dvd_audio_probe;
mod encrypt;
mod extract;
mod hddvd;
pub mod mapfile;
mod patch;
pub mod read_error;
@@ -93,8 +94,16 @@ pub enum ContentFormat {
pub enum DiscFormat {
/// 4K UHD Blu-ray (HEVC 2160p)
Uhd,
/// UHD Blu-ray with AACS 2.1 FMTS content — the main feature is a `.fmts`
/// clip (M2TS transport stream plus interleaved forensic variant segments).
/// A BD-tree disc (enumerated by [`Disc::scan_bluray_titles`]); distinct
/// from [`DiscFormat::Uhd`] only in the container + AACS generation.
Fmts,
/// Standard Blu-ray (1080p/1080i)
BluRay,
/// HD-DVD — `HVDVD_TS/` tree with `.evo` (Enhanced VOB, MPEG program stream)
/// clips. A tree-level peer of DVD/BD, enumerated by its own scanner.
HdDvd,
/// DVD
Dvd,
/// Unknown
@@ -1481,13 +1490,10 @@ impl Disc {
let (capacity, mut buffered, udf_fs) = Self::read_udf(session)?;
let meta_title = Self::read_meta_title(&mut buffered, &udf_fs);
let format = if udf_fs.find_dir("/BDMV").is_some() {
DiscFormat::BluRay // full scan distinguishes UHD vs BD
} else if udf_fs.find_dir("/VIDEO_TS").is_some() {
DiscFormat::Dvd
} else {
DiscFormat::Unknown
};
// Authoritative up front — same MKB-driven detector as the full scan
// (no titles needed: BD/UHD/FMTS come from the MKB generation). This
// no longer defaults to BluRay and defers UHD/FMTS to the full scan.
let format = Self::detect_disc_format(&mut buffered, &udf_fs, &[]);
let encrypted =
udf_fs.find_dir("/AACS").is_some() || udf_fs.find_dir("/BDMV/AACS").is_some();
let layers = if capacity > 24_000_000 { 2 } else { 1 };
@@ -1907,12 +1913,20 @@ impl Disc {
}
};
// 3. Titles — BD (MPLS playlists) or DVD (IFO title sets)
// 3. Titles + container — dispatched by on-disc tree. HD-DVD and DVD are
// tree-level peers, each with its own enumerator; FMTS shares the BD
// tree (a `.fmts` stream variant). Disc FORMAT is a separate axis
// derived below from the AACS MKB generation, not the tree.
let (mut titles, content_format) = if udf_fs.find_dir("/BDMV").is_some() {
(
Self::scan_bluray_titles(reader, &udf_fs),
ContentFormat::BdTs,
)
} else if udf_fs.find_dir("/HVDVD_TS").is_some() {
(
Self::scan_hddvd_titles(reader, &udf_fs),
ContentFormat::MpegPs,
)
} else if udf_fs.find_dir("/VIDEO_TS").is_some() {
(
Self::scan_dvd_titles(reader, &udf_fs),
@@ -1936,8 +1950,9 @@ impl Disc {
crate::labels::apply(reader, &udf_fs, &mut titles);
crate::labels::fill_defaults(&mut titles);
// 5. Derive format, layers, region
let format = Self::detect_format(&titles);
// 5. Format (AACS MKB generation → BD/UHD/FMTS; tree → HD-DVD/DVD),
// layers, region.
let format = Self::detect_disc_format(reader, &udf_fs, &titles);
let layers = if capacity > 24_000_000 { 2 } else { 1 };
let region = DiscRegion::Free;
@@ -2087,6 +2102,48 @@ impl Disc {
(lossless, max_ch, count)
}
/// The disc format, from the two on-disc axes:
/// * **tree** → HD-DVD (`HVDVD_TS/`) and DVD (`VIDEO_TS/`) are tree-level
/// peers with their own enumerators;
/// * **AACS MKB generation** → within the BD tree (`BDMV/`), the MKB Type
/// record decides BD (1.0) / UHD (2.0) / FMTS (2.1). This is the
/// authoritative, cheap signal (the Type record is the first bytes of
/// `MKB_RO.inf`) — reusing [`crate::aacs::mkb::mkb_type`] /
/// [`crate::aacs::mkb::MkbType::generation`], not a filesystem heuristic.
///
/// An unencrypted / MKB-less BD tree falls back to video resolution (still a
/// BD-tree disc, so never below [`DiscFormat::BluRay`]).
fn detect_disc_format(
reader: &mut dyn SectorSource,
udf_fs: &crate::udf::UdfFs,
titles: &[DiscTitle],
) -> DiscFormat {
use crate::aacs::mkb::{AacsVersion, mkb_type};
if udf_fs.find_dir("/HVDVD_TS").is_some() {
return DiscFormat::HdDvd;
}
if udf_fs.find_dir("/VIDEO_TS").is_some() {
return DiscFormat::Dvd;
}
if udf_fs.find_dir("/BDMV").is_some() {
// Only the Type-and-Version record (first record) is needed.
if let Ok(mkb) = udf_fs.read_file_prefix(reader, "/AACS/MKB_RO.inf", 64) {
match mkb_type(&mkb).map(|t| t.generation()) {
Some(AacsVersion::V21) => return DiscFormat::Fmts,
Some(AacsVersion::V20) => return DiscFormat::Uhd,
Some(AacsVersion::V10) => return DiscFormat::BluRay,
None => {}
}
}
// Unencrypted / unreadable MKB: refine by resolution, default BD.
return match Self::detect_format(titles) {
DiscFormat::Unknown => DiscFormat::BluRay,
other => other,
};
}
DiscFormat::Unknown
}
fn detect_format(titles: &[DiscTitle]) -> DiscFormat {
for title in titles.iter().take(3) {
for stream in &title.streams {
@@ -3977,8 +4034,10 @@ const MIN_BATCH_SECTORS: u16 = 3;
pub(crate) fn ecc_sectors(format: DiscFormat) -> u16 {
match format {
DiscFormat::Uhd | DiscFormat::BluRay => 32,
DiscFormat::Dvd => 16,
// BD-family 64 KiB ECC block (32 × 2048). FMTS is a UHD BD disc.
DiscFormat::Uhd | DiscFormat::Fmts | DiscFormat::BluRay => 32,
// 32 KiB ECC block (16 × 2048) — DVD and HD-DVD.
DiscFormat::Dvd | DiscFormat::HdDvd => 16,
DiscFormat::Unknown => 32,
}
}
@@ -4474,6 +4533,92 @@ mod tests {
assert_eq!(Disc::detect_format(&titles), DiscFormat::Unknown);
}
/// An AACS MKB Type-and-Version record (0x10) carrying `raw_type` — the only
/// record [`Disc::detect_disc_format`] reads to decide BD/UHD/FMTS.
fn mkb_type_record(raw_type: u32) -> Vec<u8> {
let mut v = vec![0x10, 0x00, 0x00, 0x0c]; // record type 0x10, rec_len 12
v.extend_from_slice(&raw_type.to_be_bytes()); // MKBType @ body offset 0
v.extend_from_slice(&0u32.to_be_bytes()); // version @ body offset 4
v
}
/// FORMAT derives from the AACS MKB generation, not the tree or filesystem:
/// 2.1 → FMTS, 2.0 → UHD, 1.0 → BD — all from the MKB Type record.
#[test]
fn detect_format_from_mkb_generation() {
use crate::udf::fixture::*;
for (raw, expected) in [
(0x4815_1003u32, DiscFormat::Fmts),
(0x4814_1003u32, DiscFormat::Uhd),
(0x0004_1003u32, DiscFormat::BluRay),
] {
let mut disc = MemDisc::new();
let root = DirSpec {
name: String::new(),
icb_lba: 10,
dir_data_lba: 11,
files: Vec::new(),
subdirs: vec![
DirSpec {
name: "BDMV".into(),
icb_lba: 12,
dir_data_lba: 13,
files: Vec::new(),
subdirs: vec![],
},
DirSpec {
name: "AACS".into(),
icb_lba: 14,
dir_data_lba: 15,
files: vec![file_with(
"MKB_RO.inf",
16,
5000,
mkb_type_record(raw),
true,
)],
subdirs: vec![],
},
],
};
build_udf_skeleton(&mut disc, 10);
lay_dir(&mut disc, &root);
let udf = crate::udf::read_filesystem(&mut disc).expect("fs");
assert_eq!(
Disc::detect_disc_format(&mut disc, &udf, &[]),
expected,
"MKB type {raw:#010x}"
);
}
}
/// HD-DVD is a tree-level format — recognized from `HVDVD_TS/`, no MKB.
#[test]
fn detect_format_hddvd_from_tree() {
use crate::udf::fixture::*;
let mut disc = MemDisc::new();
let root = DirSpec {
name: String::new(),
icb_lba: 10,
dir_data_lba: 11,
files: Vec::new(),
subdirs: vec![DirSpec {
name: "HVDVD_TS".into(),
icb_lba: 20,
dir_data_lba: 21,
files: Vec::new(),
subdirs: vec![],
}],
};
build_udf_skeleton(&mut disc, 10);
lay_dir(&mut disc, &root);
let udf = crate::udf::read_filesystem(&mut disc).expect("fs");
assert_eq!(
Disc::detect_disc_format(&mut disc, &udf, &[]),
DiscFormat::HdDvd
);
}
#[test]
fn content_format_default_bdts() {
let t = title_with_video(Codec::H264, Resolution::R1080p);