aacs: discover HD DVD AACS dir + title-key files instead of hardcoding /ANY!/VTKF000

The HD DVD AACS directory name and title-key filename are chosen by the
authoring house, but the resolver hardcoded a single spelling
(/ANY!/VTKF000.AACS, /ANY!/MKBROM.AACS, /ANY!/CONTENT_CERT.AACS). Real discs
diverge: Freedom (Memory-Tech) names its AACS dir AAC! and ships VTKF090.AACS
+ VTKF100.AACS; Harry Potter carries VTKF000/001/002/099. On such a disc the
hardcoded path finds nothing, so no MKB/title-key/cert is read and decryption
silently can't engage.

Replace the fixed HD DVD path constants with structural discovery:
- find_hddvd_aacs_dir() locates the AACS dir as the root child dir ending in
  '!' that contains MKBROM.AACS (so the ..._BAK mirror is skipped; the dozens
  of decoy advanced-content '!' dirs are excluded by the MKBROM.AACS guard).
- role_paths(udf, role) builds the ordered candidate list per role: the static
  BD/UHD /AACS/ paths first, then the discovered HD DVD files — MKBROM.AACS,
  CONTENT_CERT.AACS, and every VTKF*.AACS (sorted), not just VTKF000.
- read_first() is now generic over &str / String so it takes the Vec<String>.

BD/UHD unaffected (no '!' dir → discovery returns None, list is the /AACS/
constants exactly as before). Verified on real Freedom (AAC!/VTKF090+100) and
Dukes (ANY!/VTKF000) ISOs; unit tests cover both shapes.

Open item (TODO(hddvd-encrypted)): when a disc has multiple VTKF variants the
correct one must be chosen by validating its VUK-derived key against a real
encrypted unit rather than first-that-reads. Blocked on obtaining a genuinely
encrypted HD DVD image — all HD DVD ISOs on hand are already-decrypted rips.
This commit is contained in:
Matthew Jackson
2026-07-20 11:29:28 -07:00
parent 6718c9cdb2
commit c1f1593003
4 changed files with 252 additions and 50 deletions
+10 -5
View File
@@ -324,13 +324,18 @@ impl Disc {
use crate::aacs;
let uk_ro_data =
aacs::read_first(aacs::UNIT_KEY_RO_PATHS, |p| udf_fs.read_file(reader, p))?;
aacs::read_first(&aacs::role_paths(udf_fs, aacs::AacsRole::UnitKey), |p| {
udf_fs.read_file(reader, p)
})?;
let dh = aacs::inf::disc_hash(&uk_ro_data);
let cc = aacs::read_first(aacs::CONTENT_CERT_PATHS, |p| udf_fs.read_file(reader, p))
.ok()
.as_deref()
.and_then(aacs::inf::parse_content_cert);
let cc = aacs::read_first(
&aacs::role_paths(udf_fs, aacs::AacsRole::ContentCert),
|p| udf_fs.read_file(reader, p),
)
.ok()
.as_deref()
.and_then(aacs::inf::parse_content_cert);
let bus_encryption = cc.as_ref().map(|c| c.bus_encryption).unwrap_or(false);
// No-cert default = UHD (V20 stride), matching `read_aacs_version` so the
// scanned `AacsState.version` and the out-of-band fetch agree. A wrong
+12 -9
View File
@@ -1824,9 +1824,10 @@ impl Disc {
reader: &mut dyn SectorSource,
udf_fs: &udf::UdfFs,
) -> Result<(Vec<u8>, Vec<u8>, u8)> {
let inf = crate::aacs::read_first(crate::aacs::UNIT_KEY_RO_PATHS, |p| {
udf_fs.read_file(reader, p)
})?;
let inf = crate::aacs::read_first(
&crate::aacs::role_paths(udf_fs, crate::aacs::AacsRole::UnitKey),
|p| udf_fs.read_file(reader, p),
)?;
let mkb = Self::read_mkb_content(reader, udf_fs)?;
let version = Self::read_aacs_version(reader, udf_fs);
Ok((inf, mkb, version))
@@ -1844,9 +1845,10 @@ impl Disc {
/// mis-strided title keys (silent wrong unit keys), so a missing cert must
/// not quietly pick the V10 stride for a UHD disc.
fn read_aacs_version(reader: &mut dyn SectorSource, udf_fs: &udf::UdfFs) -> u8 {
match crate::aacs::read_first(crate::aacs::CONTENT_CERT_PATHS, |p| {
udf_fs.read_file(reader, p)
})
match crate::aacs::read_first(
&crate::aacs::role_paths(udf_fs, crate::aacs::AacsRole::ContentCert),
|p| udf_fs.read_file(reader, p),
)
.ok()
.as_deref()
.and_then(crate::aacs::inf::parse_content_cert)
@@ -1880,9 +1882,10 @@ impl Disc {
const MAX_BYTES: usize = 64 * 1024 * 1024;
let mut want = START_BYTES;
loop {
let buf = crate::aacs::read_first(crate::aacs::MKB_PATHS, |p| {
udf_fs.read_file_prefix(reader, p, want)
})?;
let buf = crate::aacs::read_first(
&crate::aacs::role_paths(udf_fs, crate::aacs::AacsRole::Mkb),
|p| udf_fs.read_file_prefix(reader, p, want),
)?;
let n = crate::aacs::mkb::mkb_content_len(&buf);
// `n` strictly inside `buf` => the record walk reached the padding
// boundary (full content captured). `buf` shorter than `want` =>