fix(labels): make deluxe master-enum selection deterministic
identify_master_enums picks, for each fingerprint, the best candidate class out of CandidatePool. Its tie-break only prefers an exact ldc count over an inexact one, so two candidates that are BOTH inexact but both within LDC_COUNT_TOLERANCE are decided purely by iteration order — and the pool was a HashMap. Rust seeds HashMap per instance, so this is not merely unstable across runs: the new test resolves the SAME jar to both Alpha and Beta within a single process, across 16 iterations. The same disc could emit different commentary/SDH/descriptive labels on consecutive rips of unchanged input, with nothing in the output saying the choice was arbitrary. BTreeMap fixes it by construction rather than by a sort someone can forget to keep. The pool is capped at MAX_CANDIDATE_CLASSES, so the ordering cost is irrelevant. The test runs the whole identification sixteen times and asserts one distinct winner. A single run cannot distinguish deterministic from lucky, and the seed does not change within a process — so repetition is what makes this a test rather than a hope. Found by the round-9 labels pass, which was dispatched specifically because every one of the ten lenses had reported leaving deluxe.rs unread. 1,159 new lines that nobody had opened.
This commit is contained in:
+65
-2
@@ -62,7 +62,7 @@ use super::class_reader::{
|
||||
use super::{LabelPurpose, LabelQualifier, ParseResult, StreamLabel, StreamLabelType, jar, vocab};
|
||||
use crate::sector::SectorSource;
|
||||
use crate::udf::UdfFs;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
|
||||
pub fn detect(reader: &mut dyn SectorSource, udf: &UdfFs) -> bool {
|
||||
// The real signal is `com/bydeluxe/` inside a top-level jar's central
|
||||
@@ -268,7 +268,14 @@ const MAX_CANDIDATE_CLASSES: usize = 65536;
|
||||
/// [`MAX_CANDIDATE_CLASSES`].
|
||||
#[derive(Default)]
|
||||
struct CandidatePool {
|
||||
by_class: HashMap<String, Vec<String>>,
|
||||
/// Ordered, NOT a HashMap: `identify_master_enums` iterates this to pick a
|
||||
/// fingerprint's best match, and its tie-break only prefers an exact ldc
|
||||
/// count over an inexact one. Two candidates that are both inexact but
|
||||
/// within `LDC_COUNT_TOLERANCE` are therefore decided by iteration order —
|
||||
/// which for a `HashMap` is seeded per process, so the same disc could
|
||||
/// resolve a different master enum on a second run and emit different
|
||||
/// labels for unchanged input.
|
||||
by_class: BTreeMap<String, Vec<String>>,
|
||||
/// Retained bytes: class names plus every retained string.
|
||||
bytes: usize,
|
||||
}
|
||||
@@ -1547,6 +1554,62 @@ mod tests {
|
||||
assert_eq!(purpose.1.values[7], "NoForcedDescriptive");
|
||||
}
|
||||
|
||||
/// Two candidates that BOTH match a fingerprint's prefix and are BOTH
|
||||
/// within `LDC_COUNT_TOLERANCE` but neither exact must resolve the same
|
||||
/// way on every run.
|
||||
///
|
||||
/// The tie-break only prefers an exact ldc count over an inexact one, so
|
||||
/// between two inexact candidates the winner is whichever the pool yields
|
||||
/// first. Backed by a `HashMap` that is per-process seeded, meaning the
|
||||
/// same disc image could pick a different master enum on a second run and
|
||||
/// emit different commentary/SDH labels for byte-identical input — with
|
||||
/// nothing in the output to say the choice was arbitrary.
|
||||
///
|
||||
/// Running the whole identification repeatedly is what makes this a test
|
||||
/// rather than a hope: a single run cannot distinguish "deterministic"
|
||||
/// from "got lucky", and the hash seed does not change within one process.
|
||||
#[test]
|
||||
fn identify_master_enums_breaks_a_tie_between_two_inexact_candidates_deterministically() {
|
||||
// Both are prefix-matching Purpose candidates at diff 2 and 3 from the
|
||||
// expected count of 8 — inside LDC_COUNT_TOLERANCE (4), neither exact,
|
||||
// so the exact-count tie-break never fires and only ordering decides.
|
||||
let mut a_vals: Vec<&str> = vec!["Normal", "Commentary", "PiP", "Trivia"];
|
||||
let a_fill: Vec<String> = (0..6).map(|i| format!("Afill{i}")).collect(); // 10, diff 2
|
||||
a_vals.extend(a_fill.iter().map(String::as_str));
|
||||
let mut b_vals: Vec<&str> = vec!["Normal", "Commentary", "PiP", "Trivia"];
|
||||
let b_fill: Vec<String> = (0..7).map(|i| format!("Bfill{i}")).collect(); // 11, diff 3
|
||||
b_vals.extend(b_fill.iter().map(String::as_str));
|
||||
|
||||
let mut winners = std::collections::BTreeSet::new();
|
||||
for _ in 0..16 {
|
||||
let zip = build_zip(&[
|
||||
(
|
||||
"com/bydeluxe/Alpha.class",
|
||||
class_with_ldc_strings("AlphaPurpose", &a_vals),
|
||||
),
|
||||
(
|
||||
"com/bydeluxe/Beta.class",
|
||||
class_with_ldc_strings("BetaPurpose", &b_vals),
|
||||
),
|
||||
]);
|
||||
let mut archive = open_jar(zip);
|
||||
let enums = identify_master_enums(&mut archive);
|
||||
let purpose = enums
|
||||
.iter()
|
||||
.find(|(label, _)| *label == "Purpose")
|
||||
.expect("both candidates match the Purpose prefix within tolerance");
|
||||
winners.insert(purpose.1.class_name.clone());
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
winners.len(),
|
||||
1,
|
||||
"the same jar must resolve the same master enum every time; got {winners:?}. \
|
||||
A per-process-seeded map here means one disc can emit different labels on \
|
||||
different runs, with nothing in the output saying the choice was arbitrary"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identify_master_enums_accepts_count_at_the_tolerance_boundary() {
|
||||
// abs_diff(expected_count, count) == LDC_COUNT_TOLERANCE (4) exactly
|
||||
|
||||
Reference in New Issue
Block a user