Compare commits

..
Author SHA1 Message Date
Matthew Jackson 1da51fdbf5 build(deps): bump actions/checkout from 5 to 7 in the all group (#13)
Bumps the all group with 1 update: [actions/checkout](https://github.com/actions/checkout).

Updates `actions/checkout` from 5 to 7
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: all
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-03 11:16:01 -07:00
Matthew Jackson 9f5116cc26 chore(dependabot): monthly grouped version updates 2026-10-02 20:24:31 -07:00
Matthew Jackson 3c6ef54cf3 tests: loopback idle bound T_IDLE is 1 s, so a slow macOS runner's scheduling gaps stay inside every margin 2026-10-01 18:06:06 -07:00
Matthew Jackson 73c7f05b4b tests: Stop tests against a silent stub idle T_STOP_IDLE (2 s), past the Stop 2026-10-01 17:20:54 -07:00
Matthew Jackson e6320d52c9 tests: the stopped reachability query idles 2 s and gets 10 s to drain 2026-10-01 16:54:40 -07:00
Matthew Jackson 49cf396c07 tests: the stopped reachability query idles past the Stop, so only the Stop ends it 2026-10-01 16:53:58 -07:00
Matthew Jackson 940bb215ab online: follow libfreemkv's halt::Progress rename to Liveness 2026-09-30 20:36:55 -07:00
Matthew Jackson 18b590407c Cargo.lock: restore (accidentally changed by local patch) 2026-09-30 20:24:57 -07:00
Matthew Jackson 116b33739b tests: halt-landing tests assert Halted, not a tight wall-clock bound 2026-09-30 20:24:46 -07:00
Matthew Jackson d935fa6ffd online: allow LAN/loopback/link-local key-service hosts, reuse libfreemkv is_blocked_ip 2026-09-30 20:03:38 -07:00
Matthew Jackson b65959d3a1 ci: run cargo-deny from the repo dir so the .cargo patches apply 2026-09-30 16:09:03 -07:00
Matthew Jackson 3f08e980a0 ci: path-patch freemkv-unlock; dependabot ignores org workflow refs
libfreemkv now deps freemkv-unlock by version with a git-tag override, so every root that reaches it patches it. Dependabot config unified; it no longer proposes freemkv/.github v1->v2.
2026-09-30 15:53:05 -07:00
Matthew Jackson 6c8e2c5c46 Allow forensic phase fallback after definitive key service misses 2026-09-29 17:20:43 -07:00
Matthew Jackson ba357e46f5 online: wire ctx.halt()/ctx.progress() into the query worker (ST-K1b) 2026-09-28 08:18:38 -07:00
Matthew Jackson a815a0a578 online: ST-K1b tests for ctx.halt()/ctx.progress() wiring (RED) 2026-09-28 08:15:20 -07:00
Matthew Jackson ba6f213be3 KU-E1b: the key-service query is not yet Stop-aware (wording)
check_keyserver_url_static's rustdoc and its test comment no longer
call the first query halt-aware: OnlineSource runs it under its own
Halt until ST-K1b wires ctx.halt() (J10).
2026-09-28 07:18:27 -07:00
Matthew Jackson 37568b1002 test: use the RFC 2606 .test TLD for the unresolvable host (leak-guard flags .invalid) 2026-09-28 07:04:48 -07:00
Matthew Jackson 40389acd3f KU-E1 review minor 4: partial stored keys plus a Km still report NoVid
The keydb keeps a NoVid miss reason beside the keys it did return, so
resolve sets vid_would_help for the CPS units only the VID could
derive. Other miss reasons are still reported only with no key.
2026-09-28 06:28:54 -07:00
Matthew Jackson 54508d8ec2 KU-E1 review minor 4 red: partial stored keys plus a Km still report NoVid
A keydb entry with stored unit keys for some CPS units and a Media Key
but no VID returns the stored keys with an empty miss path, so
resolve cannot tell that the VID would derive the rest (J23).
2026-09-28 06:28:47 -07:00
Matthew Jackson 2c93420970 KU-E1: a DNS-free key-service URL check for factory builds
check_keyserver_url_static rejects every Config fault (scheme, host,
port, a literal non-public address) with no lookup. A factory build,
which no Stop can reach, uses it; the host lookup and its SSRF guard
run at the first query on the halt-aware worker, as they already did.
A test-only counter proves the check starts no lookup.
2026-09-28 06:24:52 -07:00
Matthew Jackson c48faa042b KU-E1 red: a DNS-free key-service URL check for factory builds
The engine checks the key-service URL, a DNS lookup with a fixed 10 s
timeout that Stop cannot interrupt, on every factory build, and no
Halt reaches a factory. The test asks for a static check that rejects
Config faults with no lookup.
2026-09-28 05:45:58 -07:00
Matthew Jackson ab93818012 KU-E1 J23: OnlineSource and MultiSource report uses_vid
OnlineSource::uses_vid is true (it sends vid_b64); MultiSource is
any(inner); KeydbSource keeps the default false.
2026-09-28 05:31:51 -07:00
Matthew Jackson f3b66307cc KU-E1 J23 red: which key sources consume the disc's VID
OnlineSource sends the VID (vid_b64), so it uses it; MultiSource uses
it if any inner source does; KeydbSource does not (its Km-without-VID
case is reported per disc, as a NoVid miss path).
2026-09-28 05:31:23 -07:00
Matthew Jackson ad59f3ce45 KU-K1 review: peek the reachability slot, MultiSource forwards last_failure
record_last_failure_transport now reads LAST_DECODE_REACHABILITY.with(|c|
c.get()) directly instead of take_last_decode_reachability(), so it no longer
erases the slot freemkv-library's server reads after resolve
(server/ripper/mod.rs:505, :1361) to avoid a second key-service call. Green:
the regression test now finds the verdict still in the slot after
get_unit_keys/get_fmts_indexes. MultiSource::last_failure_was_transport
forwards any(inner), matching answer_depends_on_samples (KU3-6).
2026-09-28 04:19:04 -07:00
Matthew Jackson 4e83b3c1f8 KU-K1 review: red — record_last_failure_transport erases the reachability slot
Opus's review: freemkv-library's server calls take_online_decode_reachability()
after resolve (server/ripper/mod.rs:505, :1361) to classify a no-key without a
second probe. record_last_failure_transport currently TAKES the thread-local
slot, so the server sees None and fires probe_online_reachability — a second
key-service call, and a definite 422 misread as an outage. Red: the slot is
gone after get_unit_keys/get_fmts_indexes. Also adds the forensic-path,
mid-body-stall, transport-then-5xx-reset and MultiSource forwarding tests
requested in review; the latter fails since MultiSource has no override yet.
2026-09-28 04:18:02 -07:00
Matthew Jackson c5985d1c87 KU-K1: keydb/multi sample-dependence and online transport-class failures
Per keys-upfront-design KU-K1 (§8.2): KeydbSource::answer_depends_on_samples
is false (keyed by disc hash, asked once per resolve, N-KU10); MultiSource's
is any(inner) (KU3-6), so a nested sample-dependent source still gets the
per-piece ask. OnlineSource::last_failure_was_transport (J15) is true only
when the most recent query recorded DecodeReachability::Transport (DNS,
connect refused, or an idle stall — never an HTTP answer, 5xx included), and
resets on the next success, so resolve's J13 retry (stop-design-v5 T31)
targets exactly the requests that never got an answer.
2026-09-28 04:13:03 -07:00
Matthew Jackson 8f923fd5f9 KU-K1: tests for answer_depends_on_samples and last_failure_was_transport
KeydbSource::answer_depends_on_samples (KSK1) and MultiSource::answer_depends_on_samples
(KSK2, KU3-6 "any(inner)") both default to `true` today, so a keydb-then-online
composition would be re-asked per piece instead of once. OnlineSource::last_failure_was_transport
(J15) defaults to `false`, so a transient DNS/connect/idle failure would never be
retried by `resolve` (J13). Red: these six assertions fail against today's defaults.
2026-09-28 04:12:12 -07:00
Matthew Jackson 18fdab6b77 KU-P1: decrypt the real-unit test through test_util::decrypt_unit
aacs::content::decrypt_unit becomes pub(crate) at KU-X2 (keys-upfront design
v3.4 §2.2); the ignored real-unit test now uses the test-util door. The
feature comes in as a dev-dependency (resolved through the sibling path patch).
2026-09-28 00:22:08 -07:00
Matthew Jackson dfa02adbe2 online: a stopped key query never sends and builds no body
Review fixes, green half:
- PostJob carries the caller's Halt and re-checks it after the host lookup,
  before connecting. A Stop during DNS then never uploads the key material
  or bearer token, so a reopen's query is the only one the service sees.
- query_with returns Halted before reading or base64-encoding the MKB when
  a Stop is already pending.
2026-09-27 23:50:33 -07:00
Matthew Jackson cbab284c85 online: tests that a stopped key query sends nothing and reads no disc material
Review fixes, red half: a Stop during the host lookup must never reach the
service (never call the key service twice), and a pending Stop returns
before the body is built. KT7a's u64::MAX check now asserts saturation.
2026-09-27 23:49:08 -07:00
Matthew Jackson 0ffdb2a9f6 online: stall-only key-service timeouts and mid-flight Stop (ST-K1a)
Commit 2 of 2 (stop-design-v5 §2.7, T15-T17, D3, D4):

- query_with builds the body on the caller's thread and runs DNS + POST on
  a worker, capped at 4 per host. The caller waits for a slot and for the
  result halt-aware in 20 ms slices, so Stop returns Halted within a slice
  at any stage. The abandoned worker frees its slot at its own stall bound.
- Reachability is recorded on the caller's thread, only when the worker's
  result arrives. A Stop records nothing.
- IdleConnector, chained after DefaultConnector, owns every post-connect
  bound. Each write and read gets a fresh 60 s stall bound (T16), and the
  wait for a reply's first byte gets 60 s + request bytes / 256 KiB/s (T17).
  The 180 s phase totals are gone: no total is configured.
- A reply body that stalls or drops records Transport reachability (T16).
  An over-cap body still records its Status.

The trait path passes a fresh Halt. ctx.halt()/ctx.progress() is ST-K1b,
after KU-X2. No public API change.
2026-09-27 23:41:43 -07:00
Matthew Jackson adbb1a12a0 online: ST-K1a tests for stall-only key-service timeouts and mid-flight Stop
Commit 1 of 2 (stop-design-v5 §5.0): KT1-KT10 plus guards G12/G13, with
signatures that keep today's behaviour (query_with ignores the halt,
hardened_agent_with maps idle onto the old phase totals, the first-byte
budget does not scale), so each test compiles and fails on its assertion.
KT9 replaces the 180 s totals test. The ctx.halt()/ctx.progress() wiring
is ST-K1b (after KU-X2).
2026-09-27 23:37:22 -07:00
Matthew Jackson 05281249d1 keydb: hand out host certs best first, with no disc input
Unannotated certs in keydb order, then revoked ones by latest revocation.
The trait ignores its mkb hint; KeyDb::host_certs(mkb) is deprecated.
2026-09-27 17:24:04 -07:00
Matthew Jackson cedbc4b1fa online: offline Temporary-verdict test; proxy wording
Drop the real-DNS .invalid test for an offline check of every GuardFail::Unreachable reason; say every key lookup failed.
2026-09-27 16:44:34 -07:00
Matthew Jackson 1606ff3ca3 online: review nits for the proxy fix and keyserver URL verdict
Correct the rationale: the env proxy was resolved to the key service's pinned address, so every lookup failed.
Gate set_last_decode_reachability behind a test-hooks feature, mark the verdict types non_exhaustive, test a .invalid host, add CHANGELOG.
2026-09-27 12:40:09 -07:00
Matthew Jackson 10b0348db8 online: never use an env proxy; typed keyserver URL verdict and reachability test hook
The key-service agent disables ureq's env proxy, which would re-resolve the host and bypass the pinned, SSRF-guarded addresses.
check_keyserver_url returns a Permanent/Temporary KeyserverUrlRejection; set_last_decode_reachability plants a verdict for caller tests.
2026-09-27 12:26:39 -07:00
12 changed files with 5101 additions and 5098 deletions
+1 -7
View File
@@ -1,12 +1,6 @@
# Changelog
## [1.8.1] — Unreleased
### Changed
- Unit tests live in `*_tests.rs` side files. No behaviour change.
## [1.8.0] — 2026-10-05
## [Unreleased]
### Fixed
+5 -5
View File
@@ -1,6 +1,6 @@
[package]
name = "freemkv-keysources"
version = "1.8.0"
version = "1.7.7"
edition = "2024"
rust-version = "1.88"
license = "MIT"
@@ -17,7 +17,7 @@ publish = false
# The crate provides the `KeySource` trait + `Key`/`DiscInputs` types these impls fill.
# libfreemkv is git-only now; the committed [patch.crates-io] below redirects this
# bare version req to the libfreemkv git tag (local dev overrides it to a path).
libfreemkv = "1.8"
libfreemkv = "1.7"
# OnlineSource: POST disc inputs + samples to a key service over HTTP.
ureq = { version = "3", features = ["json"] }
serde_json = "1"
@@ -37,7 +37,7 @@ test-hooks = []
[dev-dependencies]
# `test_util` fixtures (`decrypt_unit`, …) for this crate's tests (keys-upfront
# KU-P1). CI resolves it through the sibling path patch.
libfreemkv = { version = "1.8", features = ["test-util"] }
libfreemkv = { version = "1.7", features = ["test-util"] }
[profile.release]
lto = "thin"
@@ -49,8 +49,8 @@ codegen-units = 1
# overrides it with a path patch via the gitignored .cargo/config.toml (a
# config-level [patch.crates-io] wins over this manifest one for the same crate).
[patch.crates-io]
libfreemkv = { git = "https://github.com/freemkv/libfreemkv", tag = "v1.8.0" }
freemkv-unlock = { git = "https://github.com/freemkv/freemkv-unlock", tag = "v1.8.0" }
libfreemkv = { git = "https://github.com/freemkv/libfreemkv", tag = "v1.7.7" }
freemkv-unlock = { git = "https://github.com/freemkv/freemkv-unlock", tag = "v1.7.7" }
[lints.clippy]
uninlined_format_args = "allow"
+1313 -2
View File
File diff suppressed because it is too large Load Diff
+1621 -2
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-1311
View File
File diff suppressed because it is too large Load Diff
+177 -2
View File
@@ -170,5 +170,180 @@ impl KeySource for MultiSource {
}
#[cfg(test)]
#[path = "lib_tests.rs"]
mod tests;
mod tests {
use super::*;
use libfreemkv::aacs::types::HostCert;
// A minimal KeySource that only supplies host certs — the sole MultiSource
// behaviour under test. It never resolves a unit key.
struct CertOnly {
certs: Vec<HostCert>,
}
impl KeySource for CertOnly {
fn get_unit_keys(&self, _ctx: &dyn ResolveCtx) -> Result<Vec<UnitKey>, libfreemkv::Error> {
Ok(Vec::new())
}
fn host_certs(&self, _mkb: Option<u32>) -> Vec<HostCert> {
self.certs.clone()
}
}
// A host cert tagged with a distinct marker byte so a returned set can be
// identified exactly. Synthetic bytes only; no real key material.
fn cert(marker: u8) -> HostCert {
HostCert {
private_key: [marker; 20],
certificate: vec![marker; 92],
private_key_v2: None,
certificate_v2: None,
}
}
fn boxed(certs: Vec<HostCert>) -> Box<dyn KeySource> {
Box::new(CertOnly { certs }) as Box<dyn KeySource>
}
/// `MultiSource::host_certs` must UNION every inner source's certs. Without
/// this a composed source hides an inner source's cert from the OEM
/// cert-auth route — the gap the union closes.
#[test]
fn multi_source_host_certs_unions_every_inner_source() {
let multi = MultiSource::new(vec![
boxed(vec![cert(0x01)]),
boxed(vec![cert(0x02), cert(0x03)]),
]);
let mut markers: Vec<u8> = multi
.host_certs(Some(50))
.iter()
.map(|c| c.certificate[0])
.collect();
markers.sort_unstable();
assert_eq!(
markers,
vec![0x01, 0x02, 0x03],
"certs from every inner source must be unioned"
);
}
/// With no inner source holding a cert, the union is empty (not an error).
#[test]
fn multi_source_host_certs_empty_when_no_source_has_one() {
let multi = MultiSource::new(vec![boxed(vec![]), boxed(vec![])]);
assert!(multi.host_certs(None).is_empty());
}
// A source whose `answer_depends_on_samples` is fixed to the given value —
// the only behaviour KSK2 exercises.
struct FixedDependency(bool);
impl KeySource for FixedDependency {
fn get_unit_keys(&self, _ctx: &dyn ResolveCtx) -> Result<Vec<UnitKey>, libfreemkv::Error> {
Ok(Vec::new())
}
fn answer_depends_on_samples(&self) -> bool {
self.0
}
}
// KSK2: `MultiSource::answer_depends_on_samples` is `any(inner)`, so nesting
// a sample-dependent online source under a sample-independent keydb still
// gets the per-piece ask (`resolve` must not treat the pair as keydb-only).
#[test]
fn multi_source_answer_depends_on_samples_is_any_inner() {
let all_independent = MultiSource::new(vec![
Box::new(FixedDependency(false)) as Box<dyn KeySource>,
Box::new(FixedDependency(false)) as Box<dyn KeySource>,
]);
assert!(!all_independent.answer_depends_on_samples());
let keydb_then_online = MultiSource::new(vec![
Box::new(FixedDependency(false)) as Box<dyn KeySource>,
Box::new(FixedDependency(true)) as Box<dyn KeySource>,
]);
assert!(keydb_then_online.answer_depends_on_samples());
}
// A source whose `uses_vid` is fixed to the given value.
struct FixedVid(bool);
impl KeySource for FixedVid {
fn get_unit_keys(&self, _ctx: &dyn ResolveCtx) -> Result<Vec<UnitKey>, libfreemkv::Error> {
Ok(Vec::new())
}
fn uses_vid(&self) -> bool {
self.0
}
}
// KU J23: like `answer_depends_on_samples`, a composed source uses the VID if any
// inner one does, so a nested online source still makes a Missing piece VID-derivable.
#[test]
fn multi_source_uses_vid_is_any_inner() {
let neither = MultiSource::new(vec![
Box::new(FixedVid(false)) as Box<dyn KeySource>,
Box::new(FixedVid(false)) as Box<dyn KeySource>,
]);
assert!(!neither.uses_vid());
let keydb_then_online = MultiSource::new(vec![
Box::new(FixedVid(false)) as Box<dyn KeySource>,
Box::new(FixedVid(true)) as Box<dyn KeySource>,
]);
assert!(keydb_then_online.uses_vid());
}
// A source whose `last_failure_was_transport` is fixed to the given value.
struct FixedTransport(bool);
impl KeySource for FixedTransport {
fn get_unit_keys(&self, _ctx: &dyn ResolveCtx) -> Result<Vec<UnitKey>, libfreemkv::Error> {
Ok(Vec::new())
}
fn last_failure_was_transport(&self) -> bool {
self.0
}
}
// For consistency with `answer_depends_on_samples` (KU3-6): a composed
// source's last-failure verdict is `any(inner)` too, so `resolve` retries
// the composition whenever ANY inner source's last failure was transport.
#[test]
fn multi_source_last_failure_was_transport_is_any_inner() {
let none_transport = MultiSource::new(vec![
Box::new(FixedTransport(false)) as Box<dyn KeySource>,
Box::new(FixedTransport(false)) as Box<dyn KeySource>,
]);
assert!(!none_transport.last_failure_was_transport());
let one_transport = MultiSource::new(vec![
Box::new(FixedTransport(false)) as Box<dyn KeySource>,
Box::new(FixedTransport(true)) as Box<dyn KeySource>,
]);
assert!(one_transport.last_failure_was_transport());
}
/// The `mkb` generation must reach each inner source so its OWN revocation
/// filter can act; a source that drops everything at a given generation is
/// honoured by the union.
#[test]
fn multi_source_host_certs_passes_mkb_through_to_inner_sources() {
struct GenAware;
impl KeySource for GenAware {
fn get_unit_keys(
&self,
_c: &dyn ResolveCtx,
) -> Result<Vec<UnitKey>, libfreemkv::Error> {
Ok(Vec::new())
}
fn host_certs(&self, mkb: Option<u32>) -> Vec<HostCert> {
match mkb {
// Modelled on a cert revoked in MKBv72.
Some(g) if g >= 72 => Vec::new(),
_ => vec![cert(0xaa)],
}
}
}
let multi = MultiSource::new(vec![Box::new(GenAware) as Box<dyn KeySource>]);
assert_eq!(multi.host_certs(Some(50)).len(), 1, "usable below gen 72");
assert!(
multi.host_certs(Some(72)).is_empty(),
"the gen must reach the inner source's filter"
);
}
}
-172
View File
@@ -1,172 +0,0 @@
use super::*;
use libfreemkv::aacs::types::HostCert;
// A minimal KeySource that only supplies host certs — the sole MultiSource
// behaviour under test. It never resolves a unit key.
struct CertOnly {
certs: Vec<HostCert>,
}
impl KeySource for CertOnly {
fn get_unit_keys(&self, _ctx: &dyn ResolveCtx) -> Result<Vec<UnitKey>, libfreemkv::Error> {
Ok(Vec::new())
}
fn host_certs(&self, _mkb: Option<u32>) -> Vec<HostCert> {
self.certs.clone()
}
}
// A host cert tagged with a distinct marker byte so a returned set can be
// identified exactly. Synthetic bytes only; no real key material.
fn cert(marker: u8) -> HostCert {
HostCert {
private_key: [marker; 20],
certificate: vec![marker; 92],
private_key_v2: None,
certificate_v2: None,
}
}
fn boxed(certs: Vec<HostCert>) -> Box<dyn KeySource> {
Box::new(CertOnly { certs }) as Box<dyn KeySource>
}
/// `MultiSource::host_certs` must UNION every inner source's certs. Without
/// this a composed source hides an inner source's cert from the OEM
/// cert-auth route — the gap the union closes.
#[test]
fn multi_source_host_certs_unions_every_inner_source() {
let multi = MultiSource::new(vec![
boxed(vec![cert(0x01)]),
boxed(vec![cert(0x02), cert(0x03)]),
]);
let mut markers: Vec<u8> = multi
.host_certs(Some(50))
.iter()
.map(|c| c.certificate[0])
.collect();
markers.sort_unstable();
assert_eq!(
markers,
vec![0x01, 0x02, 0x03],
"certs from every inner source must be unioned"
);
}
/// With no inner source holding a cert, the union is empty (not an error).
#[test]
fn multi_source_host_certs_empty_when_no_source_has_one() {
let multi = MultiSource::new(vec![boxed(vec![]), boxed(vec![])]);
assert!(multi.host_certs(None).is_empty());
}
// A source whose `answer_depends_on_samples` is fixed to the given value —
// the only behaviour KSK2 exercises.
struct FixedDependency(bool);
impl KeySource for FixedDependency {
fn get_unit_keys(&self, _ctx: &dyn ResolveCtx) -> Result<Vec<UnitKey>, libfreemkv::Error> {
Ok(Vec::new())
}
fn answer_depends_on_samples(&self) -> bool {
self.0
}
}
// KSK2: `MultiSource::answer_depends_on_samples` is `any(inner)`, so nesting
// a sample-dependent online source under a sample-independent keydb still
// gets the per-piece ask (`resolve` must not treat the pair as keydb-only).
#[test]
fn multi_source_answer_depends_on_samples_is_any_inner() {
let all_independent = MultiSource::new(vec![
Box::new(FixedDependency(false)) as Box<dyn KeySource>,
Box::new(FixedDependency(false)) as Box<dyn KeySource>,
]);
assert!(!all_independent.answer_depends_on_samples());
let keydb_then_online = MultiSource::new(vec![
Box::new(FixedDependency(false)) as Box<dyn KeySource>,
Box::new(FixedDependency(true)) as Box<dyn KeySource>,
]);
assert!(keydb_then_online.answer_depends_on_samples());
}
// A source whose `uses_vid` is fixed to the given value.
struct FixedVid(bool);
impl KeySource for FixedVid {
fn get_unit_keys(&self, _ctx: &dyn ResolveCtx) -> Result<Vec<UnitKey>, libfreemkv::Error> {
Ok(Vec::new())
}
fn uses_vid(&self) -> bool {
self.0
}
}
// KU J23: like `answer_depends_on_samples`, a composed source uses the VID if any
// inner one does, so a nested online source still makes a Missing piece VID-derivable.
#[test]
fn multi_source_uses_vid_is_any_inner() {
let neither = MultiSource::new(vec![
Box::new(FixedVid(false)) as Box<dyn KeySource>,
Box::new(FixedVid(false)) as Box<dyn KeySource>,
]);
assert!(!neither.uses_vid());
let keydb_then_online = MultiSource::new(vec![
Box::new(FixedVid(false)) as Box<dyn KeySource>,
Box::new(FixedVid(true)) as Box<dyn KeySource>,
]);
assert!(keydb_then_online.uses_vid());
}
// A source whose `last_failure_was_transport` is fixed to the given value.
struct FixedTransport(bool);
impl KeySource for FixedTransport {
fn get_unit_keys(&self, _ctx: &dyn ResolveCtx) -> Result<Vec<UnitKey>, libfreemkv::Error> {
Ok(Vec::new())
}
fn last_failure_was_transport(&self) -> bool {
self.0
}
}
// For consistency with `answer_depends_on_samples` (KU3-6): a composed
// source's last-failure verdict is `any(inner)` too, so `resolve` retries
// the composition whenever ANY inner source's last failure was transport.
#[test]
fn multi_source_last_failure_was_transport_is_any_inner() {
let none_transport = MultiSource::new(vec![
Box::new(FixedTransport(false)) as Box<dyn KeySource>,
Box::new(FixedTransport(false)) as Box<dyn KeySource>,
]);
assert!(!none_transport.last_failure_was_transport());
let one_transport = MultiSource::new(vec![
Box::new(FixedTransport(false)) as Box<dyn KeySource>,
Box::new(FixedTransport(true)) as Box<dyn KeySource>,
]);
assert!(one_transport.last_failure_was_transport());
}
/// The `mkb` generation must reach each inner source so its OWN revocation
/// filter can act; a source that drops everything at a given generation is
/// honoured by the union.
#[test]
fn multi_source_host_certs_passes_mkb_through_to_inner_sources() {
struct GenAware;
impl KeySource for GenAware {
fn get_unit_keys(&self, _c: &dyn ResolveCtx) -> Result<Vec<UnitKey>, libfreemkv::Error> {
Ok(Vec::new())
}
fn host_certs(&self, mkb: Option<u32>) -> Vec<HostCert> {
match mkb {
// Modelled on a cert revoked in MKBv72.
Some(g) if g >= 72 => Vec::new(),
_ => vec![cert(0xaa)],
}
}
}
let multi = MultiSource::new(vec![Box::new(GenAware) as Box<dyn KeySource>]);
assert_eq!(multi.host_certs(Some(50)).len(), 1, "usable below gen 72");
assert!(
multi.host_certs(Some(72)).is_empty(),
"the gen must reach the inner source's filter"
);
}
+1942 -2
View File
File diff suppressed because it is too large Load Diff
-1936
View File
File diff suppressed because it is too large Load Diff
+42 -2
View File
@@ -46,5 +46,45 @@ pub fn default_keydb_path() -> Option<PathBuf> {
}
#[cfg(test)]
#[path = "paths_tests.rs"]
mod tests;
mod tests {
use super::*;
/// The expected exe-local keydb path, computed the same way the code does.
/// Under `cargo test`, `current_exe()` is the 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_keydb() {
let paths = keydb_search_paths();
match expected_local() {
Some(expected) => {
assert_eq!(
paths,
vec![expected],
"search list must be exactly [<exe dir>/keydb.cfg]"
);
}
None => {
// No exe dir available → empty, no OS fallback (local only).
assert!(
paths.is_empty(),
"no exe dir means an empty search list, never an OS fallback"
);
}
}
}
#[test]
fn default_path_matches_search_head() {
// The write default is the single search entry, or None if unavailable.
assert_eq!(default_keydb_path(), expected_local());
assert_eq!(
default_keydb_path(),
keydb_search_paths().into_iter().next()
);
}
}
-40
View File
@@ -1,40 +0,0 @@
use super::*;
/// The expected exe-local keydb path, computed the same way the code does.
/// Under `cargo test`, `current_exe()` is the 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_keydb() {
let paths = keydb_search_paths();
match expected_local() {
Some(expected) => {
assert_eq!(
paths,
vec![expected],
"search list must be exactly [<exe dir>/keydb.cfg]"
);
}
None => {
// No exe dir available → empty, no OS fallback (local only).
assert!(
paths.is_empty(),
"no exe dir means an empty search list, never an OS fallback"
);
}
}
}
#[test]
fn default_path_matches_search_head() {
// The write default is the single search entry, or None if unavailable.
assert_eq!(default_keydb_path(), expected_local());
assert_eq!(
default_keydb_path(),
keydb_search_paths().into_iter().next()
);
}