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 super::{LabelPurpose, LabelQualifier, ParseResult, StreamLabel, StreamLabelType, jar, vocab};
|
||||||
use crate::sector::SectorSource;
|
use crate::sector::SectorSource;
|
||||||
use crate::udf::UdfFs;
|
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 {
|
pub fn detect(reader: &mut dyn SectorSource, udf: &UdfFs) -> bool {
|
||||||
// The real signal is `com/bydeluxe/` inside a top-level jar's central
|
// 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`].
|
/// [`MAX_CANDIDATE_CLASSES`].
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct CandidatePool {
|
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.
|
/// Retained bytes: class names plus every retained string.
|
||||||
bytes: usize,
|
bytes: usize,
|
||||||
}
|
}
|
||||||
@@ -1547,6 +1554,62 @@ mod tests {
|
|||||||
assert_eq!(purpose.1.values[7], "NoForcedDescriptive");
|
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]
|
#[test]
|
||||||
fn identify_master_enums_accepts_count_at_the_tolerance_boundary() {
|
fn identify_master_enums_accepts_count_at_the_tolerance_boundary() {
|
||||||
// abs_diff(expected_count, count) == LDC_COUNT_TOLERANCE (4) exactly
|
// abs_diff(expected_count, count) == LDC_COUNT_TOLERANCE (4) exactly
|
||||||
|
|||||||
Reference in New Issue
Block a user