Fix the DVD MPEG-audio codec mapping and the fabricated AACS docs

parse_audio_attr mapped DVD audio_coding_mode 2 to Codec::Mpeg1 — the MPEG-1
VIDEO variant. Codec::kind() reports Video for it, so a DVD MPEG-audio stream
was classified and handled as video everywhere downstream. Modes 2 and 3 are
both MPEG audio Layer II (3 adds the MPEG-2 multichannel extension), so both
map to Codec::Mp2. A test now walks every coding mode and asserts each result's
kind() is Audio, so no mode can map to a non-audio codec again.

docs/aacs.md documented an entire keydb-resolving API that does not exist:
ScanOptions::with_keydb, Disc::open_title, reader.read_unit(). None of those
symbols appear anywhere in the crate, and ScanOptions has no keydb field — its
own doc comment says "libfreemkv is lookup-free — it resolves no keys". A
reader following that page would conclude the library reads keydb.cfg, which
inverts the actual design: the caller resolves keys out-of-band through a
KeySource and applies them with Disc::decrypt_with.

The section is rewritten against the real API, and the AacsState table's
`key_source` type corrected from KeySource to KeyOrigin.

Worth recording: the first replacement example I wrote was itself wrong. It
used `input("disc://...")`, which resolve.rs explicitly rejects with
Error::DiscUrlNotDirect — live disc must go through Drive::open + Disc::scan +
DiscStream::new. Every symbol and signature in the committed example was
checked against the source rather than assumed.
This commit is contained in:
Matthew Jackson
2026-07-29 18:47:06 -07:00
parent ec5cd31ae1
commit c0434e87de
2 changed files with 60 additions and 18 deletions
+27 -16
View File
@@ -65,27 +65,38 @@ if disc.encrypted {
} }
} }
// Read content -- decryption is automatic // Read content -- decryption is applied on read by the DiscStream decorator.
let mut reader = disc.open_title(&mut session, 0).unwrap(); // Live disc does NOT go through the URL resolver: `input("disc://...")` returns
while let Some(unit) = reader.read_unit().unwrap() { // Error::DiscUrlNotDirect by design.
// decrypted content let keys = disc.decrypt_keys();
let mut stream = DiscStream::new(
Box::new(drive),
disc.titles[0].clone(),
keys,
batch_sectors,
disc.titles[0].content_format,
false, // raw: false → decrypt on read
None, // halt
)?;
while let Ok(Some(frame)) = stream.read() {
// decrypted PES frames
} }
``` ```
The application never touches keys, never calls decryption functions, and never The application never calls decryption functions and never manages the
manages handshakes. All of that is internal to `Disc::scan()` and the content drive-level handshake. It DOES own key resolution — see below.
reader.
### KEYDB Location ### Key resolution is the caller's job
`ScanOptions` controls where the keydb is loaded from. If no explicit path is `libfreemkv` is **lookup-free: it resolves no keys and reads no keydb.** There is
set, the library checks the standard config locations. To specify an explicit no `ScanOptions::with_keydb`, and `ScanOptions` has no keydb field — its only
path: scan input is the optional drive credentials for the live-drive authenticated
handshake.
```rust The caller resolves a key out-of-band through a key source and applies it with
let opts = ScanOptions::with_keydb("/path/to/keydb.cfg"); [`Disc::decrypt_with`]. `freemkv-keysources` is the crate that implements the
let disc = Disc::scan(&mut session, &opts).unwrap(); keydb and key-server sources; `ScanOptions::key_sources` takes them as
``` `Box<dyn KeySource>`.
### AacsState ### AacsState
@@ -97,7 +108,7 @@ After a successful scan, `disc.aacs` contains an `AacsState`:
| `bus_encryption` | `bool` | Whether bus encryption is active | | `bus_encryption` | `bool` | Whether bus encryption is active |
| `mkb_version` | `Option<u32>` | MKB version from disc | | `mkb_version` | `Option<u32>` | MKB version from disc |
| `disc_hash` | `String` | Identifier for the disc's key-input files | | `disc_hash` | `String` | Identifier for the disc's key-input files |
| `key_source` | `KeySource` | How the disc's key was resolved | | `key_source` | `KeyOrigin` | How the disc's key was resolved |
## keydb.cfg ## keydb.cfg
+33 -2
View File
@@ -571,10 +571,14 @@ fn parse_audio_attr(data: &[u8], offset: usize) -> Result<DvdAudioAttr> {
let b1 = byte_at(data, offset + 1)?; let b1 = byte_at(data, offset + 1)?;
let coding_mode = (b0 >> 5) & 0x07; let coding_mode = (b0 >> 5) & 0x07;
// DVD-Video audio_coding_mode. Modes 2 and 3 are both MPEG audio Layer II
// (3 adds the MPEG-2 multichannel extension), so both are Codec::Mp2.
// Mode 2 previously mapped to Codec::Mpeg1 — a VIDEO variant, so
// Codec::kind() reported Video and the audio stream was classified and
// handled as video everywhere downstream.
let codec = match coding_mode { let codec = match coding_mode {
0 => Codec::Ac3, 0 => Codec::Ac3,
2 => Codec::Mpeg1, 2 | 3 => Codec::Mp2,
3 => Codec::Mp2,
4 => Codec::Lpcm, 4 => Codec::Lpcm,
6 => Codec::Dts, 6 => Codec::Dts,
_ => Codec::Unknown(coding_mode), _ => Codec::Unknown(coding_mode),
@@ -1218,6 +1222,33 @@ mod tests {
assert_eq!(crate::mux::ps::dvd_audio_pid(0x89), Some(0xBD89)); assert_eq!(crate::mux::ps::dvd_audio_pid(0x89), Some(0xBD89));
} }
#[test]
fn every_audio_coding_mode_maps_to_an_audio_codec() {
// An audio attribute block must never yield a codec whose kind() is Video.
// Mode 2 (MPEG-1 audio Layer II) mapped to Codec::Mpeg1 — the MPEG-1 VIDEO
// variant — so a DVD MPEG-audio stream was classified as video downstream.
for (mode, want) in [
(0u8, Codec::Ac3),
(2, Codec::Mp2),
(3, Codec::Mp2),
(4, Codec::Lpcm),
(6, Codec::Dts),
] {
let mut data = vec![0u8; 16];
data[0] = mode << 5;
data[2] = b'e';
data[3] = b'n';
let attr = parse_audio_attr(&data, 0).unwrap();
assert_eq!(attr.codec, want, "coding_mode {mode} must map to {want:?}");
assert_eq!(
attr.codec.kind(),
crate::disc::CodecKind::Audio,
"coding_mode {mode} produced {:?}, whose kind is not Audio",
attr.codec
);
}
}
#[test] #[test]
fn audio_attr_dts() { fn audio_attr_dts() {
let mut data = vec![0u8; 16]; let mut data = vec![0u8; 16];