Files
libfreemkv/docs/session.md
T
Matthew Jackson 6e14fc0c0c libfreemkv: 1.7.0 groundwork — sector-recycle soundness + CI hardening
Security
- Remove the unsafe Vec::set_len over recycled sector buffers — UB reachable
  from safe code (GHSA-j8ww-f5fg-9pmh, low). Uses Vec::resize; pooling unchanged.

CI / tooling
- clippy on stable with the MSRV single-sourced from Cargo.toml; cargo-deny
  dependency-audit gate with first-party git sources allowed (#15); audience-based
  comment-guard (#17).

Docs
- README (keydb ownership, BD-J parser count), drive-access transport/reset
  notes, mux timeline module-doc links.
2026-09-02 12:40:55 -07:00

107 lines
5.1 KiB
Markdown

# session.rs — rationale and design notes
Moved here from doc comments in `src/session.rs` to stay under the
comment-guard's per-item prose cap. See the pointers in that file for
which section applies to which item.
## Module overview
Lifecycle is intentionally SPLIT — `open` does transport mechanics only,
`identify` / `scan` are separate — so a consumer can fetch a poster off a
fast `identify` and update its UI before committing to a full `scan`.
libfreemkv resolves no keys and reads no keydb: the consumer builds the
host credentials / key-source layer (from `freemkv_keysources`) and hands
them in via `KeySpec`; the session merely forwards them into
`ScanOptions` at scan time. No cert derivation happens here.
## `resolve_keys_for`
Steps, identical to what the consumers did inline:
1. Take the disc's public AACS inputs (`Disc::inputs`); a non-AACS disc has
none, so this is a no-op returning an empty trace and no fetch.
2. Sample up to `MIN_SAMPLE_UNITS` encrypted content units from the largest
title via `reader` (`read_encrypted_units`) so a candidate key is
validated against real ciphertext. Skipped (no wasted read) when the
factory yields no sources — resolution is then a guaranteed miss anyway.
3. Run the ordered sources first-valid-wins (`resolve_and_apply_traced`),
which banks the winning unit keys onto `disc`'s AACS state.
4. Build the read-time `KeyFetch` from the disc's inputs (its per-call
samples are swapped in by the closure) using the same source factory.
The `reader` is whatever the disc lives behind — a live `Drive` or a
file-backed `SectorSource` from `scan_iso`; both implement `SectorSource`.
## `DiscSession::into_drive`
`Error::DeviceNotReady` is returned when the drive is no longer held — the
public `stage_drive_as_reader` moves it into the reader slot, and calling
this twice moves it out, so an empty slot is reachable through ordinary
use rather than being a caller error. A library must not panic from
public API, and a precondition that normal flow violates is a trap rather
than a contract.
## `DiscSession::from_parts_for_test`
Test-only constructor: build a session over an injected reader plus an
already-scanned disc without opening a live `Drive`. `DiscSession::open`
needs real hardware, so this is the only way to exercise the
`MuxInput::Session` mux arm (take_reader → resolve_inline_base_map →
DiscStream → with_key_map) and `resolve_keys`'s title-sampling branch
against a synthetic reader.
The drive slot stays `None` (a `MuxInput::Session` mux never touches it —
it reads through the staged `reader`); `device` carries a sentinel path so
the driver's missing-reader error still has a name.
`disc` is an `Option` so a test can construct a session that has not been
scanned (`None`) to exercise the `resolve_keys` "called before scan" guard.
## `scan_iso`
This is the file-backed counterpart to `DiscSession::scan`: it is the one
place that opens a `FileSectorSource`, reads its capacity, and runs
`Disc::scan_image`, so consumers (CLI, autorip) stop hand-rolling that
triple and stop constructing the low-level reader themselves. No SCSI, no
handshake, no key resolution — AACS resolution during the scan uses only
whatever `opts` already carries (mirroring how `Disc::scan_image` forwards
`ScanOptions`).
The returned reader is a fresh handle positioned at the start of the
image; callers that need to sample ciphertext (key resolution) or feed a
mux can reuse it directly rather than re-opening the file. `Disc::scan_image`
reads only through the same reader, and all reads are LBA-addressed, so
the handle is fully reusable afterward.
## `scan_dir`
The extra step over `scan_iso` is the encryption verdict. `Disc::scan_with`
decides `encrypted` structurally, from the presence of an `/AACS` or
`/BDMV/AACS` directory (see `disc::aacs_dir_present`). For the common
case — a typical disc backup, which strips `AACS/` — that already gives
the right answer, and `DecryptKeys::None` is a pass-through. But a folder
copied verbatim from a decrypted disc keeps `AACS/`, and the tree shape
then claims encryption over content that is already in the clear: the rip
would fail asking for a key it does not need.
So for a folder, tree shape is not the evidence — content is. Several
aligned units at the largest title's start are sampled and judged by
`aacs_unit_needs_decrypt`, the same authority the mux read path uses:
* none need decryption → the folder is decrypted; `encrypted` is forced
false and the reason is logged.
* any unit does → the folder is a raw encrypted copy, which `dir://` does
not support; `Error::DirImageEncrypted`.
This lives here and not in `Disc::scan_image`, which is shared with the
ISO and drive paths: an ISO that carries `AACS/` and clear content is a
different situation (it may be mid-decrypt, or `--raw` output), and the
verdict must not change underneath those callers.
## `apply_folder_encryption_verdict`
Shared by `scan_dir` and by the `dir://` PES input path in `mux::resolve`.
It lives in one place because the two disagreed: a folder that ripped
through `scan_dir` failed through `input()`, which is the exact failure
this probe was written to prevent, reachable by the other door.