//! Decode conformance vectors for the `epiphany-core` score wire //! (`spec/CONTRACT_CORE_DECODE_VECTORS.md`). //! //! See `epiphany_ops::vectors` for the corpus's purpose, its column shape, and //! why `accept`/`reject` are never collapsed. This module extends the same //! committed, cross-implementation corpus to the *oldest and most load-bearing* //! wire format in the repository — the whole-`Score` codec — which had no //! literal-byte vectors at all before this tranche (only round-trip locking, //! which cannot see a self-consistent field reordering: see //! `schema_major_3_tuning_context_wire_bytes_are_frozen` in `codec.rs`, and the //! contract's account of the tranche-3b-i defect it golden-pins). //! //! Two families of surface: //! //! * **Leaves** — one per [`CanonicalValue`] type the corpus pins: the five //! representative layouts of the Binary Format companion //! (§"Representative Complete Layouts") — [`RationalTime`], [`TimeAnchor`], //! [`Pitch`], [`Event`], [`Slur`] — plus the four schema-major-3 //! tuning-context leaves ([`SmuflVersion`], [`SmuflVersionRequirement`], //! [`TuningScope`], [`TuningOverride`]) and their container //! ([`ScoreTuningContext`]). Every leaf routes through //! [`CanonicalValue::decode_canonical`] — the same public, production API a //! value-typed operation payload uses — never a decoder reimplemented here. //! * **Whole `Score`, one per schema major** — `core.score_v0` through //! `core.score_v3`, routed through [`Score::decode_canonical_versioned`]. //! Majors 0–2 are **not** literal-byte-locked at the *current* layout: a //! migration deliberately rewrites bytes (that is the point of //! default-filling), so injectivity there means the input was already //! canonical *at its own major* — `decode_vN_score` re-encodes through the //! frozen `encode_vN_score` and rejects a mismatch, so a successful decode //! already proves that. Only `core.score_v3` compares //! `decoded.canonical_bytes() == bytes`. See the contract's "trap" section; //! getting this backwards (comparing v0–v2 against the *current* encoding) //! would fail on every vector, and "fixing" it by relaxing the check would //! destroy what the vector pins. use epiphany_determinism::CanonicalF64; use crate::accidental::{SmuflVersion, SmuflVersionRequirement}; use crate::codec::Codec; use crate::event::{Event, PitchedEvent, StemConfiguration}; use crate::graph::{ScoreTuningContext, Slur, SlurKind, SpanStyle}; use crate::ids::{EventId, PitchId, ReplicaId, SlurId, VoiceId}; use crate::pitch::{ AcousticPitch, AcousticRealization, CmnNominal, IdentifiedPitch, Pitch, PitchSpaceId, PitchSpacePosition, ScalePosition, TuningReference, TuningSystemId, }; use crate::time::{ AnchorOffset, EventDuration, EventPosition, MusicalDuration, MusicalPosition, RationalTime, TimeAnchor, }; use crate::tuning::{TuningOverride, TuningScope}; use crate::{CanonicalValue, Score}; /// One vector: `(surface, verdict, class, name, bytes)`. See /// `epiphany_ops::vectors::DecodeVector`. pub type DecodeVector = (&'static str, &'static str, &'static str, String, Vec); fn row( surface: &'static str, verdict: &'static str, class: &'static str, name: impl Into, bytes: Vec, ) -> DecodeVector { (surface, verdict, class, name.into(), bytes) } // =========================================================================== // Shared fixture values. // =========================================================================== /// A `cmn-12` pitch with an explicit cents-offset realization, so its /// canonical bytes end in a length-prefixed [`CanonicalF64`] leaf (the last /// 8 bytes are the raw IEEE-754 payload) — what the `core.pitch` reject vector /// corrupts to a non-finite value. fn pitch_with_cents(cents: f64) -> Pitch { Pitch { scale_position: ScalePosition { space: PitchSpaceId::new("cmn-12"), position: PitchSpacePosition::Cmn { nominal: CmnNominal::C, alteration: 0, octave: 4, }, }, acoustic: AcousticPitch { tuning: TuningReference::Inherit, realization: AcousticRealization::CentsOffset( CanonicalF64::new(cents).expect("finite"), ), }, } } fn simple_event() -> Event { Event::Pitched(PitchedEvent { id: EventId::new(ReplicaId(1), 1), voice: VoiceId::new(ReplicaId(1), 1), position: EventPosition::Musical(MusicalPosition(RationalTime::zero())), duration: EventDuration::Musical(MusicalDuration(RationalTime::new(1, 4).unwrap())), pitches: vec![IdentifiedPitch { id: PitchId::new(ReplicaId(1), 1), pitch: pitch_with_cents(0.0), }], articulations: vec![], dynamic: None, ornaments: vec![], stem: StemConfiguration, grace: None, }) } fn simple_slur() -> Slur { Slur { id: SlurId::new(ReplicaId(1), 1), start_event: EventId::new(ReplicaId(1), 1), end_event: EventId::new(ReplicaId(1), 2), kind: SlurKind::default(), curvature_override: None, style: SpanStyle::default(), } } /// The one `TuningOverride` embedded in [`loaded_tuning_context`]: a per-voice /// override that sets `tuning_system` only, leaving `pitch_space` and /// `reference` inherited — mirrors /// `schema_major_3_tuning_context_wire_bytes_are_frozen`'s fixture exactly /// (same field values), so these bytes are known-frozen wire content, not a /// fresh layout. fn one_override() -> TuningOverride { TuningOverride { scope: TuningScope::Voice(VoiceId::new(ReplicaId(1), 7)), pitch_space: None, tuning_system: Some(TuningSystemId::new("tet-19")), reference: None, } } /// A non-default `ScoreTuningContext`: `smufl` at 1.12/1.18 (not the 1.4/1.4 /// default) and one override, so both major-3 fields are real content, not /// vacuously-default padding. Same fixture as /// `schema_major_3_tuning_context_wire_bytes_are_frozen`. fn loaded_tuning_context() -> ScoreTuningContext { let mut ctx = ScoreTuningContext { smufl: SmuflVersionRequirement { minimum: SmuflVersion::from_decimal(1, "12").unwrap(), authored_against: SmuflVersion::from_decimal(1, "18").unwrap(), }, ..ScoreTuningContext::default() }; ctx.overrides.push(one_override()); ctx } /// Encodes `ctx` with `overrides` written *before* `smufl` — the exact /// regression this tranche exists to catch (Push 4b tranche 3b-i swapped /// these two fields in both halves of `impl Codec for ScoreTuningContext`, /// and the whole workspace suite plus 8/8 conformance still passed). The /// frozen field order is `default_pitch_space` ⌢ `default_tuning_system` ⌢ /// `reference` ⌢ `smufl` ⌢ `overrides`; this swaps the last two. fn score_tuning_context_bytes_with_fields_swapped(ctx: &ScoreTuningContext) -> Vec { let mut out = Vec::new(); ctx.default_pitch_space.enc(&mut out); ctx.default_tuning_system.enc(&mut out); ctx.reference.enc(&mut out); ctx.overrides.enc(&mut out); ctx.smufl.enc(&mut out); out } /// Hand-encodes the *unreduced* rational `2/4`: there is no public /// constructor that skips [`RationalTime`]'s reduce-on-construct invariant /// (every constructor re-establishes it), so the only way to produce /// non-canonical bytes for this leaf is to write them by hand, mirroring /// [`RationalTime`]'s own `CanonicalEncode` (`time.rs`): a sign byte, then a /// length-prefixed big-endian numerator magnitude, then a length-prefixed /// big-endian denominator magnitude — wrapped in the outer `u32` leaf-length /// prefix every embedded leaf carries. Decoding reduces `2/4` to `1/2`, so the /// re-encoded bytes differ from these: the lenient-leaf-normalization case the /// fifth representative layout exists to demonstrate (a guard *can* mask a /// lenient inner codec; here the leaf's own strict check catches it directly). fn unreduced_two_fourths() -> Vec { let mut inner = Vec::new(); inner.push(1); // sign: Plus inner.extend_from_slice(&1u32.to_le_bytes()); // numerator magnitude length inner.push(2); // numerator magnitude: 2 inner.extend_from_slice(&1u32.to_le_bytes()); // denominator magnitude length inner.push(4); // denominator magnitude: 4 let mut out = Vec::new(); out.extend_from_slice(&(inner.len() as u32).to_le_bytes()); // outer leaf length prefix out.extend_from_slice(&inner); out } /// Corrupts a tagged union's leading discriminant byte to a value one past /// every assigned tag, so the decoder's `match` falls through to its /// `InvalidTag` arm regardless of which union this is. fn with_invalid_leading_tag(bytes: &[u8], tag: u8) -> Vec { let mut out = bytes.to_vec(); out[0] = tag; out } /// Overwrites the trailing 8 bytes of an accept vector's bytes — the raw /// IEEE-754 payload of a trailing [`CanonicalF64`] leaf (its 4-byte length /// prefix precedes them) — with a non-finite bit pattern. fn with_trailing_float_replaced(bytes: &[u8], value: f64) -> Vec { let mut out = bytes.to_vec(); let n = out.len(); out[n - 8..].copy_from_slice(&value.to_le_bytes()); out } fn with_trailing_byte(bytes: &[u8]) -> Vec { let mut out = bytes.to_vec(); out.push(0); out } fn truncated(bytes: &[u8]) -> Vec { let mut out = bytes.to_vec(); out.pop(); out } // =========================================================================== // The vectors. // =========================================================================== /// Every `epiphany-core` decode vector: the leaf layouts, then the per-major /// whole-`Score` snapshots. pub fn decode_vectors() -> Vec { let mut v: Vec = Vec::new(); // --- RationalTime (the fifth representative layout) -------------------- const RT: &str = "core.rational_time"; let eighth = RationalTime::new(1, 8).unwrap(); v.push(row( RT, "accept", "-", "one_eighth", eighth.canonical_bytes(), )); v.push(row( RT, "reject", "unreduced-rational-time", "two_fourths_unreduced", unreduced_two_fourths(), )); // --- TimeAnchor ---------------------------------------------------------- const TA: &str = "core.time_anchor"; let anchor = TimeAnchor::Event { id: EventId::new(ReplicaId(1), 1), offset: AnchorOffset::Musical(MusicalDuration(RationalTime::new(1, 4).unwrap())), }; let anchor_bytes = anchor.canonical_bytes(); v.push(row(TA, "accept", "-", "event_anchor", anchor_bytes.clone())); v.push(row( TA, "reject", "out-of-range-discriminant", "tag_9_one_past_the_vocabulary", with_invalid_leading_tag(&anchor_bytes, 9), )); // --- Pitch --------------------------------------------------------------- const PITCH: &str = "core.pitch"; let pitch_bytes = pitch_with_cents(1.5).canonical_bytes(); v.push(row( PITCH, "accept", "-", "cents_offset", pitch_bytes.clone(), )); v.push(row( PITCH, "reject", "non-finite-float", "cents_offset_nan", with_trailing_float_replaced(&pitch_bytes, f64::NAN), )); // --- Event ----------------------------------------------------------------- const EVENT: &str = "core.event"; let event_bytes = simple_event().canonical_bytes(); v.push(row( EVENT, "accept", "-", "pitched_event", event_bytes.clone(), )); v.push(row( EVENT, "reject", "trailing-bytes", "pitched_event_trailing", with_trailing_byte(&event_bytes), )); // --- Slur -------------------------------------------------------------- const SLUR: &str = "core.slur"; let slur_bytes = simple_slur().canonical_bytes(); v.push(row(SLUR, "accept", "-", "simple_slur", slur_bytes.clone())); v.push(row( SLUR, "reject", "truncated", "simple_slur_truncated", truncated(&slur_bytes), )); // --- ScoreTuningContext (schema major 3) -------------------------------- const STC: &str = "core.score_tuning_context"; let ctx = loaded_tuning_context(); let ctx_bytes = ctx.canonical_bytes(); v.push(row(STC, "accept", "-", "loaded_context", ctx_bytes)); // THE direct regression vector for the 3b-i defect: bytes with `overrides` // written before `smufl` must be rejected by the (correctly-ordered) // decoder, even though a self-consistently-reordered codec would accept // its own output. This is what a byte-literal corpus catches that // round-trip locking cannot (see the module doc). v.push(row( STC, "reject", "swapped-major-3-field-order", "overrides_before_smufl", score_tuning_context_bytes_with_fields_swapped(&ctx), )); // --- TuningOverride ------------------------------------------------------ const TO: &str = "core.tuning_override"; let override_bytes = one_override().canonical_bytes(); v.push(row( TO, "accept", "-", "voice_scoped", override_bytes.clone(), )); v.push(row( TO, "reject", "trailing-bytes", "voice_scoped_trailing", with_trailing_byte(&override_bytes), )); // --- TuningScope --------------------------------------------------------- const TS: &str = "core.tuning_scope"; let scope_bytes = TuningScope::Voice(VoiceId::new(ReplicaId(1), 7)).canonical_bytes(); v.push(row(TS, "accept", "-", "voice", scope_bytes.clone())); v.push(row( TS, "reject", "out-of-range-discriminant", "tag_9_one_past_the_vocabulary", with_invalid_leading_tag(&scope_bytes, 9), )); // --- SmuflVersionRequirement ----------------------------------------------- const SVR: &str = "core.smufl_version_requirement"; let svr_bytes = SmuflVersionRequirement { minimum: SmuflVersion::from_decimal(1, "12").unwrap(), authored_against: SmuflVersion::from_decimal(1, "18").unwrap(), } .canonical_bytes(); v.push(row( SVR, "accept", "-", "one_twelve_one_eighteen", svr_bytes.clone(), )); v.push(row( SVR, "reject", "trailing-bytes", "one_twelve_one_eighteen_trailing", with_trailing_byte(&svr_bytes), )); // --- SmuflVersion -------------------------------------------------------- const SV: &str = "core.smufl_version"; let sv_bytes = SmuflVersion::from_decimal(1, "4") .unwrap() .canonical_bytes(); v.push(row(SV, "accept", "-", "one_four", sv_bytes.clone())); v.push(row( SV, "reject", "truncated", "one_four_truncated", truncated(&sv_bytes), )); // --- Whole Score, one per schema major ----------------------------------- // // A single, real, well-formed `Score` (the positive generator's output — // migration-safe: its schema-major-1/2/3 fields all sit at their canonical // defaults, exactly what `valid_score`'s existing migration tests already // rely on), encoded through each frozen per-major encoder. Majors 0-2 are // genuinely *older* wire forms of the same value, synthesized via the // pub(crate) `encode_vN_score` mirrors (never a fresh hand-rolled layout); // major 3 is the live `canonical_bytes()`. let score = crate::generators::valid_score(7); let v0 = crate::codec::encode_v0_score(&score); let v1 = crate::codec::encode_v1_score(&score); let v2 = crate::codec::encode_v2_score(&score); let v3 = score.canonical_bytes(); const SV0: &str = "core.score_v0"; v.push(row(SV0, "accept", "-", "valid_score_seed_7", v0.clone())); v.push(row( SV0, "reject", "trailing-bytes", "valid_score_seed_7_trailing", with_trailing_byte(&v0), )); const SV1: &str = "core.score_v1"; v.push(row(SV1, "accept", "-", "valid_score_seed_7", v1.clone())); v.push(row( SV1, "reject", "trailing-bytes", "valid_score_seed_7_trailing", with_trailing_byte(&v1), )); const SV2: &str = "core.score_v2"; v.push(row(SV2, "accept", "-", "valid_score_seed_7", v2.clone())); v.push(row( SV2, "reject", "trailing-bytes", "valid_score_seed_7_trailing", with_trailing_byte(&v2), )); const SV3: &str = "core.score_v3"; v.push(row(SV3, "accept", "-", "valid_score_seed_7", v3.clone())); v.push(row( SV3, "reject", "trailing-bytes", "valid_score_seed_7_trailing", with_trailing_byte(&v3), )); v } // =========================================================================== // Verification. // =========================================================================== /// Runs `T`'s [`CanonicalValue::decode_canonical`] — the same public, /// production API a value-typed operation payload decodes through — never a /// decoder reimplemented in this module. fn leaf_check(bytes: &[u8]) -> Result { match T::decode_canonical(bytes) { Ok(v) => Ok(v.canonical_bytes() == bytes), Err(e) => Err(format!("{e}")), } } /// Runs [`Score::decode_canonical_versioned`] at `major`. Majors 0-2 report /// injectivity as `true` unconditionally on a successful decode: migration /// deliberately rewrites the bytes (default-filling new fields), so comparing /// against the *current* `canonical_bytes()` would fail on every vector, and /// `decode_vN_score`'s own re-encode-through-`encode_vN_score` guard already /// proved the input canonical at *its own* major before returning `Ok` at all /// (see the module doc's account of the contract's "trap"). Only major 3 /// compares `decoded.canonical_bytes() == bytes` — the live layout, where that /// comparison is exactly what injectivity means. fn score_check(bytes: &[u8], major: u16) -> Result { match Score::decode_canonical_versioned(bytes, major) { Ok(decoded) => { if major == 3 { Ok(decoded.canonical_bytes() == bytes) } else { Ok(true) } } Err(e) => Err(format!("{e}")), } } /// Applies `surface`'s decoder to `bytes`. See `epiphany_ops::vectors::check` /// for the exact `Ok`/`Err` semantics. `None` when the surface is not owned by /// this crate. pub fn check(surface: &str, bytes: &[u8]) -> Option> { match surface { "core.rational_time" => Some(leaf_check::(bytes)), "core.time_anchor" => Some(leaf_check::(bytes)), "core.pitch" => Some(leaf_check::(bytes)), "core.event" => Some(leaf_check::(bytes)), "core.slur" => Some(leaf_check::(bytes)), "core.score_tuning_context" => Some(leaf_check::(bytes)), "core.tuning_override" => Some(leaf_check::(bytes)), "core.tuning_scope" => Some(leaf_check::(bytes)), "core.smufl_version_requirement" => Some(leaf_check::(bytes)), "core.smufl_version" => Some(leaf_check::(bytes)), "core.score_v0" => Some(score_check(bytes, 0)), "core.score_v1" => Some(score_check(bytes, 1)), "core.score_v2" => Some(score_check(bytes, 2)), "core.score_v3" => Some(score_check(bytes, 3)), _ => None, } } #[cfg(test)] mod tests { use super::*; /// Each vector must get the verdict it declares — see /// `epiphany_ops::vectors::tests::every_vector_gets_its_declared_verdict`. #[test] fn every_vector_gets_its_declared_verdict() { for (surface, verdict, class, name, bytes) in decode_vectors() { let result = check(surface, &bytes).expect("a surface this crate owns"); match (verdict, &result) { ("accept", Ok(true)) => {} ("accept", Ok(false)) => { panic!("{surface}/{name}: accepted but does not re-encode to its bytes") } ("reject", Err(_)) => {} _ => panic!("{surface}/{name} ({class}): declared {verdict}, got {result:?}"), } } } /// Every surface carries both verdicts, or the corpus pins half a /// contract. #[test] fn every_surface_carries_both_verdicts() { use std::collections::BTreeMap; let mut seen: BTreeMap<&str, (bool, bool)> = BTreeMap::new(); for (surface, verdict, ..) in decode_vectors() { let e = seen.entry(surface).or_default(); match verdict { "accept" => e.0 = true, "reject" => e.1 = true, other => panic!("unknown verdict {other}"), } } assert_eq!(seen.len(), 14, "surfaces: {:?}", seen.keys()); for (surface, (accept, reject)) in seen { assert!(accept, "{surface} has no accept vector"); assert!(reject, "{surface} has no reject vector"); } } /// The mandatory regression vector for the 3b-i defect is present: it is /// what makes this tranche's existence justified (see the module doc). #[test] fn the_3b_i_regression_vector_is_present() { assert!(decode_vectors() .iter() .any(|(s, v, c, ..)| *s == "core.score_tuning_context" && *v == "reject" && *c == "swapped-major-3-field-order")); } }