freemkv-keysources: fixture-based integration tests for key sources
Add tests/key_sources.rs exercising the published KeySource impls end-to-end over real fixture files and the libfreemkv parsers: - KeydbSource: disc-hash lookup from a real keydb.cfg (VUK hit ranks ahead of the universal DK pool), hash miss yields only the pool, missing file is silent/not errored, label + needs_samples, and host-cert serving from a | HC | row (inherent and trait paths). - paths: exe-local search list, default == search head, existing path reflects on-disk state (local-only, no OS fallback). - MapfileSource: persisted # freemkv-uk: keys read back as a terminal Key::Unit, one-shot exhaustion, missing/keyless mapfile offers nothing. - OnlineSource: unconfigured no-op (no network), one-shot latch, metadata, and validate_keyserver_url scheme/SSRF gating. - MultiSource: caller-supplied order/precedence (and its reverse), empty-source skip, needs_samples/errored OR-aggregation, nesting, and a real keydb-then-mapfile precedence chain over fixtures.
This commit is contained in:
@@ -0,0 +1,534 @@
|
||||
//! Fixture-based integration tests for the published key sources.
|
||||
//!
|
||||
//! These exercise the *public* surface of `freemkv-keysources` end-to-end —
|
||||
//! real files on disk, the real `KeyDb`/`Mapfile` parsers from libfreemkv, and
|
||||
//! the `KeySource` trait the applications drive — rather than the pure
|
||||
//! `candidates_from` unit tests that already live next to the source code.
|
||||
//!
|
||||
//! Covered:
|
||||
//! - `KeydbSource`: lookup by disc hash through a real `keydb.cfg` file; the
|
||||
//! exe-local `keydb_search_paths` / default / existing path helpers; host-cert
|
||||
//! serving from a `| HC |` row.
|
||||
//! - `MapfileSource`: terminal `Key::Unit` read back from a rip mapfile's
|
||||
//! `# freemkv-uk:` header; one-shot exhaustion; missing-file silence.
|
||||
//! - `OnlineSource`: SSRF/scheme validation and the unconfigured no-op (no
|
||||
//! network is touched in CI).
|
||||
//! - `MultiSource`: caller-supplied ordering / precedence, exhaustion advance,
|
||||
//! `needs_samples`/`errored` aggregation, and nesting.
|
||||
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use freemkv_keysources::{
|
||||
DiscInputs, Key, KeySource, KeydbSource, MapfileSource, MultiSource, OnlineSource,
|
||||
default_keydb_path, existing_keydb_path, keydb_search_paths, validate_keyserver_url,
|
||||
};
|
||||
|
||||
// ── fixture helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
/// A unique scratch dir for one test, removed on `Drop` so fixtures never leak.
|
||||
struct Scratch(PathBuf);
|
||||
|
||||
impl Scratch {
|
||||
fn new(tag: &str) -> Self {
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"fmk_ks_{}_{}_{:?}",
|
||||
tag,
|
||||
std::process::id(),
|
||||
std::thread::current().id()
|
||||
));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
Scratch(dir)
|
||||
}
|
||||
|
||||
fn write(&self, name: &str, body: &str) -> PathBuf {
|
||||
let p = self.0.join(name);
|
||||
let mut f = std::fs::File::create(&p).unwrap();
|
||||
f.write_all(body.as_bytes()).unwrap();
|
||||
p
|
||||
}
|
||||
|
||||
fn path(&self, name: &str) -> PathBuf {
|
||||
self.0.join(name)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Scratch {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Bare `DiscInputs` keyed only by disc hash — the lookup key a keydb uses.
|
||||
fn inputs(hash: &str) -> DiscInputs {
|
||||
DiscInputs {
|
||||
disc_hash: hash.into(),
|
||||
volume_id: [0u8; 16],
|
||||
mkb: Vec::new(),
|
||||
unit_key_ro: Vec::new(),
|
||||
samples: Vec::new(),
|
||||
volume_label: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Drain a source completely into the ordered list of candidates it yields.
|
||||
/// `Key` does not implement `PartialEq`, so callers compare via [`tags`].
|
||||
fn drain(src: &mut dyn KeySource, inp: &DiscInputs) -> Vec<Key> {
|
||||
let mut out = Vec::new();
|
||||
while let Some(k) = src.next_key(inp) {
|
||||
out.push(k);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// A comparable fingerprint for a `Key` (which has no `PartialEq`): the variant
|
||||
/// plus its first identifying byte, enough to assert ordering deterministically.
|
||||
fn tag(k: &Key) -> (u8, u8) {
|
||||
match k {
|
||||
Key::Device(_) => (0, 0),
|
||||
Key::Processing(p) => (1, p.first().map(|b| b[0]).unwrap_or(0)),
|
||||
Key::Media(m) => (2, m.first().map(|b| b[0]).unwrap_or(0)),
|
||||
Key::Volume(v) => (3, v[0]),
|
||||
Key::Unit(u) => (4, u.first().map(|(_, b)| b[0]).unwrap_or(0)),
|
||||
// `Key` is #[non_exhaustive]; any future variant gets a distinct tag.
|
||||
_ => (255, 0),
|
||||
}
|
||||
}
|
||||
|
||||
fn tags(ks: &[Key]) -> Vec<(u8, u8)> {
|
||||
ks.iter().map(tag).collect()
|
||||
}
|
||||
|
||||
// ── KeydbSource: real-file lookup by disc hash ──────────────────────────────
|
||||
|
||||
const DISC_HASH: &str = "0xaabbccddaabbccddaabbccddaabbccddaabbccdd";
|
||||
|
||||
/// A `keydb.cfg` with one per-disc entry (VUK only) plus a universal DK pool.
|
||||
/// `0xHASH = TITLE | V | 0xVUK` is the disc-entry shape libfreemkv parses.
|
||||
fn keydb_with_disc_entry() -> String {
|
||||
format!(
|
||||
"; fixture keydb\n\
|
||||
| DK | DEVICE_KEY 0x{dk} | DEVICE_NODE 0x0001 | KEY_UV 0x00000002 | KEY_U_MASK_SHIFT 0x00\n\
|
||||
{hash} = FIXTURE_DISC | V | 0x{vuk}\n",
|
||||
dk = "22".repeat(16),
|
||||
hash = DISC_HASH,
|
||||
vuk = "11".repeat(16),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keydb_source_looks_up_disc_by_hash_from_file() {
|
||||
let s = Scratch::new("keydb_hit");
|
||||
let path = s.write("keydb.cfg", &keydb_with_disc_entry());
|
||||
|
||||
let mut src = KeydbSource::new(&path);
|
||||
let cands = drain(&mut src, &inputs(DISC_HASH));
|
||||
|
||||
// The disc's own VUK (hash hit) must be the FIRST candidate, ahead of the
|
||||
// universal device-key pool fallback.
|
||||
assert!(
|
||||
matches!(cands.first(), Some(Key::Volume(v)) if *v == [0x11u8; 16]),
|
||||
"per-disc VUK from the hash hit must rank first, got {cands:?}"
|
||||
);
|
||||
assert!(
|
||||
cands.iter().any(|k| matches!(k, Key::Device(_))),
|
||||
"the universal device-key pool is still offered as a fallback"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keydb_source_hash_miss_yields_only_universal_pool() {
|
||||
let s = Scratch::new("keydb_miss");
|
||||
let path = s.write("keydb.cfg", &keydb_with_disc_entry());
|
||||
|
||||
// A different disc: no per-disc entry, so no Volume/Unit candidate — only
|
||||
// the universal DK pool the library walks against the disc's own MKB.
|
||||
let mut src = KeydbSource::new(&path);
|
||||
let cands = drain(
|
||||
&mut src,
|
||||
&inputs("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"),
|
||||
);
|
||||
|
||||
assert_eq!(cands.len(), 1, "hash miss offers only the universal pool");
|
||||
assert!(matches!(cands[0], Key::Device(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keydb_source_missing_file_is_silent_not_errored() {
|
||||
// A missing keydb is not an error — it simply offers no candidates, so a
|
||||
// later source in the chain can still supply the key.
|
||||
let mut src = KeydbSource::new("/nonexistent/path/keydb.cfg");
|
||||
assert!(src.next_key(&inputs(DISC_HASH)).is_none());
|
||||
assert!(!src.errored(), "a missing keydb must not flag errored()");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keydb_source_label_and_needs_samples() {
|
||||
let src = KeydbSource::new("/nonexistent/keydb.cfg");
|
||||
assert_eq!(src.label(), "keydb");
|
||||
// A keydb can hand out a terminal Key::Unit applied as-is, so it must demand
|
||||
// ciphertext samples for validation.
|
||||
assert!(src.needs_samples());
|
||||
}
|
||||
|
||||
// ── KeydbSource: host-cert serving from a file ──────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn keydb_source_serves_host_cert_from_hc_row() {
|
||||
let s = Scratch::new("keydb_hc");
|
||||
// `| HC |` row with all-zero placeholder material (never a real key) — same
|
||||
// convention libfreemkv's own parse_host_cert test uses.
|
||||
let line = format!(
|
||||
"| HC | HOST_PRIV_KEY 0x{} | HOST_CERT 0x{}\n",
|
||||
"00".repeat(20),
|
||||
"00".repeat(92)
|
||||
);
|
||||
let path = s.write("keydb.cfg", &line);
|
||||
|
||||
let src = KeydbSource::new(&path);
|
||||
// Both the inherent method and the trait method must surface the cert — the
|
||||
// OEM/AACS cert-auth route collects through the trait.
|
||||
let inherent = src.host_certs();
|
||||
let via_trait = KeySource::host_certs(&src);
|
||||
assert_eq!(inherent.len(), 1, "inherent host_certs sees the HC row");
|
||||
assert_eq!(via_trait.len(), 1, "trait host_certs sees the HC row");
|
||||
assert_eq!(via_trait[0].certificate.len(), 92);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keydb_source_host_certs_empty_when_file_missing() {
|
||||
let src = KeydbSource::new("/nonexistent/keydb.cfg");
|
||||
assert!(src.host_certs().is_empty());
|
||||
assert!(KeySource::host_certs(&src).is_empty());
|
||||
}
|
||||
|
||||
// ── path policy: exe-local, local-only ──────────────────────────────────────
|
||||
|
||||
/// The exe-local keydb path, computed the way the module does — under
|
||||
/// `cargo test` `current_exe()` is the integration-test binary under `target/`.
|
||||
fn expected_local() -> Option<PathBuf> {
|
||||
std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|exe| exe.parent().map(|dir| dir.join("keydb.cfg")))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_paths_is_exactly_exe_local() {
|
||||
let paths = keydb_search_paths();
|
||||
match expected_local() {
|
||||
Some(expected) => assert_eq!(
|
||||
paths,
|
||||
vec![expected],
|
||||
"search list must be exactly [<exe dir>/keydb.cfg], no OS fallback"
|
||||
),
|
||||
None => assert!(paths.is_empty(), "no exe dir → empty, never an OS fallback"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_path_matches_search_head() {
|
||||
assert_eq!(default_keydb_path(), expected_local());
|
||||
assert_eq!(
|
||||
default_keydb_path(),
|
||||
keydb_search_paths().into_iter().next()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn existing_keydb_path_reflects_disk_state() {
|
||||
// The exe-local path almost certainly does not exist under target/ during a
|
||||
// test run; existing_keydb_path() returns Some only when the head path is
|
||||
// actually present on disk. Assert the two agree.
|
||||
let head_exists = keydb_search_paths()
|
||||
.first()
|
||||
.map(|p| p.exists())
|
||||
.unwrap_or(false);
|
||||
assert_eq!(existing_keydb_path().is_some(), head_exists);
|
||||
}
|
||||
|
||||
// ── MapfileSource: persisted unit keys read back from a mapfile ─────────────
|
||||
|
||||
/// A minimal ddrescue-style mapfile carrying two persisted unit keys in its
|
||||
/// `# freemkv-uk:` header plus one data line (the "current state" + region).
|
||||
fn mapfile_with_keys() -> String {
|
||||
"# Rescue Logfile. Created by freemkv test\n\
|
||||
# freemkv-uk: 0:11111111111111111111111111111111\n\
|
||||
# freemkv-uk: 1:22222222222222222222222222222222\n\
|
||||
0x0 0x200 +\n"
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapfile_source_reads_persisted_unit_keys() {
|
||||
let s = Scratch::new("mapfile_keys");
|
||||
let path = s.write("rip.mapfile", &mapfile_with_keys());
|
||||
|
||||
let mut src = MapfileSource::new(&path);
|
||||
// MapfileSource ignores DiscInputs — disc identity is implicit in the path.
|
||||
let first = src.next_key(&inputs("ignored"));
|
||||
match first {
|
||||
Some(Key::Unit(uks)) => {
|
||||
assert_eq!(
|
||||
uks,
|
||||
vec![(0u32, [0x11u8; 16]), (1u32, [0x22u8; 16])],
|
||||
"both persisted unit keys must be read back, in order"
|
||||
);
|
||||
}
|
||||
other => panic!("expected terminal Key::Unit from the mapfile, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapfile_source_is_one_shot() {
|
||||
let s = Scratch::new("mapfile_oneshot");
|
||||
let path = s.write("rip.mapfile", &mapfile_with_keys());
|
||||
|
||||
let mut src = MapfileSource::new(&path);
|
||||
assert!(
|
||||
src.next_key(&inputs("x")).is_some(),
|
||||
"first ask yields the UK set"
|
||||
);
|
||||
assert!(
|
||||
src.next_key(&inputs("x")).is_none(),
|
||||
"the mapfile holds exactly one UK set — a second ask is exhausted"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapfile_source_missing_or_keyless_offers_nothing() {
|
||||
// Missing file: silent None, not an error.
|
||||
let mut missing = MapfileSource::new("/nonexistent/rip.mapfile");
|
||||
assert!(missing.next_key(&inputs("x")).is_none());
|
||||
assert!(!missing.errored());
|
||||
|
||||
// A mapfile with NO freemkv-uk header (unresolved / VID-only): nothing.
|
||||
let s = Scratch::new("mapfile_keyless");
|
||||
let path = s.write(
|
||||
"rip.mapfile",
|
||||
"# Rescue Logfile. Created by freemkv test\n0x0 0x200 +\n",
|
||||
);
|
||||
let mut keyless = MapfileSource::new(&path);
|
||||
assert!(
|
||||
keyless.next_key(&inputs("x")).is_none(),
|
||||
"a keyless mapfile offers no candidate"
|
||||
);
|
||||
}
|
||||
|
||||
// ── OnlineSource: validation + unconfigured no-op (no network in CI) ─────────
|
||||
|
||||
#[test]
|
||||
fn online_source_unconfigured_is_silent_no_op() {
|
||||
// Empty base URL → a clean "no service" None, no network, not an error.
|
||||
let mut src = OnlineSource::new("", "");
|
||||
assert!(src.next_key(&inputs(DISC_HASH)).is_none());
|
||||
assert!(
|
||||
!src.errored(),
|
||||
"an unconfigured online source is not errored"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn online_source_one_shot_after_unconfigured_ask() {
|
||||
let mut src = OnlineSource::new("", "");
|
||||
assert!(src.next_key(&inputs(DISC_HASH)).is_none());
|
||||
// `asked` latched — a second ask is a no-op None regardless.
|
||||
assert!(src.next_key(&inputs(DISC_HASH)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn online_source_metadata() {
|
||||
let src = OnlineSource::new("https://example.invalid/keys", "tok");
|
||||
assert_eq!(src.label(), "online");
|
||||
assert!(
|
||||
src.needs_samples(),
|
||||
"the key service validates against ciphertext"
|
||||
);
|
||||
// No host-cert serving today — a no-op empty, no network touched.
|
||||
assert!(KeySource::host_certs(&src).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_keyserver_url_gates_scheme_and_ssrf() {
|
||||
// Public literal IP (no DNS) passes.
|
||||
assert!(validate_keyserver_url("https://8.8.8.8/keys").is_ok());
|
||||
// Internal / metadata / bad-scheme are rejected at config time.
|
||||
assert!(validate_keyserver_url("http://127.0.0.1/keys").is_err());
|
||||
assert!(validate_keyserver_url("http://169.254.169.254/latest/meta-data/").is_err());
|
||||
assert!(validate_keyserver_url("http://[::1]:9000/keys").is_err());
|
||||
assert!(validate_keyserver_url("ftp://example.com/keys").is_err());
|
||||
assert!(validate_keyserver_url("").is_err());
|
||||
}
|
||||
|
||||
// ── MultiSource: ordering, precedence, aggregation, nesting ─────────────────
|
||||
|
||||
/// A scripted source for composition tests: yields its queued keys in order,
|
||||
/// then None, and reports its configured `needs_samples`/`errored`.
|
||||
struct ScriptedSource {
|
||||
queue: std::vec::IntoIter<Key>,
|
||||
needs_samples: bool,
|
||||
errored: bool,
|
||||
label: &'static str,
|
||||
}
|
||||
|
||||
impl ScriptedSource {
|
||||
fn new(label: &'static str, keys: Vec<Key>) -> Self {
|
||||
Self {
|
||||
queue: keys.into_iter(),
|
||||
needs_samples: false,
|
||||
errored: false,
|
||||
label,
|
||||
}
|
||||
}
|
||||
fn with_needs_samples(mut self, v: bool) -> Self {
|
||||
self.needs_samples = v;
|
||||
self
|
||||
}
|
||||
fn with_errored(mut self, v: bool) -> Self {
|
||||
self.errored = v;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl KeySource for ScriptedSource {
|
||||
fn next_key(&mut self, _inputs: &DiscInputs) -> Option<Key> {
|
||||
self.queue.next()
|
||||
}
|
||||
fn needs_samples(&self) -> bool {
|
||||
self.needs_samples
|
||||
}
|
||||
fn errored(&self) -> bool {
|
||||
self.errored
|
||||
}
|
||||
fn label(&self) -> &'static str {
|
||||
self.label
|
||||
}
|
||||
}
|
||||
|
||||
fn vol(b: u8) -> Key {
|
||||
Key::Volume([b; 16])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_source_preserves_caller_order() {
|
||||
// Caller supplies [A, B]; MultiSource must exhaust A fully before B,
|
||||
// preserving within-source order — the "which sources, in what order"
|
||||
// policy lives entirely with the caller.
|
||||
let a = ScriptedSource::new("A", vec![vol(0xa1), vol(0xa2)]);
|
||||
let b = ScriptedSource::new("B", vec![vol(0xb1)]);
|
||||
let mut multi = MultiSource::new(vec![Box::new(a), Box::new(b)]);
|
||||
|
||||
let got = drain(&mut multi, &inputs("x"));
|
||||
assert_eq!(
|
||||
tags(&got),
|
||||
tags(&[vol(0xa1), vol(0xa2), vol(0xb1)]),
|
||||
"A is exhausted (in order) before B is consulted"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_source_order_is_reversible() {
|
||||
// The SAME two sources in the opposite order yield the opposite precedence —
|
||||
// proving the order is the caller's, not baked in.
|
||||
let a = ScriptedSource::new("A", vec![vol(0xa1)]);
|
||||
let b = ScriptedSource::new("B", vec![vol(0xb1)]);
|
||||
let mut multi = MultiSource::new(vec![Box::new(b), Box::new(a)]);
|
||||
|
||||
let got = drain(&mut multi, &inputs("x"));
|
||||
assert_eq!(
|
||||
tags(&got),
|
||||
tags(&[vol(0xb1), vol(0xa1)]),
|
||||
"B-first ordering wins"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_source_skips_empty_sources() {
|
||||
// An empty source in the middle is transparently skipped to the next.
|
||||
let empty = ScriptedSource::new("empty", vec![]);
|
||||
let real = ScriptedSource::new("real", vec![vol(0xc1)]);
|
||||
let mut multi = MultiSource::new(vec![Box::new(empty), Box::new(real)]);
|
||||
|
||||
assert_eq!(tags(&drain(&mut multi, &inputs("x"))), tags(&[vol(0xc1)]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_source_aggregates_needs_samples_and_errored() {
|
||||
// needs_samples / errored are OR-aggregated across the composed sources.
|
||||
let plain = ScriptedSource::new("plain", vec![]);
|
||||
let sampler = ScriptedSource::new("sampler", vec![]).with_needs_samples(true);
|
||||
let multi = MultiSource::new(vec![Box::new(plain), Box::new(sampler)]);
|
||||
assert!(
|
||||
multi.needs_samples(),
|
||||
"any source needing samples propagates"
|
||||
);
|
||||
|
||||
let ok = ScriptedSource::new("ok", vec![]);
|
||||
let bad = ScriptedSource::new("bad", vec![]).with_errored(true);
|
||||
let multi2 = MultiSource::new(vec![Box::new(ok), Box::new(bad)]);
|
||||
assert!(multi2.errored(), "any errored source propagates");
|
||||
|
||||
// All-clean composition reports neither.
|
||||
let c1 = ScriptedSource::new("c1", vec![]);
|
||||
let c2 = ScriptedSource::new("c2", vec![]);
|
||||
let clean = MultiSource::new(vec![Box::new(c1), Box::new(c2)]);
|
||||
assert!(!clean.needs_samples());
|
||||
assert!(!clean.errored());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_source_nests() {
|
||||
// MultiSource is itself a KeySource, so it composes inside another
|
||||
// MultiSource — inner [A, B] then outer C: order A, B, C.
|
||||
let a = ScriptedSource::new("A", vec![vol(0xa1)]);
|
||||
let b = ScriptedSource::new("B", vec![vol(0xb1)]);
|
||||
let inner = MultiSource::new(vec![Box::new(a), Box::new(b)]);
|
||||
let c = ScriptedSource::new("C", vec![vol(0xc1)]);
|
||||
let mut outer = MultiSource::new(vec![Box::new(inner), Box::new(c)]);
|
||||
|
||||
assert_eq!(
|
||||
tags(&drain(&mut outer, &inputs("x"))),
|
||||
tags(&[vol(0xa1), vol(0xb1), vol(0xc1)]),
|
||||
"nested MultiSource preserves the flattened caller order"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_source_real_keydb_then_mapfile_precedence() {
|
||||
// End-to-end precedence with the REAL sources over fixture files: a
|
||||
// keydb-first chain hands the keydb's per-disc VUK ahead of the mapfile's
|
||||
// terminal UK. (Resume chains flip this to [Mapfile, Keydb].)
|
||||
let s = Scratch::new("multi_real");
|
||||
let keydb = s.write("keydb.cfg", &keydb_with_disc_entry());
|
||||
let map = s.write("rip.mapfile", &mapfile_with_keys());
|
||||
|
||||
let mut multi = MultiSource::new(vec![
|
||||
Box::new(KeydbSource::new(&keydb)),
|
||||
Box::new(MapfileSource::new(&map)),
|
||||
]);
|
||||
let got = drain(&mut multi, &inputs(DISC_HASH));
|
||||
|
||||
// First candidate is the keydb's VUK (hash hit), proving keydb precedes the
|
||||
// mapfile; the mapfile's terminal Unit appears later in the chain.
|
||||
assert!(
|
||||
matches!(got.first(), Some(Key::Volume(v)) if *v == [0x11u8; 16]),
|
||||
"keydb VUK leads the keydb-first chain, got {got:?}"
|
||||
);
|
||||
assert!(
|
||||
got.iter()
|
||||
.any(|k| matches!(k, Key::Unit(uks) if uks.contains(&(0u32, [0x11u8; 16])))),
|
||||
"the mapfile's terminal unit keys follow once the keydb is exhausted"
|
||||
);
|
||||
// KeydbSource needs samples → the composed chain demands them too.
|
||||
assert!(multi.needs_samples());
|
||||
}
|
||||
|
||||
/// A keydb file that exists but exercises the `Path`-typed constructor with a
|
||||
/// borrowed path (the apps pass `&Path`/`PathBuf` interchangeably).
|
||||
#[test]
|
||||
fn keydb_source_accepts_borrowed_path() {
|
||||
let s = Scratch::new("keydb_borrow");
|
||||
let path: &Path = &s.path("keydb.cfg");
|
||||
std::fs::write(path, keydb_with_disc_entry()).unwrap();
|
||||
let mut src = KeydbSource::new(path);
|
||||
assert!(src.next_key(&inputs(DISC_HASH)).is_some());
|
||||
}
|
||||
Reference in New Issue
Block a user