//! Canonical byte codec for the whole [`Score`] graph (item 5 / Agent B's //! "whole-score codec"). //! //! The determinism crate fixes the canonical byte form of the *primitives* //! (identifiers, hashes, [`CanonicalF64`], [`RationalTime`]); this module //! composes those into a total, reversible byte form for every value reachable //! from a [`Score`], so the materialized graph itself — not just the Chapter 6 //! bookkeeping projection — has a canonical serialization //! ([`Score::canonical_bytes`] / [`Score::decode_canonical`]). //! //! The form is deliberately boring and total, mirroring `epiphany-ops`' //! [`MaterializedState`](crate) codec and Appendix D §"Canonical serialization //! determinism": //! //! * Integers are little-endian. Booleans are a single `0`/`1` byte. //! * Every variable-width *leaf* (an id, a [`RationalTime`], a string) is //! `u32`-length-prefixed, so the decoder never guesses a boundary and is //! width-agnostic. //! * Composites encode their fields in declaration order; the decoder knows the //! structure, so fixed-shape composites are not length-prefixed. //! * Sequences and maps carry a `u32` count then each element (maps/sets in //! their canonical key order). Maps and sets are rebuilt on decode. //! * Tagged unions carry a single discriminant byte then the variant payload. //! * Strings are length-prefixed UTF-8 and are **not** NFC-folded here: the //! graph's [`Score`] equality is byte-exact on its [`String`] fields, so the //! codec preserves them exactly (`decode(encode(x)) == x` for every valid //! score). Catalog ids are already NFC at construction. //! //! This is a concrete, reversible canonical form that predates the Binary Format //! companion specification; when that lands, reconcile the two (see //! `DECISIONS.md`, P11-4). The convention set above is RATIFIED by Pass 11 //! (item 1.8): core_spec §"Binary Format Companion", //! Requirement `req:format:codec-conventions` blesses these conventions as the //! companion's inherited baseline, so core/ops/bundle stay mutually consistent //! until Agent J formalizes the full companion. use core::num::NonZeroU16; use std::collections::{BTreeMap, BTreeSet}; use epiphany_determinism::{CanonicalDecode, CanonicalEncode, CanonicalF64, ContentHash}; use crate::accidental::{SmuflVersion, SmuflVersionRequirement}; use crate::event::{ ArticulationMark, CueEvent, CueRendering, DynamicMark, Event, EventArena, GraceKind, GraphicEvent, IndeterminacyHints, IndeterminacyKind, IndeterminateEvent, OrnamentMark, PitchedEvent, PlaybackBinding, Rest, StaffPosition, StemConfiguration, TrajectoryDisplay, TrajectoryEndpoint, TrajectoryEvent, TrajectoryShape, UnpitchedEvent, UnpitchedMemberId, }; use crate::graph::{ AleatoricAnchoringDiscipline, AleatoricTimeModel, AnalysisLayer, AnalyticalAnnotation, AnnotationAnchor, BarlineAlignmentGroup, BarlineAlignmentMember, Beam, BeamGeometryOverride, BeatGroup, BracketKind, Canvas, CanvasLayoutDefaults, CanvasMargins, CanvasSize, ChordSymbol, Clef, ClefChange, ClefShape, Comment, CrossCuttingRegistry, CurvatureOverride, CurveDirection, DecompositionAttachment, DecompositionSource, EventOrderingDAG, GestureAnchoring, GraphicContent, GraphicGesture, GraphicObject, HairpinDirection, Instrument, KeySignature, KeySignatureChange, LineStyle, LyricLine, Marker, Measure, MeasureNumberVisibility, MetadataEntry, MetadataValue, MeterChange, MetricGrid, MetricTimeModel, NotatedComponent, NoteValue, OctaveOffset, PartDefinition, PedalKind, PowerOfTwo, ProportionalTimeModel, Region, RegionContent, RegionTimeModel, RepeatKind, RepeatStructure, Score, ScoreMetadata, ScoreTuningContext, Slur, SlurKind, SoundConfiguration, SpaceUnit, SpanStyle, Spanner, SpannerKind, Staff, StaffBasedContent, StaffBracketKind, StaffExtent, StaffGroup, StaffGroupKind, StaffInstance, StaffLineConfiguration, StemDirection, SubBeam, TempoMapReference, TextLineDefinition, Tie, TieClass, TimeExtent, TimeSignature, TimeSignatureDisplay, Timestamp, TuningContextSettings, Tuplet, TupletRatio, UnpitchedMember, ViewDefinition, Voice, VoiceOrigin, Volta, }; use crate::ids::{ AnalysisLayerId, AnalyticalAnnotationId, BarlineAlignmentGroupId, BeamId, ChordSymbolId, CommentId, EventId, GraphicGestureId, GraphicObjectId, IdentityContext, InstrumentId, LyricLineId, MarkerId, MeasureId, OperationId, PartDefinitionId, PitchId, RegionId, RepeatStructureId, ReplicaId, SlurId, SpannerId, StaffGroupId, StaffId, StaffInstanceId, TieId, TimeSignatureId, TupletId, ViewId, VoiceId, }; use crate::pitch::{ AccidentalId, AcousticPitch, AcousticRealization, CmnNominal, ForeignFormatId, IdentifiedPitch, NominalRegistryId, Pitch, PitchRange, PitchSpaceId, PitchSpacePosition, PitchSpelling, PositionRegistryId, ReferencePitch, ScalePosition, SpellingAttachment, SpellingDirective, SpellingNominal, SpellingPrecedence, SpellingRenderHints, SpellingRule, SpellingRuleSetId, SpellingScope, SpellingSource, SpellingSourceKind, StaffGroupKindRegistryId, TieClassRegistryId, TranspositionInterval, TuningReference, TuningSystemId, VoiceSelector, }; use crate::tempo::{Tempo, TempoMap, TempoSegment, TempoShape}; use crate::time::{ AnchorOffset, ConcreteDuration, DurationBounds, EventBounds, EventDuration, EventPosition, MeasurePosition, MusicalDuration, MusicalPosition, RationalTime, RegionEdge, TimeAnchor, TimeBounds, WallClockDuration, WallClockTime, }; use crate::tuning::{TuningOverride, TuningScope}; // =========================================================================== // Errors and the reader cursor. // =========================================================================== /// Why decoding canonical [`Score`] bytes failed. #[derive(Clone, PartialEq, Eq, Debug)] pub enum ScoreDecodeError { /// A field ended before its declared or fixed width. UnexpectedEof, /// A length prefix or count cannot be represented as `usize` on this target. LengthOverflow, /// A tagged union carried an unknown discriminant. InvalidTag { kind: &'static str, tag: u8 }, /// A primitive leaf failed its own canonical decoder. InvalidValue(&'static str), /// A canonical boolean was not `0` or `1`. InvalidBoolean(u8), /// A canonical text field was not UTF-8. InvalidUtf8, /// A value failed a type invariant on reconstruction (e.g. a time signature /// whose beat groups do not sum to its measure duration). Reconstruct(&'static str), /// Bytes remained after the complete score was decoded. TrailingBytes, } impl core::fmt::Display for ScoreDecodeError { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Self::UnexpectedEof => f.write_str("unexpected end of score bytes"), Self::LengthOverflow => f.write_str("score length does not fit usize"), Self::InvalidTag { kind, tag } => write!(f, "invalid {kind} tag {tag}"), Self::InvalidValue(kind) => write!(f, "invalid canonical {kind}"), Self::InvalidBoolean(v) => write!(f, "invalid canonical boolean {v}"), Self::InvalidUtf8 => f.write_str("invalid UTF-8 in canonical text"), Self::Reconstruct(kind) => write!(f, "{kind} failed its invariant on decode"), Self::TrailingBytes => f.write_str("trailing bytes after score"), } } } impl std::error::Error for ScoreDecodeError {} type Result = core::result::Result; /// A forward-only cursor over canonical bytes. pub(crate) struct Reader<'a> { bytes: &'a [u8], pos: usize, } impl<'a> Reader<'a> { fn new(bytes: &'a [u8]) -> Self { Self { bytes, pos: 0 } } fn take(&mut self, n: usize) -> Result<&'a [u8]> { let end = self .pos .checked_add(n) .ok_or(ScoreDecodeError::LengthOverflow)?; if end > self.bytes.len() { return Err(ScoreDecodeError::UnexpectedEof); } let out = &self.bytes[self.pos..end]; self.pos = end; Ok(out) } fn u8(&mut self) -> Result { Ok(self.take(1)?[0]) } fn u16(&mut self) -> Result { Ok(u16::from_le_bytes( self.take(2)?.try_into().expect("2 bytes"), )) } fn u32(&mut self) -> Result { Ok(u32::from_le_bytes( self.take(4)?.try_into().expect("4 bytes"), )) } fn u64(&mut self) -> Result { Ok(u64::from_le_bytes( self.take(8)?.try_into().expect("8 bytes"), )) } fn count(&mut self) -> Result { let n = usize::try_from(self.u32()?).map_err(|_| ScoreDecodeError::LengthOverflow)?; // A count/length prefix can never exceed the bytes remaining, in both of // its uses: a length-prefixed leaf needs exactly `n` further bytes // (`lp`), and a collection prefix counts elements, each of which is at // least one byte in this codec, so `n` elements need at least `n` bytes. // (An empty leaf or collection has `n == 0`, which passes.) A larger `n` // is corrupt or adversarial: reject it up front rather than looping // element-by-element toward EOF — that bounds decode time on hostile // input (a garbage `u32` count is otherwise a soft-DoS: e.g. misparsing // v1 bytes as v0 reads a huge count and iterates ~remaining times) and // caps the `Vec`/set allocation to a real size. if n > self.bytes.len() - self.pos { return Err(ScoreDecodeError::UnexpectedEof); } Ok(n) } /// A `u32`-length-prefixed byte slice. fn lp(&mut self) -> Result<&'a [u8]> { let n = self.count()?; self.take(n) } fn finish(self) -> Result<()> { if self.pos == self.bytes.len() { Ok(()) } else { Err(ScoreDecodeError::TrailingBytes) } } } // =========================================================================== // The codec trait and combinators. // =========================================================================== /// A type with a canonical byte form within a [`Score`]. `enc`/`dec` are exact /// inverses, kept adjacent per type so they cannot drift. pub(crate) trait Codec: Sized { fn enc(&self, out: &mut Vec); fn dec(r: &mut Reader<'_>) -> Result; } /// Appends a `u32` little-endian length/count prefix. fn put_len(out: &mut Vec, n: usize) { debug_assert!(n <= u32::MAX as usize, "canonical length exceeds u32"); out.extend_from_slice(&(n as u32).to_le_bytes()); } /// A length-prefixed leaf: encode the determinism-canonical bytes behind a /// `u32` length so the decoder is width-agnostic. fn put_leaf(out: &mut Vec, v: &T) { let mut scratch = Vec::new(); v.encode_canonical(&mut scratch); put_len(out, scratch.len()); out.extend_from_slice(&scratch); } fn get_leaf(r: &mut Reader<'_>, name: &'static str) -> Result { let slice = r.lp()?; T::decode_canonical(slice).map_err(|_| ScoreDecodeError::InvalidValue(name)) } // --- Primitive impls. ------------------------------------------------------- impl Codec for bool { fn enc(&self, out: &mut Vec) { out.push(*self as u8); } fn dec(r: &mut Reader<'_>) -> Result { match r.u8()? { 0 => Ok(false), 1 => Ok(true), v => Err(ScoreDecodeError::InvalidBoolean(v)), } } } impl Codec for u8 { fn enc(&self, out: &mut Vec) { out.push(*self); } fn dec(r: &mut Reader<'_>) -> Result { r.u8() } } impl Codec for u16 { fn enc(&self, out: &mut Vec) { out.extend_from_slice(&self.to_le_bytes()); } fn dec(r: &mut Reader<'_>) -> Result { r.u16() } } impl Codec for u32 { fn enc(&self, out: &mut Vec) { out.extend_from_slice(&self.to_le_bytes()); } fn dec(r: &mut Reader<'_>) -> Result { r.u32() } } impl Codec for u64 { fn enc(&self, out: &mut Vec) { out.extend_from_slice(&self.to_le_bytes()); } fn dec(r: &mut Reader<'_>) -> Result { r.u64() } } impl Codec for i8 { fn enc(&self, out: &mut Vec) { out.push(*self as u8); } fn dec(r: &mut Reader<'_>) -> Result { Ok(r.u8()? as i8) } } impl Codec for i16 { fn enc(&self, out: &mut Vec) { out.extend_from_slice(&self.to_le_bytes()); } fn dec(r: &mut Reader<'_>) -> Result { Ok(r.u16()? as i16) } } impl Codec for i32 { fn enc(&self, out: &mut Vec) { out.extend_from_slice(&self.to_le_bytes()); } fn dec(r: &mut Reader<'_>) -> Result { Ok(r.u32()? as i32) } } impl Codec for i64 { fn enc(&self, out: &mut Vec) { out.extend_from_slice(&self.to_le_bytes()); } fn dec(r: &mut Reader<'_>) -> Result { Ok(r.u64()? as i64) } } impl Codec for u128 { fn enc(&self, out: &mut Vec) { out.extend_from_slice(&self.to_le_bytes()); } fn dec(r: &mut Reader<'_>) -> Result { Ok(u128::from_le_bytes( r.take(16)?.try_into().expect("16 bytes"), )) } } impl Codec for String { fn enc(&self, out: &mut Vec) { put_len(out, self.len()); out.extend_from_slice(self.as_bytes()); } fn dec(r: &mut Reader<'_>) -> Result { let bytes = r.lp()?; core::str::from_utf8(bytes) .map(|s| s.to_owned()) .map_err(|_| ScoreDecodeError::InvalidUtf8) } } // --- Generic combinators. --------------------------------------------------- impl Codec for Option { fn enc(&self, out: &mut Vec) { match self { None => out.push(0), Some(v) => { out.push(1); v.enc(out); } } } fn dec(r: &mut Reader<'_>) -> Result { match r.u8()? { 0 => Ok(None), 1 => Ok(Some(T::dec(r)?)), tag => Err(ScoreDecodeError::InvalidTag { kind: "Option", tag, }), } } } impl Codec for Vec { fn enc(&self, out: &mut Vec) { put_len(out, self.len()); for item in self { item.enc(out); } } fn dec(r: &mut Reader<'_>) -> Result { let n = r.count()?; let mut v = Vec::with_capacity(n.min(1024)); for _ in 0..n { v.push(T::dec(r)?); } Ok(v) } } impl Codec for BTreeSet { fn enc(&self, out: &mut Vec) { put_len(out, self.len()); for item in self { item.enc(out); } } fn dec(r: &mut Reader<'_>) -> Result { let n = r.count()?; let mut set = BTreeSet::new(); for _ in 0..n { set.insert(T::dec(r)?); } Ok(set) } } impl Codec for BTreeMap { fn enc(&self, out: &mut Vec) { put_len(out, self.len()); for (k, v) in self { k.enc(out); v.enc(out); } } fn dec(r: &mut Reader<'_>) -> Result { let n = r.count()?; let mut map = BTreeMap::new(); for _ in 0..n { let k = K::dec(r)?; let v = V::dec(r)?; map.insert(k, v); } Ok(map) } } impl Codec for (A, B) { fn enc(&self, out: &mut Vec) { self.0.enc(out); self.1.enc(out); } fn dec(r: &mut Reader<'_>) -> Result { let a = A::dec(r)?; let b = B::dec(r)?; Ok((a, b)) } } // --- Leaf impls (length-prefixed determinism-canonical bytes). -------------- /// Implements [`Codec`] for a determinism-canonical leaf by length-prefixing /// its canonical bytes; decode runs the leaf's own validating decoder. macro_rules! leaf_codec { ($($ty:ty => $name:literal),* $(,)?) => { $( impl Codec for $ty { fn enc(&self, out: &mut Vec) { put_leaf(out, self); } fn dec(r: &mut Reader<'_>) -> Result { get_leaf::<$ty>(r, $name) } } )* }; } leaf_codec! { ReplicaId => "ReplicaId", OperationId => "OperationId", EventId => "EventId", PitchId => "PitchId", VoiceId => "VoiceId", StaffId => "StaffId", StaffInstanceId => "StaffInstanceId", StaffGroupId => "StaffGroupId", RegionId => "RegionId", InstrumentId => "InstrumentId", PartDefinitionId => "PartDefinitionId", MeasureId => "MeasureId", BarlineAlignmentGroupId => "BarlineAlignmentGroupId", SlurId => "SlurId", TieId => "TieId", BeamId => "BeamId", SpannerId => "SpannerId", TupletId => "TupletId", MarkerId => "MarkerId", AnalyticalAnnotationId => "AnalyticalAnnotationId", CommentId => "CommentId", RepeatStructureId => "RepeatStructureId", LyricLineId => "LyricLineId", ChordSymbolId => "ChordSymbolId", GraphicObjectId => "GraphicObjectId", GraphicGestureId => "GraphicGestureId", TimeSignatureId => "TimeSignatureId", AnalysisLayerId => "AnalysisLayerId", ViewId => "ViewId", ContentHash => "ContentHash", CanonicalF64 => "CanonicalF64", RationalTime => "RationalTime", MusicalPosition => "MusicalPosition", MusicalDuration => "MusicalDuration", WallClockTime => "WallClockTime", WallClockDuration => "WallClockDuration", } // =========================================================================== // Boilerplate macros for the composite types. // =========================================================================== /// [`Codec`] for a plain struct whose fields all implement [`Codec`]: encode in /// declaration order; decode the same and rebuild the literal. macro_rules! struct_codec { ($ty:ident { $($field:ident),* $(,)? }) => { impl Codec for $ty { fn enc(&self, out: &mut Vec) { $( self.$field.enc(out); )* } fn dec(r: &mut Reader<'_>) -> Result { $( let $field = Codec::dec(r)?; )* Ok($ty { $($field),* }) } } impl crate::textvalue::TextValue for $ty { fn project(&self) -> crate::textvalue::Sexp { let $ty { $($field),* } = self; crate::textvalue::Sexp::List(vec![ crate::textvalue::Sexp::Symbol(crate::textvalue::kebab(stringify!($ty))), $( crate::textvalue::TextValue::project($field), )* ]) } fn parse( s: &crate::textvalue::Sexp, ) -> core::result::Result { const ARITY: usize = [$(stringify!($field)),*].len(); let fields = s.expect_struct(&crate::textvalue::kebab(stringify!($ty)), ARITY)?; let mut next = fields.iter(); $( let $field = crate::textvalue::TextValue::parse( next.next().expect("arity checked by expect_struct"))?; )* Ok($ty { $($field),* }) } } }; } /// [`Codec`] for a zero-field unit struct: no bytes. And its [`TextValue`]: the /// bare symbol, as a fieldless variant is. It encodes to no bytes and carries no /// value in the text either (`req:textproj:value-projection` clause 1). /// /// [`TextValue`]: crate::textvalue::TextValue macro_rules! unit_codec { ($($ty:ident),* $(,)?) => { $( impl Codec for $ty { fn enc(&self, _out: &mut Vec) {} fn dec(_r: &mut Reader<'_>) -> Result { Ok($ty) } } impl crate::textvalue::TextValue for $ty { fn project(&self) -> crate::textvalue::Sexp { crate::textvalue_impls::project_unit(stringify!($ty)) } fn parse( s: &crate::textvalue::Sexp, ) -> core::result::Result { crate::textvalue_impls::parse_unit(s, stringify!($ty)).map(|()| $ty) } } )* }; } /// [`Codec`] for a fieldless ("C-like") enum: a single discriminant byte. And its /// [`TextValue`]: the variant name as a bare symbol. /// /// Both `match`es are exhaustive over the enum, so a variant added to the type and /// not to this invocation **fails to compile** — the guarantee /// `operation_kind_tag_vocabulary!` gives the operation decoder, here for every /// C-like enum in Chapter 5. /// /// [`TextValue`]: crate::textvalue::TextValue macro_rules! cstyle_enum_codec { ($ty:ident { $($tag:literal => $variant:ident),* $(,)? }) => { impl Codec for $ty { fn enc(&self, out: &mut Vec) { let tag: u8 = match self { $( $ty::$variant => $tag, )* }; out.push(tag); } fn dec(r: &mut Reader<'_>) -> Result { match r.u8()? { $( $tag => Ok($ty::$variant), )* tag => Err(ScoreDecodeError::InvalidTag { kind: stringify!($ty), tag }), } } } impl crate::textvalue::TextValue for $ty { fn project(&self) -> crate::textvalue::Sexp { let variant = match self { $( $ty::$variant => stringify!($variant), )* }; crate::textvalue::Sexp::Symbol(crate::textvalue::kebab(variant)) } fn parse( s: &crate::textvalue::Sexp, ) -> core::result::Result { let name = s.as_symbol().ok_or(crate::textvalue::TextError::Expected { expected: "symbol", found: crate::textvalue_impls::class_of(s), })?; $( if name == crate::textvalue::kebab(stringify!($variant)) { return Ok($ty::$variant); } )* Err(crate::textvalue::TextError::UnknownConstructor { type_name: stringify!($ty), found: name.to_owned(), }) } } }; } /// [`Codec`] for a catalog-id newtype over `String` (construct via `new`, read /// via `as_str`; the stored form is already NFC). macro_rules! catalog_id_codec { ($($ty:ident),* $(,)?) => { $( impl Codec for $ty { fn enc(&self, out: &mut Vec) { self.as_str().to_owned().enc(out); } fn dec(r: &mut Reader<'_>) -> Result { Ok($ty::new(String::dec(r)?)) } } // A catalog id is canonical text, so it projects as a quoted string. // `new` folds to NFC, so `parse` interns and then *compares*: returning // the folded value would accept a non-NFC spelling and silently // normalize it (`req:textproj:strict-parse`). impl crate::textvalue::TextValue for $ty { fn project(&self) -> crate::textvalue::Sexp { crate::textvalue::Sexp::Str(self.as_str().to_owned()) } fn parse( s: &crate::textvalue::Sexp, ) -> core::result::Result { crate::textvalue_impls::parse_catalog_id(s, $ty::new, |v| v.as_str()) } } )* }; } unit_codec!( ArticulationMark, DynamicMark, OrnamentMark, StemConfiguration, TrajectoryDisplay, PlaybackBinding, CueRendering, TempoMapReference, ); catalog_id_codec!( PitchSpaceId, TuningSystemId, AccidentalId, NominalRegistryId, PositionRegistryId, TieClassRegistryId, StaffGroupKindRegistryId, SpellingRuleSetId, ForeignFormatId, ); // --- Small newtypes / std types needing reconstruction. --------------------- impl Codec for StaffPosition { fn enc(&self, out: &mut Vec) { self.0.enc(out); } fn dec(r: &mut Reader<'_>) -> Result { Ok(StaffPosition(i16::dec(r)?)) } } impl Codec for UnpitchedMemberId { fn enc(&self, out: &mut Vec) { self.0.enc(out); } fn dec(r: &mut Reader<'_>) -> Result { Ok(UnpitchedMemberId(u32::dec(r)?)) } } impl Codec for PowerOfTwo { fn enc(&self, out: &mut Vec) { self.get().enc(out); } fn dec(r: &mut Reader<'_>) -> Result { PowerOfTwo::new(u16::dec(r)?).ok_or(ScoreDecodeError::Reconstruct("PowerOfTwo")) } } impl Codec for NonZeroU16 { fn enc(&self, out: &mut Vec) { self.get().enc(out); } fn dec(r: &mut Reader<'_>) -> Result { NonZeroU16::new(u16::dec(r)?).ok_or(ScoreDecodeError::Reconstruct("NonZeroU16")) } } // =========================================================================== // time.rs // =========================================================================== cstyle_enum_codec!(MeasurePosition { 0 => Start, 1 => End }); cstyle_enum_codec!(RegionEdge { 0 => Start, 1 => End }); impl Codec for AnchorOffset { fn enc(&self, out: &mut Vec) { match self { AnchorOffset::Musical(d) => { out.push(0); d.enc(out); } AnchorOffset::WallClock(d) => { out.push(1); d.enc(out); } AnchorOffset::Zero => out.push(2), } } fn dec(r: &mut Reader<'_>) -> Result { match r.u8()? { 0 => Ok(AnchorOffset::Musical(Codec::dec(r)?)), 1 => Ok(AnchorOffset::WallClock(Codec::dec(r)?)), 2 => Ok(AnchorOffset::Zero), tag => Err(ScoreDecodeError::InvalidTag { kind: "AnchorOffset", tag, }), } } } impl Codec for TimeAnchor { fn enc(&self, out: &mut Vec) { match self { TimeAnchor::Event { id, offset } => { out.push(0); id.enc(out); offset.enc(out); } TimeAnchor::Measure { id, position, offset, } => { out.push(1); id.enc(out); position.enc(out); offset.enc(out); } TimeAnchor::Region { id, edge, offset } => { out.push(2); id.enc(out); edge.enc(out); offset.enc(out); } TimeAnchor::WallClock { time } => { out.push(3); time.enc(out); } } } fn dec(r: &mut Reader<'_>) -> Result { match r.u8()? { 0 => Ok(TimeAnchor::Event { id: Codec::dec(r)?, offset: Codec::dec(r)?, }), 1 => Ok(TimeAnchor::Measure { id: Codec::dec(r)?, position: Codec::dec(r)?, offset: Codec::dec(r)?, }), 2 => Ok(TimeAnchor::Region { id: Codec::dec(r)?, edge: Codec::dec(r)?, offset: Codec::dec(r)?, }), 3 => Ok(TimeAnchor::WallClock { time: Codec::dec(r)?, }), tag => Err(ScoreDecodeError::InvalidTag { kind: "TimeAnchor", tag, }), } } } impl Codec for EventPosition { fn enc(&self, out: &mut Vec) { match self { EventPosition::Musical(p) => { out.push(0); p.enc(out); } EventPosition::WallClock(t) => { out.push(1); t.enc(out); } } } fn dec(r: &mut Reader<'_>) -> Result { match r.u8()? { 0 => Ok(EventPosition::Musical(Codec::dec(r)?)), 1 => Ok(EventPosition::WallClock(Codec::dec(r)?)), tag => Err(ScoreDecodeError::InvalidTag { kind: "EventPosition", tag, }), } } } impl Codec for ConcreteDuration { fn enc(&self, out: &mut Vec) { match self { ConcreteDuration::Musical(d) => { out.push(0); d.enc(out); } ConcreteDuration::WallClock(d) => { out.push(1); d.enc(out); } } } fn dec(r: &mut Reader<'_>) -> Result { match r.u8()? { 0 => Ok(ConcreteDuration::Musical(Codec::dec(r)?)), 1 => Ok(ConcreteDuration::WallClock(Codec::dec(r)?)), tag => Err(ScoreDecodeError::InvalidTag { kind: "ConcreteDuration", tag, }), } } } struct_codec!(DurationBounds { lower, upper }); impl Codec for EventDuration { fn enc(&self, out: &mut Vec) { match self { EventDuration::Musical(d) => { out.push(0); d.enc(out); } EventDuration::WallClock(d) => { out.push(1); d.enc(out); } EventDuration::Indeterminate(b) => { out.push(2); b.enc(out); } } } fn dec(r: &mut Reader<'_>) -> Result { match r.u8()? { 0 => Ok(EventDuration::Musical(Codec::dec(r)?)), 1 => Ok(EventDuration::WallClock(Codec::dec(r)?)), 2 => Ok(EventDuration::Indeterminate(Codec::dec(r)?)), tag => Err(ScoreDecodeError::InvalidTag { kind: "EventDuration", tag, }), } } } impl Codec for TimeBounds { fn enc(&self, out: &mut Vec) { match self { TimeBounds::MusicalRange { min, max } => { out.push(0); min.enc(out); max.enc(out); } TimeBounds::WallClockRange { min, max } => { out.push(1); min.enc(out); max.enc(out); } TimeBounds::Unbounded => out.push(2), } } fn dec(r: &mut Reader<'_>) -> Result { match r.u8()? { 0 => Ok(TimeBounds::MusicalRange { min: Codec::dec(r)?, max: Codec::dec(r)?, }), 1 => Ok(TimeBounds::WallClockRange { min: Codec::dec(r)?, max: Codec::dec(r)?, }), 2 => Ok(TimeBounds::Unbounded), tag => Err(ScoreDecodeError::InvalidTag { kind: "TimeBounds", tag, }), } } } struct_codec!(EventBounds { start, end }); // =========================================================================== // pitch.rs // =========================================================================== cstyle_enum_codec!(CmnNominal { 0 => C, 1 => D, 2 => E, 3 => F, 4 => G, 5 => A, 6 => B, }); cstyle_enum_codec!(SpellingSourceKind { 0 => UserChosen, 1 => Imported, 2 => Propagated, 3 => Inferred, 4 => Analytical, }); impl Codec for PitchSpacePosition { fn enc(&self, out: &mut Vec) { match self { PitchSpacePosition::Cmn { nominal, alteration, octave, } => { out.push(0); nominal.enc(out); alteration.enc(out); octave.enc(out); } PitchSpacePosition::Integer { space_size, index } => { out.push(1); space_size.enc(out); index.enc(out); } PitchSpacePosition::JiVector { components } => { out.push(2); components.enc(out); } PitchSpacePosition::Registered(id) => { out.push(3); id.enc(out); } } } fn dec(r: &mut Reader<'_>) -> Result { match r.u8()? { 0 => Ok(PitchSpacePosition::Cmn { nominal: Codec::dec(r)?, alteration: Codec::dec(r)?, octave: Codec::dec(r)?, }), 1 => Ok(PitchSpacePosition::Integer { space_size: Codec::dec(r)?, index: Codec::dec(r)?, }), 2 => Ok(PitchSpacePosition::JiVector { components: Codec::dec(r)?, }), 3 => Ok(PitchSpacePosition::Registered(Codec::dec(r)?)), tag => Err(ScoreDecodeError::InvalidTag { kind: "PitchSpacePosition", tag, }), } } } struct_codec!(ScalePosition { space, position }); impl Codec for TuningReference { fn enc(&self, out: &mut Vec) { match self { TuningReference::Inherit => out.push(0), TuningReference::Explicit(id) => { out.push(1); id.enc(out); } } } fn dec(r: &mut Reader<'_>) -> Result { match r.u8()? { 0 => Ok(TuningReference::Inherit), 1 => Ok(TuningReference::Explicit(Codec::dec(r)?)), tag => Err(ScoreDecodeError::InvalidTag { kind: "TuningReference", tag, }), } } } impl Codec for AcousticRealization { fn enc(&self, out: &mut Vec) { match self { AcousticRealization::Implicit => out.push(0), AcousticRealization::CentsOffset(c) => { out.push(1); c.enc(out); } AcousticRealization::AbsoluteHz(c) => { out.push(2); c.enc(out); } } } fn dec(r: &mut Reader<'_>) -> Result { match r.u8()? { 0 => Ok(AcousticRealization::Implicit), 1 => Ok(AcousticRealization::CentsOffset(Codec::dec(r)?)), 2 => Ok(AcousticRealization::AbsoluteHz(Codec::dec(r)?)), tag => Err(ScoreDecodeError::InvalidTag { kind: "AcousticRealization", tag, }), } } } impl Codec for ReferencePitch { fn enc(&self, out: &mut Vec) { self.position.enc(out); let hz = CanonicalF64::new(self.frequency_hz()).expect("valid reference pitch is finite"); hz.enc(out); } fn dec(r: &mut Reader<'_>) -> Result { let position = PitchSpacePosition::dec(r)?; let hz = CanonicalF64::dec(r)?; ReferencePitch::new(position, hz.get()) .ok_or(ScoreDecodeError::Reconstruct("ReferencePitch")) } } struct_codec!(AcousticPitch { tuning, realization }); // Schema major 1: `Instrument.range` embeds this (appended after `name`). struct_codec!(PitchRange { lowest, highest }); struct_codec!(Pitch { scale_position, acoustic }); struct_codec!(IdentifiedPitch { id, pitch }); impl Codec for SpellingNominal { fn enc(&self, out: &mut Vec) { match self { SpellingNominal::Cmn(n) => { out.push(0); n.enc(out); } SpellingNominal::Integer(i) => { out.push(1); i.enc(out); } SpellingNominal::Registered(id) => { out.push(2); id.enc(out); } } } fn dec(r: &mut Reader<'_>) -> Result { match r.u8()? { 0 => Ok(SpellingNominal::Cmn(Codec::dec(r)?)), 1 => Ok(SpellingNominal::Integer(Codec::dec(r)?)), 2 => Ok(SpellingNominal::Registered(Codec::dec(r)?)), tag => Err(ScoreDecodeError::InvalidTag { kind: "SpellingNominal", tag, }), } } } struct_codec!(SpellingRenderHints { parenthesized, cautionary, editorial, small_print }); struct_codec!(PitchSpelling { nominal, accidentals, octave, render_hints }); impl Codec for SpellingSource { fn enc(&self, out: &mut Vec) { match self { SpellingSource::UserChosen => out.push(0), SpellingSource::Inferred => out.push(1), SpellingSource::Imported { format } => { out.push(2); format.enc(out); } SpellingSource::Propagated { from } => { out.push(3); from.enc(out); } SpellingSource::Analytical => out.push(4), } } fn dec(r: &mut Reader<'_>) -> Result { match r.u8()? { 0 => Ok(SpellingSource::UserChosen), 1 => Ok(SpellingSource::Inferred), 2 => Ok(SpellingSource::Imported { format: Codec::dec(r)?, }), 3 => Ok(SpellingSource::Propagated { from: Codec::dec(r)?, }), 4 => Ok(SpellingSource::Analytical), tag => Err(ScoreDecodeError::InvalidTag { kind: "SpellingSource", tag, }), } } } impl Codec for SpellingPrecedence { fn enc(&self, out: &mut Vec) { self.order_ref().to_vec().enc(out); } fn dec(r: &mut Reader<'_>) -> Result { let order: Vec = Codec::dec(r)?; SpellingPrecedence::new(order).ok_or(ScoreDecodeError::Reconstruct("SpellingPrecedence")) } } impl Codec for VoiceSelector { fn enc(&self, out: &mut Vec) { match self { VoiceSelector::All => out.push(0), VoiceSelector::Voices(v) => { out.push(1); v.enc(out); } } } fn dec(r: &mut Reader<'_>) -> Result { match r.u8()? { 0 => Ok(VoiceSelector::All), 1 => Ok(VoiceSelector::Voices(Codec::dec(r)?)), tag => Err(ScoreDecodeError::InvalidTag { kind: "VoiceSelector", tag, }), } } } struct_codec!(SpellingRule { rule_set }); impl Codec for SpellingScope { fn enc(&self, out: &mut Vec) { match self { SpellingScope::Pitch(id) => { out.push(0); id.enc(out); } SpellingScope::Range { start, end, voices } => { out.push(1); start.enc(out); end.enc(out); voices.enc(out); } } } fn dec(r: &mut Reader<'_>) -> Result { match r.u8()? { 0 => Ok(SpellingScope::Pitch(Codec::dec(r)?)), 1 => Ok(SpellingScope::Range { start: Codec::dec(r)?, end: Codec::dec(r)?, voices: Codec::dec(r)?, }), tag => Err(ScoreDecodeError::InvalidTag { kind: "SpellingScope", tag, }), } } } impl Codec for SpellingDirective { fn enc(&self, out: &mut Vec) { match self { SpellingDirective::Explicit(s) => { out.push(0); s.enc(out); } SpellingDirective::Rule(rule) => { out.push(1); rule.enc(out); } } } fn dec(r: &mut Reader<'_>) -> Result { match r.u8()? { 0 => Ok(SpellingDirective::Explicit(Codec::dec(r)?)), 1 => Ok(SpellingDirective::Rule(Codec::dec(r)?)), tag => Err(ScoreDecodeError::InvalidTag { kind: "SpellingDirective", tag, }), } } } struct_codec!(SpellingAttachment { scope, directive, source, priority, layer }); // =========================================================================== // event.rs // =========================================================================== impl Codec for GraceKind { fn enc(&self, out: &mut Vec) { match self { GraceKind::Acciaccatura => out.push(0), GraceKind::Appoggiatura => out.push(1), GraceKind::Unmeasured => out.push(2), GraceKind::MeasuredFraction(d) => { out.push(3); d.enc(out); } } } fn dec(r: &mut Reader<'_>) -> Result { match r.u8()? { 0 => Ok(GraceKind::Acciaccatura), 1 => Ok(GraceKind::Appoggiatura), 2 => Ok(GraceKind::Unmeasured), 3 => Ok(GraceKind::MeasuredFraction(Codec::dec(r)?)), tag => Err(ScoreDecodeError::InvalidTag { kind: "GraceKind", tag, }), } } } impl Codec for IndeterminacyKind { fn enc(&self, out: &mut Vec) { match self { IndeterminacyKind::Pitch => out.push(0), IndeterminacyKind::Duration => out.push(1), IndeterminacyKind::Choice => out.push(2), IndeterminacyKind::Compound(v) => { out.push(3); v.enc(out); } } } fn dec(r: &mut Reader<'_>) -> Result { match r.u8()? { 0 => Ok(IndeterminacyKind::Pitch), 1 => Ok(IndeterminacyKind::Duration), 2 => Ok(IndeterminacyKind::Choice), 3 => Ok(IndeterminacyKind::Compound(Codec::dec(r)?)), tag => Err(ScoreDecodeError::InvalidTag { kind: "IndeterminacyKind", tag, }), } } } struct_codec!(IndeterminacyHints { duration_bounds, alternatives, textual_instruction }); impl Codec for TrajectoryEndpoint { fn enc(&self, out: &mut Vec) { match self { TrajectoryEndpoint::EventPitch(id) => { out.push(0); id.enc(out); } TrajectoryEndpoint::ExplicitPitch(p) => { out.push(1); p.enc(out); } } } fn dec(r: &mut Reader<'_>) -> Result { match r.u8()? { 0 => Ok(TrajectoryEndpoint::EventPitch(Codec::dec(r)?)), 1 => Ok(TrajectoryEndpoint::ExplicitPitch(Codec::dec(r)?)), tag => Err(ScoreDecodeError::InvalidTag { kind: "TrajectoryEndpoint", tag, }), } } } impl Codec for TrajectoryShape { fn enc(&self, out: &mut Vec) { match self { TrajectoryShape::Linear => out.push(0), TrajectoryShape::Exponential => out.push(1), TrajectoryShape::Curve => out.push(2), TrajectoryShape::Stepwise(v) => { out.push(3); v.enc(out); } } } fn dec(r: &mut Reader<'_>) -> Result { match r.u8()? { 0 => Ok(TrajectoryShape::Linear), 1 => Ok(TrajectoryShape::Exponential), 2 => Ok(TrajectoryShape::Curve), 3 => Ok(TrajectoryShape::Stepwise(Codec::dec(r)?)), tag => Err(ScoreDecodeError::InvalidTag { kind: "TrajectoryShape", tag, }), } } } struct_codec!(PitchedEvent { id, voice, position, duration, pitches, articulations, dynamic, ornaments, stem, grace }); struct_codec!(UnpitchedEvent { id, voice, position, duration, staff_position, instrument_member, articulations, dynamic, stem, grace }); struct_codec!(Rest { id, voice, position, duration, vertical_position, visible }); struct_codec!(IndeterminateEvent { id, voice, position, duration, indeterminacy, hints }); struct_codec!(TrajectoryEvent { id, voice, position, duration, start, end, shape, display }); struct_codec!(GraphicEvent { id, voice, position, duration, graphics, playback_bindings }); struct_codec!(CueEvent { id, voice, position, duration, source, rendering }); impl Codec for Event { fn enc(&self, out: &mut Vec) { match self { Event::Pitched(e) => { out.push(0); e.enc(out); } Event::Unpitched(e) => { out.push(1); e.enc(out); } Event::Rest(e) => { out.push(2); e.enc(out); } Event::Indeterminate(e) => { out.push(3); e.enc(out); } Event::Trajectory(e) => { out.push(4); e.enc(out); } Event::Graphic(e) => { out.push(5); e.enc(out); } Event::Cue(e) => { out.push(6); e.enc(out); } } } fn dec(r: &mut Reader<'_>) -> Result { match r.u8()? { 0 => Ok(Event::Pitched(Codec::dec(r)?)), 1 => Ok(Event::Unpitched(Codec::dec(r)?)), 2 => Ok(Event::Rest(Codec::dec(r)?)), 3 => Ok(Event::Indeterminate(Codec::dec(r)?)), 4 => Ok(Event::Trajectory(Codec::dec(r)?)), 5 => Ok(Event::Graphic(Codec::dec(r)?)), 6 => Ok(Event::Cue(Codec::dec(r)?)), tag => Err(ScoreDecodeError::InvalidTag { kind: "Event", tag }), } } } impl Codec for EventArena { fn enc(&self, out: &mut Vec) { // Canonical (ascending EventId) order; identity travels inside each event. let events: Vec<&Event> = self.iter_canonical().collect(); put_len(out, events.len()); for event in events { event.enc(out); } } fn dec(r: &mut Reader<'_>) -> Result { let n = r.count()?; let mut arena = EventArena::new(); for _ in 0..n { let event = Event::dec(r)?; arena .insert(event) .map_err(|_| ScoreDecodeError::Reconstruct("EventArena"))?; } Ok(arena) } } // =========================================================================== // tempo.rs // =========================================================================== cstyle_enum_codec!(TempoShape { 0 => Constant, 1 => Linear, 2 => Exponential, 3 => Curve, }); impl Codec for Tempo { fn enc(&self, out: &mut Vec) { let bpm = CanonicalF64::new(self.bpm()).expect("valid tempo bpm is finite"); bpm.enc(out); self.beat_unit().enc(out); } fn dec(r: &mut Reader<'_>) -> Result { let bpm = CanonicalF64::dec(r)?; let beat_unit = MusicalDuration::dec(r)?; Tempo::new(bpm.get(), beat_unit).ok_or(ScoreDecodeError::Reconstruct("Tempo")) } } struct_codec!(TempoSegment { start, end, start_tempo, end_tempo, shape }); struct_codec!(TempoMap { initial, segments }); // =========================================================================== // graph.rs // =========================================================================== cstyle_enum_codec!(StemDirection { 0 => Up, 1 => Down }); cstyle_enum_codec!(MeasureNumberVisibility { 0 => Auto, 1 => Always, 2 => Never, }); cstyle_enum_codec!(AleatoricAnchoringDiscipline { 0 => Musical, 1 => WallClock, 2 => EitherPerEvent, 3 => FreelyMixed, }); cstyle_enum_codec!(NoteValue { 0 => Whole, 1 => Half, 2 => Quarter, 3 => Eighth, 4 => Sixteenth, 5 => ThirtySecond, 6 => SixtyFourth, }); // --- Schema-major-2 leaf types (Binary Format §Schema Major 2). ------------- // Every layout below is the ratified v2 wire form: tag-only enums are one // discriminant byte; `SpaceUnit` rides its `CanonicalF64` leaf (framed 12); // `Timestamp` is one bare i64 LE; fixed-width primitives inside structs are // bare; identifiers keep the leaf framing. impl Codec for Timestamp { fn enc(&self, out: &mut Vec) { self.0.enc(out); } fn dec(r: &mut Reader<'_>) -> Result { Ok(Timestamp(i64::dec(r)?)) } } impl Codec for SpaceUnit { fn enc(&self, out: &mut Vec) { self.0.enc(out); } fn dec(r: &mut Reader<'_>) -> Result { Ok(SpaceUnit(Codec::dec(r)?)) } } impl Codec for SoundConfiguration { fn enc(&self, out: &mut Vec) { self.0.enc(out); } fn dec(r: &mut Reader<'_>) -> Result { Ok(SoundConfiguration(Codec::dec(r)?)) } } impl Codec for OctaveOffset { fn enc(&self, out: &mut Vec) { self.0.enc(out); } fn dec(r: &mut Reader<'_>) -> Result { Ok(OctaveOffset(i8::dec(r)?)) } } cstyle_enum_codec!(LineStyle { 0 => Solid, 1 => Dashed, 2 => Dotted }); cstyle_enum_codec!(SlurKind { 0 => Legato, 1 => Phrase, 2 => Articulation, 3 => Editorial, }); cstyle_enum_codec!(CurveDirection { 0 => Above, 1 => Below }); cstyle_enum_codec!(HairpinDirection { 0 => Crescendo, 1 => Diminuendo, }); cstyle_enum_codec!(PedalKind { 0 => Sustain, 1 => Sostenuto, 2 => UnaCorda, }); cstyle_enum_codec!(BracketKind { 0 => Square }); cstyle_enum_codec!(StaffBracketKind { 0 => Brace, 1 => Bracket }); impl Codec for SpannerKind { fn enc(&self, out: &mut Vec) { match self { SpannerKind::Generic => out.push(0), SpannerKind::Hairpin(d) => { out.push(1); d.enc(out); } SpannerKind::OctaveLine(o) => { out.push(2); o.enc(out); } SpannerKind::PedalLine(p) => { out.push(3); p.enc(out); } SpannerKind::TrillExtension => out.push(4), SpannerKind::Glissando => out.push(5), SpannerKind::Portamento => out.push(6), SpannerKind::TextLine(t) => { out.push(7); t.enc(out); } SpannerKind::Bracket(b) => { out.push(8); b.enc(out); } } } fn dec(r: &mut Reader<'_>) -> Result { match r.u8()? { 0 => Ok(SpannerKind::Generic), 1 => Ok(SpannerKind::Hairpin(Codec::dec(r)?)), 2 => Ok(SpannerKind::OctaveLine(Codec::dec(r)?)), 3 => Ok(SpannerKind::PedalLine(Codec::dec(r)?)), 4 => Ok(SpannerKind::TrillExtension), 5 => Ok(SpannerKind::Glissando), 6 => Ok(SpannerKind::Portamento), 7 => Ok(SpannerKind::TextLine(Codec::dec(r)?)), 8 => Ok(SpannerKind::Bracket(Codec::dec(r)?)), tag => Err(ScoreDecodeError::InvalidTag { kind: "SpannerKind", tag, }), } } } impl Codec for RepeatKind { fn enc(&self, out: &mut Vec) { match self { RepeatKind::SimpleRepeat { count } => { out.push(0); count.enc(out); } RepeatKind::DaCapo { end_target } => { out.push(1); end_target.enc(out); } RepeatKind::DalSegno { segno, end_target } => { out.push(2); segno.enc(out); end_target.enc(out); } RepeatKind::Volta => out.push(3), } } fn dec(r: &mut Reader<'_>) -> Result { match r.u8()? { 0 => Ok(RepeatKind::SimpleRepeat { count: Codec::dec(r)?, }), 1 => Ok(RepeatKind::DaCapo { end_target: Codec::dec(r)?, }), 2 => Ok(RepeatKind::DalSegno { segno: Codec::dec(r)?, end_target: Codec::dec(r)?, }), 3 => Ok(RepeatKind::Volta), tag => Err(ScoreDecodeError::InvalidTag { kind: "RepeatKind", tag, }), } } } impl Codec for MetadataValue { fn enc(&self, out: &mut Vec) { match self { MetadataValue::Text(s) => { out.push(0); s.enc(out); } MetadataValue::Integer(i) => { out.push(1); i.enc(out); } MetadataValue::Flag(b) => { out.push(2); b.enc(out); } } } fn dec(r: &mut Reader<'_>) -> Result { match r.u8()? { 0 => Ok(MetadataValue::Text(Codec::dec(r)?)), 1 => Ok(MetadataValue::Integer(Codec::dec(r)?)), 2 => Ok(MetadataValue::Flag(Codec::dec(r)?)), tag => Err(ScoreDecodeError::InvalidTag { kind: "MetadataValue", tag, }), } } } struct_codec!(CurvatureOverride { direction, height }); struct_codec!(SpanStyle { line, thickness }); struct_codec!(SubBeam { level, events }); struct_codec!(BeamGeometryOverride { slope, offset }); struct_codec!(TextLineDefinition { text }); struct_codec!(Volta { endings, start, end }); struct_codec!(MetadataEntry { key, value }); struct_codec!(TranspositionInterval { diatonic_steps, chromatic_steps }); struct_codec!(UnpitchedMember { member, name, staff_position }); // Schema major 2: `ScoreMetadata` appended six fields after the major-0/1 // order. The frozen prior layout (`title`, `composer`, `copyright`) is read // by `dec_metadata_v1`. struct_codec!(ScoreMetadata { title, composer, copyright, subtitle, lyricist, arranger, creation_timestamp, modification_timestamp, additional }); // Schema major 1: `Instrument` gained `range` (appended after `name`); the // frozen major-0 layout (`id`, `name`) is read by `dec_instruments_v0`. // Schema major 2 appended six more fields after `range`; the frozen major-1 // layout (`id`, `name`, `range`) is read by `dec_instruments_v1`. struct_codec!(Instrument { id, name, range, abbreviation, sound_config, transposition, default_clef, default_staff_lines, unpitched_members }); // Schema major 2: three fields appended after `line_count`; the frozen prior // layout (`line_count` only) is read by `dec_staff_lines_v1`. struct_codec!(StaffLineConfiguration { line_count, line_spacing, line_style, bracket }); struct_codec!(GraphicObject { id }); struct_codec!(GraphicContent { objects }); struct_codec!(TimeExtent { start, end }); struct_codec!(StaffExtent { staves }); // Schema major 2: `default_clef` appended last; the frozen prior layout is // read by `dec_staff_v1`. struct_codec!(Staff { id, name, abbreviation, instrument, default_staff_lines, group, default_clef }); struct_codec!(PartDefinition { id, name, staves }); struct_codec!(AnalysisLayer { id, name }); struct_codec!(ViewDefinition { id, name, active_layers }); struct_codec!(MeterChange { anchor, time_signature }); struct_codec!(MetricGrid { meter_sequence }); struct_codec!(ProportionalTimeModel { duration }); struct_codec!(MetricTimeModel { meters, tempo }); cstyle_enum_codec!(ClefShape { 0 => G, 1 => F, 2 => C, 3 => Percussion, }); struct_codec!(Clef { shape, line, octave_shift }); impl Codec for KeySignature { fn enc(&self, out: &mut Vec) { self.fifths().enc(out); } fn dec(r: &mut Reader<'_>) -> Result { KeySignature::new(i8::dec(r)?).ok_or(ScoreDecodeError::Reconstruct("KeySignature")) } } struct_codec!(ClefChange { anchor, clef }); struct_codec!(KeySignatureChange { anchor, key }); struct_codec!(Measure { id, start, time_signature, explicit_number, number_visibility }); struct_codec!(BeatGroup { duration, subdivision, accent }); // =========================================================================== // accidental.rs / tuning.rs — the schema-major-3 leaf layouts staged onto the // wire this tranche (Push 4b tranche 3b-i, `spec/CONTRACT_PUSH4B_3BI_WIRE.md`). // Only these four types gain a `Codec`; `accidental_extensions` and its whole // subtree stay in-memory-only (no `Codec` exists, or may exist, for them — // `accidental.rs`'s own module doc). Hand-written rather than // `struct_codec!`/`cstyle_enum_codec!`: `struct_codec!` also generates a // `TextValue` impl, and these four types have none (text projection is a // separate surface this tranche does not touch — see the staging-boundary // tests below and `textvalue_graph.rs`), so the macro would either fail to // compile (no `TextValue` for `u16`/`TuningScope`) or silently open a new // text-projection surface neither asked for nor specified. Every layout here // is now FROZEN (`req:binfmt:frozen-layout`) — get the field order right. impl Codec for SmuflVersion { fn enc(&self, out: &mut Vec) { self.major.enc(out); self.minor_centi.enc(out); } fn dec(r: &mut Reader<'_>) -> Result { let major = u16::dec(r)?; let minor_centi = u16::dec(r)?; Ok(SmuflVersion { major, minor_centi }) } } impl Codec for SmuflVersionRequirement { fn enc(&self, out: &mut Vec) { self.minimum.enc(out); self.authored_against.enc(out); } fn dec(r: &mut Reader<'_>) -> Result { let minimum = SmuflVersion::dec(r)?; let authored_against = SmuflVersion::dec(r)?; Ok(SmuflVersionRequirement { minimum, authored_against, }) } } impl Codec for TuningScope { fn enc(&self, out: &mut Vec) { match self { TuningScope::Voice(id) => { out.push(0); id.enc(out); } TuningScope::Staff(id) => { out.push(1); id.enc(out); } TuningScope::Region(id) => { out.push(2); id.enc(out); } TuningScope::Range { start, end, voices } => { out.push(3); start.enc(out); end.enc(out); voices.enc(out); } } } fn dec(r: &mut Reader<'_>) -> Result { match r.u8()? { 0 => Ok(TuningScope::Voice(Codec::dec(r)?)), 1 => Ok(TuningScope::Staff(Codec::dec(r)?)), 2 => Ok(TuningScope::Region(Codec::dec(r)?)), 3 => Ok(TuningScope::Range { start: Codec::dec(r)?, end: Codec::dec(r)?, voices: Codec::dec(r)?, }), tag => Err(ScoreDecodeError::InvalidTag { kind: "TuningScope", tag, }), } } } impl Codec for TuningOverride { fn enc(&self, out: &mut Vec) { self.scope.enc(out); self.pitch_space.enc(out); self.tuning_system.enc(out); self.reference.enc(out); } fn dec(r: &mut Reader<'_>) -> Result { let scope = Codec::dec(r)?; let pitch_space = Codec::dec(r)?; let tuning_system = Codec::dec(r)?; let reference = Codec::dec(r)?; Ok(TuningOverride { scope, pitch_space, tuning_system, reference, }) } } // `ScoreTuningContext` is schema major 3 (Push 4b tranche 3b-i, // `spec/CONTRACT_PUSH4B_3BI_WIRE.md`): `smufl` and `overrides` join the // major-0 prefix (`default_pitch_space`, `default_tuning_system`, // `reference`) on the wire, in that order — append-after-existing, per the // frozen-layout rule. `accidental_extensions` stays in-memory-only (staged to // a later major, appended after `overrides` when it lands); `struct_codec!` // still cannot express that one holdout, so this impl stays hand-written. // `dec` default-fills only `accidental_extensions: Vec::new()`. // // The pre-v3 form (3 fields; majors 0/1/2 all share it) is frozen separately // as `enc_tuning_context_v2`/`dec_tuning_context_v2` below, consumed by the // frozen `decode_v0_score`/`decode_v1_score`/`decode_v2_score` (and their // byte-exact-inverse `encode_v*_score` siblings) so those frozen forms never // drift when this live impl changes. A round-trip test just below // (`score_tuning_context_smufl_and_overrides_reach_the_wire_accidental_extensions_do_not`) // proves `smufl`/`overrides` now survive `enc`→`dec` while // `accidental_extensions` still does not. The text projection now draws the // staging line in the same place, and its proofs are the direct analogues: // `textvalue_graph.rs`'s `score_tuning_context_round_trips_and_overrides_project` // and `score_tuning_context_projects_smufl_and_overrides_but_not_accidental_extensions`. // (3b-i left the text surface untouched, deferring it; the parity pass that // followed moved `smufl`/`overrides` there for the reason stated in // `TextValue for ScoreTuningContext`'s doc — the text projection is the same // canonical surface this codec is.) impl Codec for ScoreTuningContext { fn enc(&self, out: &mut Vec) { self.default_pitch_space.enc(out); self.default_tuning_system.enc(out); self.reference.enc(out); self.smufl.enc(out); self.overrides.enc(out); } fn dec(r: &mut Reader<'_>) -> Result { let default_pitch_space = Codec::dec(r)?; let default_tuning_system = Codec::dec(r)?; let reference = Codec::dec(r)?; let smufl = Codec::dec(r)?; let overrides = Codec::dec(r)?; Ok(ScoreTuningContext { default_pitch_space, default_tuning_system, reference, accidental_extensions: Vec::new(), smufl, overrides, }) } } // `TuningContextSettings` (genesis tranche G2b, // `spec/CONTRACT_GENESIS_G2B_TUNING.md` §1): the authored subset of // `ScoreTuningContext` that `SetTuningContext` carries — exactly its five // wire-bearing fields, in the codec's existing order. This walk is // **byte-identical** to `ScoreTuningContext`'s `enc`/`dec` above by // construction (same five fields, same order, same per-field codecs) — a // type-level narrowing, not a new wire form. The identity is asserted by a // round-trip test just below // (`tuning_context_settings_canonical_bytes_match_score_tuning_context`). impl Codec for TuningContextSettings { fn enc(&self, out: &mut Vec) { self.default_pitch_space.enc(out); self.default_tuning_system.enc(out); self.reference.enc(out); self.smufl.enc(out); self.overrides.enc(out); } fn dec(r: &mut Reader<'_>) -> Result { let default_pitch_space = Codec::dec(r)?; let default_tuning_system = Codec::dec(r)?; let reference = Codec::dec(r)?; let smufl = Codec::dec(r)?; let overrides = Codec::dec(r)?; Ok(TuningContextSettings { default_pitch_space, default_tuning_system, reference, smufl, overrides, }) } } // Schema major 2: the cross-cutting bodies filled (appended fields); the // frozen prior layouts are read by the `dec_*_v1` sub-decoders. struct_codec!(Slur { id, start_event, end_event, kind, curvature_override, style }); struct_codec!(Beam { id, events, level, sub_beams, geometry_override }); // TupletRatio is not a plain struct_codec!: it has private fields and a checked // constructor that rejects degenerate ratios, so decode must validate too // (a malformed bundle cannot inject a degenerate ratio). impl Codec for TupletRatio { fn enc(&self, out: &mut Vec) { self.actual().enc(out); self.notated().enc(out); } fn dec(r: &mut Reader<'_>) -> Result { let actual = Codec::dec(r)?; let notated = Codec::dec(r)?; TupletRatio::new(actual, notated).ok_or(ScoreDecodeError::Reconstruct( "TupletRatio: degenerate ratio", )) } } struct_codec!(Tuplet { id, ratio, members, parent, required_total }); struct_codec!(Spanner { id, start, end, staves, kind, style }); struct_codec!(Marker { id, anchor }); struct_codec!(RepeatStructure { id, start, end, kind, voltas }); struct_codec!(Comment { id, anchor, resolved }); struct_codec!(LyricLine { id, events }); struct_codec!(ChordSymbol { id, anchor }); struct_codec!(AnalyticalAnnotation { id, anchor, layer }); struct_codec!(GraphicGesture { id, objects, anchoring }); struct_codec!(BarlineAlignmentMember { staff_instance, measure, position }); struct_codec!(BarlineAlignmentGroup { id, members }); struct_codec!(NotatedComponent { base_value, dots, tuplet, tied_to_next }); struct_codec!(Voice { id, events, default_stem_direction, is_primary, origin }); struct_codec!(StaffInstance { id, staff, voices, clef_sequence, key_sequence, local_metric_grid, measures, instrument_override, staff_lines_override, visible }); struct_codec!(StaffBasedContent { staff_instances, default_metric_grid, barline_alignment_groups, user_system_breaks, user_page_breaks }); // Schema major 1: `Region` gained `permits_spanning_slurs` (appended after // `local_tempo_map`). The frozen major-0 layout (the six fields before it) is // read by `dec_region_v0`. `Region` is also a `CanonicalValue` embedded in the // canonical `CreateRegion` op payload, so this byte change is canonical — the // op-block migration for it is a separate change (schema-major track, D2). struct_codec!(Region { id, time_model, content, time_extent, staff_extent, local_tempo_map, permits_spanning_slurs }); struct_codec!(CanvasSize { width, height }); struct_codec!(CanvasMargins { top, right, bottom, left }); struct_codec!(CanvasLayoutDefaults { page_size, margins }); // Schema major 1: `Canvas` gained `layout_defaults` (appended after `regions`). // The frozen major-0 layout (`regions` only) is read by `decode_v0_score`. struct_codec!(Canvas { regions, layout_defaults }); struct_codec!(CrossCuttingRegistry { slurs, ties, beams, tuplets, spanners, markers, repeats, analytical, comments, graphic_gestures, lyrics, chord_symbols }); struct_codec!(IdentityContext { replica_id, next_counter }); impl Codec for EventOrderingDAG { fn enc(&self, out: &mut Vec) { self.edges_ref().enc(out); } fn dec(r: &mut Reader<'_>) -> Result { let edges: BTreeMap> = Codec::dec(r)?; EventOrderingDAG::try_new(edges).ok_or(ScoreDecodeError::Reconstruct("EventOrderingDAG")) } } struct_codec!(AleatoricTimeModel { ordering, anchoring, bounds, duration_hint }); impl Codec for RegionTimeModel { fn enc(&self, out: &mut Vec) { match self { RegionTimeModel::Metric(m) => { out.push(0); m.enc(out); } RegionTimeModel::Proportional(p) => { out.push(1); p.enc(out); } RegionTimeModel::Aleatoric(a) => { out.push(2); a.enc(out); } } } fn dec(r: &mut Reader<'_>) -> Result { match r.u8()? { 0 => Ok(RegionTimeModel::Metric(Codec::dec(r)?)), 1 => Ok(RegionTimeModel::Proportional(Codec::dec(r)?)), 2 => Ok(RegionTimeModel::Aleatoric(Codec::dec(r)?)), tag => Err(ScoreDecodeError::InvalidTag { kind: "RegionTimeModel", tag, }), } } } impl Codec for RegionContent { fn enc(&self, out: &mut Vec) { match self { RegionContent::StaffBased(s) => { out.push(0); s.enc(out); } RegionContent::FreeGraphic(g) => { out.push(1); g.enc(out); } RegionContent::Hybrid { staves, overlay, overlay_below_staves, } => { out.push(2); staves.enc(out); overlay.enc(out); overlay_below_staves.enc(out); } } } fn dec(r: &mut Reader<'_>) -> Result { match r.u8()? { 0 => Ok(RegionContent::StaffBased(Codec::dec(r)?)), 1 => Ok(RegionContent::FreeGraphic(Codec::dec(r)?)), 2 => Ok(RegionContent::Hybrid { staves: Codec::dec(r)?, overlay: Codec::dec(r)?, overlay_below_staves: Codec::dec(r)?, }), tag => Err(ScoreDecodeError::InvalidTag { kind: "RegionContent", tag, }), } } } impl Codec for VoiceOrigin { fn enc(&self, out: &mut Vec) { match self { VoiceOrigin::UserDeclared => out.push(0), VoiceOrigin::Imported { format } => { out.push(1); format.enc(out); } VoiceOrigin::SystemPromoted { winning_operation, losing_operation, original_voice, } => { out.push(2); winning_operation.enc(out); losing_operation.enc(out); original_voice.enc(out); } } } fn dec(r: &mut Reader<'_>) -> Result { match r.u8()? { 0 => Ok(VoiceOrigin::UserDeclared), 1 => Ok(VoiceOrigin::Imported { format: Codec::dec(r)?, }), 2 => Ok(VoiceOrigin::SystemPromoted { winning_operation: Codec::dec(r)?, losing_operation: Codec::dec(r)?, original_voice: Codec::dec(r)?, }), tag => Err(ScoreDecodeError::InvalidTag { kind: "VoiceOrigin", tag, }), } } } impl Codec for StaffGroupKind { fn enc(&self, out: &mut Vec) { match self { StaffGroupKind::GrandStaff => out.push(0), StaffGroupKind::Bracket => out.push(1), StaffGroupKind::SubBracket => out.push(2), StaffGroupKind::Choral => out.push(3), StaffGroupKind::Registered(id) => { out.push(4); id.enc(out); } } } fn dec(r: &mut Reader<'_>) -> Result { match r.u8()? { 0 => Ok(StaffGroupKind::GrandStaff), 1 => Ok(StaffGroupKind::Bracket), 2 => Ok(StaffGroupKind::SubBracket), 3 => Ok(StaffGroupKind::Choral), 4 => Ok(StaffGroupKind::Registered(Codec::dec(r)?)), tag => Err(ScoreDecodeError::InvalidTag { kind: "StaffGroupKind", tag, }), } } } struct_codec!(StaffGroup { id, name, kind, members }); impl Codec for TieClass { fn enc(&self, out: &mut Vec) { match self { TieClass::Standard => out.push(0), TieClass::Editorial => out.push(1), TieClass::CrossVoice => out.push(2), TieClass::LaissezVibrer => out.push(3), TieClass::Registered(id) => { out.push(4); id.enc(out); } } } fn dec(r: &mut Reader<'_>) -> Result { match r.u8()? { 0 => Ok(TieClass::Standard), 1 => Ok(TieClass::Editorial), 2 => Ok(TieClass::CrossVoice), 3 => Ok(TieClass::LaissezVibrer), 4 => Ok(TieClass::Registered(Codec::dec(r)?)), tag => Err(ScoreDecodeError::InvalidTag { kind: "TieClass", tag, }), } } } struct_codec!(Tie { id, start_event, end_event, pitch_pairing, class, style }); impl Codec for AnnotationAnchor { fn enc(&self, out: &mut Vec) { match self { AnnotationAnchor::Event(id) => { out.push(0); id.enc(out); } AnnotationAnchor::Range { start, end } => { out.push(1); start.enc(out); end.enc(out); } AnnotationAnchor::Region(id) => { out.push(2); id.enc(out); } } } fn dec(r: &mut Reader<'_>) -> Result { match r.u8()? { 0 => Ok(AnnotationAnchor::Event(Codec::dec(r)?)), 1 => Ok(AnnotationAnchor::Range { start: Codec::dec(r)?, end: Codec::dec(r)?, }), 2 => Ok(AnnotationAnchor::Region(Codec::dec(r)?)), tag => Err(ScoreDecodeError::InvalidTag { kind: "AnnotationAnchor", tag, }), } } } impl Codec for GestureAnchoring { fn enc(&self, out: &mut Vec) { match self { GestureAnchoring::Events(v) => { out.push(0); v.enc(out); } GestureAnchoring::Range { start, end, staves } => { out.push(1); start.enc(out); end.enc(out); staves.enc(out); } GestureAnchoring::Free => out.push(2), } } fn dec(r: &mut Reader<'_>) -> Result { match r.u8()? { 0 => Ok(GestureAnchoring::Events(Codec::dec(r)?)), 1 => Ok(GestureAnchoring::Range { start: Codec::dec(r)?, end: Codec::dec(r)?, staves: Codec::dec(r)?, }), 2 => Ok(GestureAnchoring::Free), tag => Err(ScoreDecodeError::InvalidTag { kind: "GestureAnchoring", tag, }), } } } impl Codec for DecompositionSource { fn enc(&self, out: &mut Vec) { match self { DecompositionSource::UserChosen => out.push(0), DecompositionSource::Inferred => out.push(1), DecompositionSource::Imported { format } => { out.push(2); format.enc(out); } DecompositionSource::Propagated { from } => { out.push(3); from.enc(out); } } } fn dec(r: &mut Reader<'_>) -> Result { match r.u8()? { 0 => Ok(DecompositionSource::UserChosen), 1 => Ok(DecompositionSource::Inferred), 2 => Ok(DecompositionSource::Imported { format: Codec::dec(r)?, }), 3 => Ok(DecompositionSource::Propagated { from: Codec::dec(r)?, }), tag => Err(ScoreDecodeError::InvalidTag { kind: "DecompositionSource", tag, }), } } } struct_codec!(DecompositionAttachment { target, components, source }); impl Codec for TimeSignatureDisplay { fn enc(&self, out: &mut Vec) { match self { TimeSignatureDisplay::Standard { numerator, denominator, } => { out.push(0); numerator.enc(out); denominator.enc(out); } TimeSignatureDisplay::Compound { numerators, denominator, } => { out.push(1); numerators.enc(out); denominator.enc(out); } TimeSignatureDisplay::Irrational { numerator, denominator, } => { out.push(2); numerator.enc(out); denominator.enc(out); } TimeSignatureDisplay::MixedDenominators { components } => { out.push(3); components.enc(out); } TimeSignatureDisplay::None => out.push(4), TimeSignatureDisplay::Symbolic(v) => { out.push(5); v.enc(out); } } } fn dec(r: &mut Reader<'_>) -> Result { match r.u8()? { 0 => Ok(TimeSignatureDisplay::Standard { numerator: Codec::dec(r)?, denominator: Codec::dec(r)?, }), 1 => Ok(TimeSignatureDisplay::Compound { numerators: Codec::dec(r)?, denominator: Codec::dec(r)?, }), 2 => Ok(TimeSignatureDisplay::Irrational { numerator: Codec::dec(r)?, denominator: Codec::dec(r)?, }), 3 => Ok(TimeSignatureDisplay::MixedDenominators { components: Codec::dec(r)?, }), 4 => Ok(TimeSignatureDisplay::None), 5 => Ok(TimeSignatureDisplay::Symbolic(Codec::dec(r)?)), tag => Err(ScoreDecodeError::InvalidTag { kind: "TimeSignatureDisplay", tag, }), } } } impl Codec for TimeSignature { fn enc(&self, out: &mut Vec) { self.id.enc(out); self.display.enc(out); self.measure_duration().enc(out); self.beat_groups().to_vec().enc(out); } fn dec(r: &mut Reader<'_>) -> Result { let id = Codec::dec(r)?; let display = TimeSignatureDisplay::dec(r)?; let measure_duration = MusicalDuration::dec(r)?; let beat_groups: Vec = Codec::dec(r)?; TimeSignature::new(id, display, measure_duration, beat_groups) .ok_or(ScoreDecodeError::Reconstruct("TimeSignature")) } } struct_codec!(Score { metadata, canvas, instruments, staves, staff_groups, parts, cross_cutting, time_signatures, tuning_context, tempo_map, events, spelling_attachments, decomposition_attachments, spelling_precedence, analysis_layers, views, identity, tombstoned_pitches, tombstoned_events }); // =========================================================================== // Public entry points on Score. // =========================================================================== impl Score { /// The canonical byte serialization of the whole score graph. Two equal /// scores produce identical bytes; the bytes round-trip through /// [`Score::decode_canonical`]. pub fn canonical_bytes(&self) -> Vec { let mut out = Vec::new(); self.enc(&mut out); out } /// Decodes the exact inverse of [`Score::canonical_bytes`], validating every /// tag, length, primitive, and type invariant. Trailing bytes are rejected. /// /// This is the **current (schema major 3)** layout. To decode bytes whose /// schema major is not known to be current, use /// [`Score::decode_canonical_versioned`]. /// /// Decoding is **strictly canonical**: an accepted byte string is its own /// canonical form (`req:format:codec-conventions`). Several leaf and /// collection decoders are individually *lenient* — they normalize on decode /// (an unreduced [`RationalTime`](crate::RationalTime) reduces to lowest /// terms; a `BTreeSet`/`BTreeMap` re-sorts and de-duplicates; a /// [`CanonicalF64`] / `ReferencePitch` / `Tempo` normalizes via its /// constructor) — so decode alone is not injective. This entry point closes /// that gap uniformly: it re-encodes the decoded score and rejects any input /// that is not already canonical, so two byte strings can never map to one /// score (which would break content-addressing). A valid encoder never emits /// a non-canonical score, so this only rejects corrupted or adversarial bytes. pub fn decode_canonical(bytes: &[u8]) -> Result { let mut r = Reader::new(bytes); let score = Score::dec(&mut r)?; r.finish()?; if score.canonical_bytes() != bytes { return Err(ScoreDecodeError::InvalidValue( "non-canonical Score encoding", )); } Ok(score) } /// The **schema-version dispatch seam** (Binary Format companion /// §"Schema Major 1" / §"Schema Major 2" / §"Schema Major 3"): decodes a /// full-`Score` snapshot whose bytes were written under the given schema /// `major`, migrating a lower-major encoding up to the current in-memory /// form on read. Major 3 is the current layout /// ([`Score::decode_canonical`]); majors 2, 1, and 0 are decoded through /// their frozen wire forms (`decode_v2_score`, `decode_v1_score`, /// `decode_v0_score`), each a total default-filling migration — the /// composed v0→v1→v2→v3 translation happens in the one v0 read. /// /// The caller (the bundle read path) only reaches this after the chunk gate /// has admitted the major into its accept-set, so a major outside /// `{0, 1, 2, 3}` is a defensive error, not an expected path. pub fn decode_canonical_versioned(bytes: &[u8], major: u16) -> Result { match major { 3 => Score::decode_canonical(bytes), 2 => decode_v2_score(bytes), 1 => decode_v1_score(bytes), 0 => decode_v0_score(bytes), _ => Err(ScoreDecodeError::InvalidValue("unsupported schema major")), } } } /// Decodes **schema-major-0** `Score` bytes into the current-layout `Score`, /// migrating on read. /// /// This is the **frozen v0 wire form, decoded by value** — a hand-written walk /// of the 19 `Score` fields in declaration order, using the current [`Codec`] /// for every field whose layout is unchanged, the frozen **v1** sub-decoders /// (v0 == v1) for the types schema major 2 filled, the frozen **v2** /// sub-decoder ([`dec_tuning_context_v2`]) for `tuning_context` (v0 == v1 == /// v2 — schema major 3 is the first bump to touch it, Push 4b tranche 3b-i), /// and a frozen v0 sub-decoder for the three that grew a field in schema /// major 1: /// /// * `Canvas` (field 2) — v0 was `regions` only; v1 appended `layout_defaults`. /// [`dec_canvas_v0`] reads the region vector and default-fills the defaults. /// * `Instrument` (inside field 3, `instruments: Vec`) — v0 was /// `{ id, name }`; v1 appended `range`. [`dec_instruments_v0`] default-fills /// `range: None`. /// * `Region` (inside `Canvas.regions`) — v0 was the six fields before /// `permits_spanning_slurs`; v1 appended it. [`dec_region_v0`] default-fills /// `false`. /// /// A byte-splice sufficed while only `Canvas` (a single top-level field) had /// changed, but two of the three grown structs are nested inside `Vec`s, so /// there is no single splice point — the walk must reconstruct each element. /// The migration is **total and default-filling**: no score context is needed, /// and every new field takes its canonical default. It is frozen by value — it /// depends only on the v0 field lists above and the current codec for unchanged /// fields; the `v0_score_migrates_*` golden tests guard it (they synthesize real /// v0 bytes via a mirror v0 encoder and check the migration reconstructs the /// original score with the new fields at their defaults). **Must** read /// `tuning_context` via [`dec_tuning_context_v2`], never via the live /// [`Codec`] — the live codec now reads/writes the 5-field schema-major-3 /// form, and reading v0/v1/v2 bytes through it would desynchronize the /// cursor for every field after `tuning_context`. fn decode_v0_score(bytes: &[u8]) -> Result { let mut r = Reader::new(bytes); // The 19 Score fields in declaration order (codec.rs `struct_codec!(Score)`). // `canvas` (2) and `instruments` (3) carry v0-specific sub-forms; the // schema-major-2 fills route `metadata`, `staves`, `cross_cutting`, and // (transitively, inside regions) the staff instances through the frozen // v1 sub-decoders — v0 == v1 for every type major 2 changed. `tuning_context` // routes through the frozen v2 sub-decoder — v0 == v1 == v2 for it, only // major 3 changes it. let metadata = dec_metadata_v1(&mut r)?; let canvas = dec_canvas_v0(&mut r)?; let instruments = dec_instruments_v0(&mut r)?; let staves = dec_staves_v1(&mut r)?; let staff_groups = Codec::dec(&mut r)?; let parts = Codec::dec(&mut r)?; let cross_cutting = dec_ccr_v1(&mut r)?; let time_signatures = Codec::dec(&mut r)?; let tuning_context = dec_tuning_context_v2(&mut r)?; let tempo_map = Codec::dec(&mut r)?; let events = Codec::dec(&mut r)?; let spelling_attachments = Codec::dec(&mut r)?; let decomposition_attachments = Codec::dec(&mut r)?; let spelling_precedence = Codec::dec(&mut r)?; let analysis_layers = Codec::dec(&mut r)?; let views = Codec::dec(&mut r)?; let identity = Codec::dec(&mut r)?; let tombstoned_pitches = Codec::dec(&mut r)?; let tombstoned_events = Codec::dec(&mut r)?; r.finish()?; let score = Score { metadata, canvas, instruments, staves, staff_groups, parts, cross_cutting, time_signatures, tuning_context, tempo_map, events, spelling_attachments, decomposition_attachments, spelling_precedence, analysis_layers, views, identity, tombstoned_pitches, tombstoned_events, }; // Strictly canonical on the **v0 wire form**, exactly as // `Score::decode_canonical` is for v1: the frozen field walk above decodes // leniently (the unchanged fields normalize on decode — an unreduced // RationalTime, an unsorted set, …), so re-encode the migrated score to the // frozen v0 form and reject any input that is not already its canonical v0 // encoding. This keeps the migration injective over v0 snapshots too (a // major-0 chunk is content-addressed just like a major-1 one). A valid v0 // writer never emitted a non-canonical snapshot. if encode_v0_score(&score) != bytes { return Err(ScoreDecodeError::InvalidValue( "non-canonical v0 Score encoding", )); } Ok(score) } /// The **frozen schema-major-0** encoding of a score — the byte-exact inverse of /// [`decode_v0_score`]'s field walk: `Canvas` without `layout_defaults`, /// `Instrument` without `range`, `Region` without `permits_spanning_slurs`, /// `tuning_context` through the frozen 3-field [`enc_tuning_context_v2`] (not /// the live [`Codec`], which now writes the 5-field schema-major-3 form); /// every other field through the current [`Codec`]. Used by `decode_v0_score` /// to enforce strict v0 canonicality (and by the migration tests to synthesize /// genuine v0 bytes). A migrated score's schema-major-1 fields hold their /// defaults, so this simply omits them. pub(crate) fn encode_v0_score(s: &Score) -> Vec { fn enc_region_v0(reg: &Region, out: &mut Vec) { reg.id.enc(out); reg.time_model.enc(out); // Content through the frozen v1 (== v0) sub-form: its staff // instances embed the pre-major-2 StaffLineConfiguration. enc_content_v1(®.content, out); reg.time_extent.enc(out); reg.staff_extent.enc(out); reg.local_tempo_map.enc(out); // v0: no permits_spanning_slurs. } let mut out = Vec::new(); enc_metadata_v1(&s.metadata, &mut out); // Canvas v0: `regions` only (no `layout_defaults`). put_len(&mut out, s.canvas.regions.len()); for reg in &s.canvas.regions { enc_region_v0(reg, &mut out); } // Instruments v0: `{ id, name }` only (no `range`). put_len(&mut out, s.instruments.len()); for inst in &s.instruments { inst.id.enc(&mut out); inst.name.enc(&mut out); } // Fields 4..19: the schema-major-2-changed ones go through the frozen // v1 (== v0) sub-encoders; the rest are unchanged since v0. enc_staves_v1(&s.staves, &mut out); s.staff_groups.enc(&mut out); s.parts.enc(&mut out); enc_ccr_v1(&s.cross_cutting, &mut out); s.time_signatures.enc(&mut out); enc_tuning_context_v2(&s.tuning_context, &mut out); s.tempo_map.enc(&mut out); s.events.enc(&mut out); s.spelling_attachments.enc(&mut out); s.decomposition_attachments.enc(&mut out); s.spelling_precedence.enc(&mut out); s.analysis_layers.enc(&mut out); s.views.enc(&mut out); s.identity.enc(&mut out); s.tombstoned_pitches.enc(&mut out); s.tombstoned_events.enc(&mut out); out } /// Frozen v0 decoder for `Canvas`: the v0 layout was **just `regions`** (a /// `Vec` in v0 element form), with no `layout_defaults`. Reads the /// region vector via [`dec_region_v0`] and default-fills the new field. fn dec_canvas_v0(r: &mut Reader<'_>) -> Result { let n = r.count()?; let mut regions = Vec::with_capacity(n.min(1024)); for _ in 0..n { regions.push(dec_region_v0(r)?); } Ok(Canvas { regions, layout_defaults: CanvasLayoutDefaults::default(), }) } /// Frozen v0 decoder for a `Region` element: the six fields before /// `permits_spanning_slurs`, which is default-filled `false`. (The `Vec` /// framing is the caller's; this reads one element.) fn dec_region_v0(r: &mut Reader<'_>) -> Result { let id = Codec::dec(r)?; let time_model = Codec::dec(r)?; let content = dec_content_v1(r)?; let time_extent = Codec::dec(r)?; let staff_extent = Codec::dec(r)?; let local_tempo_map = Codec::dec(r)?; Ok(Region { id, time_model, content, time_extent, staff_extent, local_tempo_map, permits_spanning_slurs: false, }) } /// Frozen v0 decoder for `Score.instruments`: v0 `Instrument` was `{ id, name }` /// (no `range`), which is default-filled `None`. Mirrors the `Vec` framing (a /// `u32` count then each element). fn dec_instruments_v0(r: &mut Reader<'_>) -> Result> { let n = r.count()?; let mut instruments = Vec::with_capacity(n.min(1024)); for _ in 0..n { let id = Codec::dec(r)?; let name = Codec::dec(r)?; instruments.push(Instrument { id, name, range: None, abbreviation: None, sound_config: SoundConfiguration::default(), transposition: None, default_clef: Clef::treble(), default_staff_lines: StaffLineConfiguration::default(), unpitched_members: Vec::new(), }); } Ok(instruments) } // =========================================================================== // Frozen schema-major-1 wire form (Binary Format §Schema Major 2). // =========================================================================== // // Schema major 2 filled nine type bodies (Slur/Tie/Beam/Spanner, // RepeatStructure, Staff, StaffLineConfiguration, Instrument, ScoreMetadata). // The v1 (= v0, for all of these — major 1 touched none of them) layouts are // frozen here as `enc_*_v1`/`dec_*_v1` sub-codecs, shared by // [`decode_v1_score`]/[`encode_v1_score`] and the v0 pair (whose walk routes // the transitively-changed fields through these). Each `dec_*_v1` // default-fills the appended v2 fields per the companion's total migration // table. The chain is transitive where an Option/Vec hides the embedding: // Region → RegionContent → StaffBasedContent → StaffInstance → // `staff_lines_override`. fn enc_vec_v1(items: &[T], out: &mut Vec, enc: impl Fn(&T, &mut Vec)) { put_len(out, items.len()); for item in items { enc(item, out); } } fn dec_vec_v1(r: &mut Reader<'_>, dec: impl Fn(&mut Reader<'_>) -> Result) -> Result> { let n = r.count()?; let mut items = Vec::with_capacity(n.min(1024)); for _ in 0..n { items.push(dec(r)?); } Ok(items) } fn enc_metadata_v1(m: &ScoreMetadata, out: &mut Vec) { m.title.enc(out); m.composer.enc(out); m.copyright.enc(out); } fn dec_metadata_v1(r: &mut Reader<'_>) -> Result { Ok(ScoreMetadata { title: Codec::dec(r)?, composer: Codec::dec(r)?, copyright: Codec::dec(r)?, ..Default::default() }) } fn enc_staff_lines_v1(c: &StaffLineConfiguration, out: &mut Vec) { c.line_count.enc(out); } fn dec_staff_lines_v1(r: &mut Reader<'_>) -> Result { Ok(StaffLineConfiguration { line_count: Codec::dec(r)?, ..Default::default() }) } fn enc_staff_v1(s: &Staff, out: &mut Vec) { s.id.enc(out); s.name.enc(out); s.abbreviation.enc(out); s.instrument.enc(out); enc_staff_lines_v1(&s.default_staff_lines, out); s.group.enc(out); } fn dec_staff_v1(r: &mut Reader<'_>) -> Result { Ok(Staff { id: Codec::dec(r)?, name: Codec::dec(r)?, abbreviation: Codec::dec(r)?, instrument: Codec::dec(r)?, default_staff_lines: dec_staff_lines_v1(r)?, group: Codec::dec(r)?, default_clef: Clef::treble(), }) } fn enc_staves_v1(staves: &[Staff], out: &mut Vec) { enc_vec_v1(staves, out, enc_staff_v1); } fn dec_staves_v1(r: &mut Reader<'_>) -> Result> { dec_vec_v1(r, dec_staff_v1) } fn enc_slur_v1(s: &Slur, out: &mut Vec) { s.id.enc(out); s.start_event.enc(out); s.end_event.enc(out); } fn dec_slur_v1(r: &mut Reader<'_>) -> Result { Ok(Slur { id: Codec::dec(r)?, start_event: Codec::dec(r)?, end_event: Codec::dec(r)?, kind: SlurKind::Legato, curvature_override: None, style: SpanStyle::default(), }) } fn enc_tie_v1(t: &Tie, out: &mut Vec) { t.id.enc(out); t.start_event.enc(out); t.end_event.enc(out); t.pitch_pairing.enc(out); t.class.enc(out); } fn dec_tie_v1(r: &mut Reader<'_>) -> Result { Ok(Tie { id: Codec::dec(r)?, start_event: Codec::dec(r)?, end_event: Codec::dec(r)?, pitch_pairing: Codec::dec(r)?, class: Codec::dec(r)?, style: SpanStyle::default(), }) } fn enc_beam_v1(b: &Beam, out: &mut Vec) { b.id.enc(out); b.events.enc(out); b.level.enc(out); } fn dec_beam_v1(r: &mut Reader<'_>) -> Result { Ok(Beam { id: Codec::dec(r)?, events: Codec::dec(r)?, level: Codec::dec(r)?, sub_beams: Vec::new(), geometry_override: None, }) } fn enc_spanner_v1(s: &Spanner, out: &mut Vec) { s.id.enc(out); s.start.enc(out); s.end.enc(out); s.staves.enc(out); } fn dec_spanner_v1(r: &mut Reader<'_>) -> Result { Ok(Spanner { id: Codec::dec(r)?, start: Codec::dec(r)?, end: Codec::dec(r)?, staves: Codec::dec(r)?, kind: SpannerKind::Generic, style: SpanStyle::default(), }) } fn enc_repeat_v1(rep: &RepeatStructure, out: &mut Vec) { rep.id.enc(out); rep.start.enc(out); rep.end.enc(out); } fn dec_repeat_v1(r: &mut Reader<'_>) -> Result { Ok(RepeatStructure { id: Codec::dec(r)?, start: Codec::dec(r)?, end: Codec::dec(r)?, kind: RepeatKind::migration_default(), voltas: Vec::new(), }) } fn enc_ccr_v1(c: &CrossCuttingRegistry, out: &mut Vec) { enc_vec_v1(&c.slurs, out, enc_slur_v1); enc_vec_v1(&c.ties, out, enc_tie_v1); enc_vec_v1(&c.beams, out, enc_beam_v1); c.tuplets.enc(out); enc_vec_v1(&c.spanners, out, enc_spanner_v1); c.markers.enc(out); enc_vec_v1(&c.repeats, out, enc_repeat_v1); c.analytical.enc(out); c.comments.enc(out); c.graphic_gestures.enc(out); c.lyrics.enc(out); c.chord_symbols.enc(out); } fn dec_ccr_v1(r: &mut Reader<'_>) -> Result { Ok(CrossCuttingRegistry { slurs: dec_vec_v1(r, dec_slur_v1)?, ties: dec_vec_v1(r, dec_tie_v1)?, beams: dec_vec_v1(r, dec_beam_v1)?, tuplets: Codec::dec(r)?, spanners: dec_vec_v1(r, dec_spanner_v1)?, markers: Codec::dec(r)?, repeats: dec_vec_v1(r, dec_repeat_v1)?, analytical: Codec::dec(r)?, comments: Codec::dec(r)?, graphic_gestures: Codec::dec(r)?, lyrics: Codec::dec(r)?, chord_symbols: Codec::dec(r)?, }) } fn enc_staff_instance_v1(si: &StaffInstance, out: &mut Vec) { si.id.enc(out); si.staff.enc(out); si.voices.enc(out); si.clef_sequence.enc(out); si.key_sequence.enc(out); si.local_metric_grid.enc(out); si.measures.enc(out); si.instrument_override.enc(out); match &si.staff_lines_override { None => out.push(0), Some(c) => { out.push(1); enc_staff_lines_v1(c, out); } } si.visible.enc(out); } fn dec_staff_instance_v1(r: &mut Reader<'_>) -> Result { Ok(StaffInstance { id: Codec::dec(r)?, staff: Codec::dec(r)?, voices: Codec::dec(r)?, clef_sequence: Codec::dec(r)?, key_sequence: Codec::dec(r)?, local_metric_grid: Codec::dec(r)?, measures: Codec::dec(r)?, instrument_override: Codec::dec(r)?, staff_lines_override: match r.u8()? { 0 => None, 1 => Some(dec_staff_lines_v1(r)?), tag => { return Err(ScoreDecodeError::InvalidTag { kind: "Option", tag, }) } }, visible: Codec::dec(r)?, }) } fn enc_sbc_v1(sbc: &StaffBasedContent, out: &mut Vec) { enc_vec_v1(&sbc.staff_instances, out, enc_staff_instance_v1); sbc.default_metric_grid.enc(out); sbc.barline_alignment_groups.enc(out); sbc.user_system_breaks.enc(out); sbc.user_page_breaks.enc(out); } fn dec_sbc_v1(r: &mut Reader<'_>) -> Result { Ok(StaffBasedContent { staff_instances: dec_vec_v1(r, dec_staff_instance_v1)?, default_metric_grid: Codec::dec(r)?, barline_alignment_groups: Codec::dec(r)?, user_system_breaks: Codec::dec(r)?, user_page_breaks: Codec::dec(r)?, }) } fn enc_content_v1(content: &RegionContent, out: &mut Vec) { match content { RegionContent::StaffBased(s) => { out.push(0); enc_sbc_v1(s, out); } RegionContent::FreeGraphic(g) => { out.push(1); g.enc(out); } RegionContent::Hybrid { staves, overlay, overlay_below_staves, } => { out.push(2); enc_sbc_v1(staves, out); overlay.enc(out); overlay_below_staves.enc(out); } } } fn dec_content_v1(r: &mut Reader<'_>) -> Result { match r.u8()? { 0 => Ok(RegionContent::StaffBased(dec_sbc_v1(r)?)), 1 => Ok(RegionContent::FreeGraphic(Codec::dec(r)?)), 2 => Ok(RegionContent::Hybrid { staves: dec_sbc_v1(r)?, overlay: Codec::dec(r)?, overlay_below_staves: Codec::dec(r)?, }), tag => Err(ScoreDecodeError::InvalidTag { kind: "RegionContent", tag, }), } } fn enc_region_v1(reg: &Region, out: &mut Vec) { reg.id.enc(out); reg.time_model.enc(out); enc_content_v1(®.content, out); reg.time_extent.enc(out); reg.staff_extent.enc(out); reg.local_tempo_map.enc(out); reg.permits_spanning_slurs.enc(out); } fn dec_region_v1(r: &mut Reader<'_>) -> Result { Ok(Region { id: Codec::dec(r)?, time_model: Codec::dec(r)?, content: dec_content_v1(r)?, time_extent: Codec::dec(r)?, staff_extent: Codec::dec(r)?, local_tempo_map: Codec::dec(r)?, permits_spanning_slurs: Codec::dec(r)?, }) } fn enc_canvas_v1(c: &Canvas, out: &mut Vec) { enc_vec_v1(&c.regions, out, enc_region_v1); c.layout_defaults.enc(out); } fn dec_canvas_v1(r: &mut Reader<'_>) -> Result { Ok(Canvas { regions: dec_vec_v1(r, dec_region_v1)?, layout_defaults: Codec::dec(r)?, }) } fn enc_instruments_v1(instruments: &[Instrument], out: &mut Vec) { put_len(out, instruments.len()); for inst in instruments { inst.id.enc(out); inst.name.enc(out); inst.range.enc(out); } } fn dec_instruments_v1(r: &mut Reader<'_>) -> Result> { let n = r.count()?; let mut instruments = Vec::with_capacity(n.min(1024)); for _ in 0..n { instruments.push(Instrument { id: Codec::dec(r)?, name: Codec::dec(r)?, range: Codec::dec(r)?, abbreviation: None, sound_config: SoundConfiguration::default(), transposition: None, default_clef: Clef::treble(), default_staff_lines: StaffLineConfiguration::default(), unpitched_members: Vec::new(), }); } Ok(instruments) } /// The **frozen schema-major-1** encoding of a score — the byte-exact inverse /// of [`decode_v1_score`]'s field walk (`tuning_context` through the frozen /// [`enc_tuning_context_v2`], not the live [`Codec`] — see /// [`encode_v0_score`]'s note). Used by `decode_v1_score` to enforce strict v1 /// canonicality, and by migration tests and the decode fuzzer to synthesize /// genuine v1 bytes. A migrated score's schema-major-2 fields hold their /// defaults, so this simply omits them. pub(crate) fn encode_v1_score(s: &Score) -> Vec { let mut out = Vec::new(); enc_metadata_v1(&s.metadata, &mut out); enc_canvas_v1(&s.canvas, &mut out); enc_instruments_v1(&s.instruments, &mut out); enc_staves_v1(&s.staves, &mut out); s.staff_groups.enc(&mut out); s.parts.enc(&mut out); enc_ccr_v1(&s.cross_cutting, &mut out); s.time_signatures.enc(&mut out); enc_tuning_context_v2(&s.tuning_context, &mut out); s.tempo_map.enc(&mut out); s.events.enc(&mut out); s.spelling_attachments.enc(&mut out); s.decomposition_attachments.enc(&mut out); s.spelling_precedence.enc(&mut out); s.analysis_layers.enc(&mut out); s.views.enc(&mut out); s.identity.enc(&mut out); s.tombstoned_pitches.enc(&mut out); s.tombstoned_events.enc(&mut out); out } /// Decodes **schema-major-1** `Score` bytes into the current-layout `Score`, /// migrating on read (total, default-filling — Binary Format §Schema /// Major 2's migration table). Strictly canonical on the v1 wire form, like /// its v0 sibling: re-encodes through [`encode_v1_score`] and rejects any /// input that is not already its canonical v1 encoding. fn decode_v1_score(bytes: &[u8]) -> Result { let mut r = Reader::new(bytes); let metadata = dec_metadata_v1(&mut r)?; let canvas = dec_canvas_v1(&mut r)?; let instruments = dec_instruments_v1(&mut r)?; let staves = dec_staves_v1(&mut r)?; let staff_groups = Codec::dec(&mut r)?; let parts = Codec::dec(&mut r)?; let cross_cutting = dec_ccr_v1(&mut r)?; let time_signatures = Codec::dec(&mut r)?; let tuning_context = dec_tuning_context_v2(&mut r)?; let tempo_map = Codec::dec(&mut r)?; let events = Codec::dec(&mut r)?; let spelling_attachments = Codec::dec(&mut r)?; let decomposition_attachments = Codec::dec(&mut r)?; let spelling_precedence = Codec::dec(&mut r)?; let analysis_layers = Codec::dec(&mut r)?; let views = Codec::dec(&mut r)?; let identity = Codec::dec(&mut r)?; let tombstoned_pitches = Codec::dec(&mut r)?; let tombstoned_events = Codec::dec(&mut r)?; r.finish()?; let score = Score { metadata, canvas, instruments, staves, staff_groups, parts, cross_cutting, time_signatures, tuning_context, tempo_map, events, spelling_attachments, decomposition_attachments, spelling_precedence, analysis_layers, views, identity, tombstoned_pitches, tombstoned_events, }; if encode_v1_score(&score) != bytes { return Err(ScoreDecodeError::InvalidValue( "non-canonical v1 Score encoding", )); } Ok(score) } /// The **frozen schema-major-2** encoding of [`ScoreTuningContext`] — the /// 3-field pre-major-3 form (`default_pitch_space`, `default_tuning_system`, /// `reference`) that majors 0, 1, and 2 all share (schema major 3, Push 4b /// tranche 3b-i, `spec/CONTRACT_PUSH4B_3BI_WIRE.md`, is the first bump to /// change this type — it appends `smufl` and `overrides`). Used by /// `encode_v0_score`, `encode_v1_score`, and [`encode_v2_score`] — every /// frozen pre-major-3 score form — so none of them silently drift onto the /// live (now 5-field) [`Codec`]. fn enc_tuning_context_v2(ctx: &ScoreTuningContext, out: &mut Vec) { ctx.default_pitch_space.enc(out); ctx.default_tuning_system.enc(out); ctx.reference.enc(out); } /// The exact inverse of [`enc_tuning_context_v2`]: reads the 3-field /// pre-major-3 form and default-fills the fields major 3 (and earlier /// tranches) added — `accidental_extensions: Vec::new()`, /// `smufl: SmuflVersionRequirement::default()`, `overrides: Vec::new()`. /// Used by `decode_v0_score`, `decode_v1_score`, and [`decode_v2_score`]. fn dec_tuning_context_v2(r: &mut Reader<'_>) -> Result { let default_pitch_space = Codec::dec(r)?; let default_tuning_system = Codec::dec(r)?; let reference = Codec::dec(r)?; Ok(ScoreTuningContext { default_pitch_space, default_tuning_system, reference, accidental_extensions: Vec::new(), smufl: SmuflVersionRequirement::default(), overrides: Vec::new(), }) } /// The **frozen schema-major-2** encoding of a score — the byte-exact inverse /// of [`decode_v2_score`]'s field walk. Schema major 3 (Push 4b tranche 3b-i) /// changes only `tuning_context` (Section "the frozen major-3 wire layout"); /// every other `Score` field is byte-identical to the live [`Codec`], so this /// walk uses the live codec directly for all 18 other fields and /// [`enc_tuning_context_v2`] for `tuning_context`. Used by [`decode_v2_score`] /// to enforce strict v2 canonicality, and by migration tests to synthesize /// genuine v2 bytes. pub(crate) fn encode_v2_score(s: &Score) -> Vec { let mut out = Vec::new(); s.metadata.enc(&mut out); s.canvas.enc(&mut out); s.instruments.enc(&mut out); s.staves.enc(&mut out); s.staff_groups.enc(&mut out); s.parts.enc(&mut out); s.cross_cutting.enc(&mut out); s.time_signatures.enc(&mut out); enc_tuning_context_v2(&s.tuning_context, &mut out); s.tempo_map.enc(&mut out); s.events.enc(&mut out); s.spelling_attachments.enc(&mut out); s.decomposition_attachments.enc(&mut out); s.spelling_precedence.enc(&mut out); s.analysis_layers.enc(&mut out); s.views.enc(&mut out); s.identity.enc(&mut out); s.tombstoned_pitches.enc(&mut out); s.tombstoned_events.enc(&mut out); out } /// Decodes **schema-major-2** `Score` bytes into the current-layout `Score`, /// migrating on read (total, default-filling — Binary Format §"Schema Major /// 3"'s migration table: `tuning_context` default-fills `smufl` and /// `overrides`; every other field is unchanged since major 2). /// Strictly canonical on the v2 wire form, like its v0/v1 siblings: re-encodes /// through [`encode_v2_score`] and rejects any input that is not already its /// canonical v2 encoding. fn decode_v2_score(bytes: &[u8]) -> Result { let mut r = Reader::new(bytes); let metadata = Codec::dec(&mut r)?; let canvas = Codec::dec(&mut r)?; let instruments = Codec::dec(&mut r)?; let staves = Codec::dec(&mut r)?; let staff_groups = Codec::dec(&mut r)?; let parts = Codec::dec(&mut r)?; let cross_cutting = Codec::dec(&mut r)?; let time_signatures = Codec::dec(&mut r)?; let tuning_context = dec_tuning_context_v2(&mut r)?; let tempo_map = Codec::dec(&mut r)?; let events = Codec::dec(&mut r)?; let spelling_attachments = Codec::dec(&mut r)?; let decomposition_attachments = Codec::dec(&mut r)?; let spelling_precedence = Codec::dec(&mut r)?; let analysis_layers = Codec::dec(&mut r)?; let views = Codec::dec(&mut r)?; let identity = Codec::dec(&mut r)?; let tombstoned_pitches = Codec::dec(&mut r)?; let tombstoned_events = Codec::dec(&mut r)?; r.finish()?; let score = Score { metadata, canvas, instruments, staves, staff_groups, parts, cross_cutting, time_signatures, tuning_context, tempo_map, events, spelling_attachments, decomposition_attachments, spelling_precedence, analysis_layers, views, identity, tombstoned_pitches, tombstoned_events, }; if encode_v2_score(&score) != bytes { return Err(ScoreDecodeError::InvalidValue( "non-canonical v2 Score encoding", )); } Ok(score) } // =========================================================================== // Public per-value canonical codec (the K↔J seam). // =========================================================================== /// A public, whole-buffer canonical byte form for an *individual* value /// reachable from a [`Score`] — the per-type analogue of /// [`Score::canonical_bytes`]. /// /// ## Why this exists /// /// The internal `Codec` trait (and its `Reader`) are crate-private: they are /// the composition machinery for the whole-score codec, and exposing them would /// leak the cursor and the combinator surface. But Track B's Operation Catalog /// (Agent K) needs *value-typed* operation payloads — an `InsertEvent` that /// carries the real [`Event`], a `RespellPitch` that carries the real /// [`PitchSpelling`] — and those payloads must serialize canonically so an /// operation envelope stays hashable across an implementation boundary. /// /// `CanonicalValue` is that agreed surface: a thin, public, per-type /// `canonical_bytes`/`decode_canonical` pair that **delegates to the existing, /// ratified `Codec` byte layout** (Pass 11 item 1.8, /// `req:format:codec-conventions`). It introduces *no new byte layout* — the /// bytes are byte-for-byte the same ones the whole-score codec already emits for /// these values, merely made reachable for an individual value. The Binary /// Format companion (Agent J) documents this surface as normative; until then it /// inherits core's conventions exactly, so core/ops stay consistent. See /// `DECISIONS.md` (P11-4 / the K↔J seam). /// /// Implemented for the value types operation payloads embed, and — since /// `spec/CONTRACT_CORE_DECODE_VECTORS.md` — for the value types the /// cross-implementation decode-vector corpus (`crate::vectors`) pins: the /// representative layouts of the Binary Format companion's /// §"Representative Complete Layouts" and the schema-major-3 tuning-context /// leaves. Not for every `Codec` type, to keep the public surface intentional; /// the intent has simply widened to a second reason. pub trait CanonicalValue: Sized { /// The canonical bytes of this single value, using the same layout the /// whole-score codec uses for it. fn canonical_bytes(&self) -> Vec; /// The exact inverse of [`CanonicalValue::canonical_bytes`]. Validates every /// tag, length, primitive, and invariant; rejects trailing bytes. fn decode_canonical(bytes: &[u8]) -> core::result::Result; } /// Implements [`CanonicalValue`] for value types that already have a [`Codec`], /// by delegating to it. No new byte layout is introduced. macro_rules! canonical_value { ($($ty:ty),* $(,)?) => { $( impl CanonicalValue for $ty { fn canonical_bytes(&self) -> Vec { let mut out = Vec::new(); Codec::enc(self, &mut out); out } fn decode_canonical(bytes: &[u8]) -> Result { let mut r = Reader::new(bytes); let v = <$ty as Codec>::dec(&mut r)?; r.finish()?; // Strictly canonical, exactly as `Score::decode_canonical`: // reject a non-canonical encoding so per-value decode is // injective (these bytes are content-addressed inside // operation payloads). if v.canonical_bytes() != bytes { return Err(ScoreDecodeError::InvalidValue( "non-canonical value encoding", )); } Ok(v) } } )* }; } canonical_value! { Event, Rest, Pitch, IdentifiedPitch, PitchSpelling, Tie, Slur, Beam, Spanner, RegionTimeModel, TimeAnchor, // Structural containers (M2c) — value-typed create payloads embed these. Region, StaffInstance, Voice, // Pre-pass annotation leaves — give DerivedAnnotations a canonical byte // fingerprint (a normative surface, vs. its Debug form). DecompositionAttachment, SpellingSourceKind, // Score settings (M2d) — value-typed set-* payloads embed these. ScoreMetadata, MetricGrid, // Phase-3 ops tranche — value-typed create/set payloads embed these. // `TimeSignature::dec` re-validates the beat-group sum through // `TimeSignature::new` (the "reject at construction *and* at decode" // discipline the operation catalog's §"Meter and Tempo Overwrites" // requires), so a malformed value never round-trips. Staff, TimeSignature, TempoSegment, StaffLineConfiguration, // Genesis tranche G1 (`CONTRACT_GENESIS_G1_INSTRUMENT.md`) — CreateInstrument // embeds the full value, mirroring CreateStaff's `Staff`. `Instrument` // already has a `Codec` (`struct_codec!`, `:1756`) and already ships inside // `Score`; this makes that existing layout reachable per-value, same as // every other entry here. Instrument, // Repeat authoring (schema-major-2 revision) — CreateRepeatStructure // embeds the full value. RepeatStructure, // Decode-vector corpus (`spec/CONTRACT_CORE_DECODE_VECTORS.md`): the fifth // representative layout (`RationalTime` — the other four, `Pitch`, // `TimeAnchor`, `Event`, `Slur`, are already above), plus the // schema-major-3 tuning-context container and its four leaf types, frozen // by Push 4b tranche 3b-i. None of these gain a byte layout here — every // one already has a `Codec` impl the whole-score codec uses; this macro // only makes that existing layout reachable per-value. RationalTime, ScoreTuningContext, TuningOverride, TuningScope, SmuflVersionRequirement, SmuflVersion, // Genesis tranche G2a (`CONTRACT_GENESIS_G2A_SETTINGS.md`) — // SetCanvasLayoutDefaults and SetSpellingPrecedence embed the full value, // mirroring SetMetadata's `ScoreMetadata`. Both already have a `Codec` // (`struct_codec!` for `CanvasLayoutDefaults` at `:2115`; the hand-written // `impl Codec for SpellingPrecedence` at `:1144`) and already ship inside // `Score`; this makes that existing layout reachable per-value, same as // every other entry here. Neither carries a schema-major-1+ layout of its // own — see the contract's "Why these two" table. CanvasLayoutDefaults, SpellingPrecedence, // Genesis tranche G2b (`CONTRACT_GENESIS_G2B_TUNING.md` §1) — // SetTuningContext embeds this authored subset, not the full // `ScoreTuningContext`. Its `Codec` above is byte-identical to // `ScoreTuningContext`'s five-field walk; this makes that layout // reachable per-value, exactly as `ScoreTuningContext` above is. TuningContextSettings, // Genesis tranche G3a (`CONTRACT_GENESIS_G3A_ENTITIES.md` pin 3) — the // four root-level mints (`CreateStaffGroup`, `CreatePartDefinition`, // `CreateAnalysisLayer`, `CreateView`) embed the full value, mirroring // `CreateStaff`'s `Staff` / `CreateInstrument`'s `Instrument`. All four // already have a `Codec` (`struct_codec!` at `:1790`–`:1796` and `:2329`) // and already ship inside `Score`; this makes those existing layouts // reachable per-value, same as every other entry here. No new byte // layout, and no `textvalue_graph.rs` work — `struct_codec!` already // generated each type's `TextValue` impl alongside its `Codec`. StaffGroup, PartDefinition, AnalysisLayer, ViewDefinition, // Genesis tranche G3b (`CONTRACT_GENESIS_G3B_MEASURE.md` pin 3) — // CreateMeasure embeds the full value, mirroring CreateStaffInstance's // `StaffInstance`. `Measure` already has a `Codec` (`struct_codec!` at // `:1825`) and already ships inside `Score`; this makes that existing // layout reachable per-value, same as every other entry here. No new wire // layout, and no `textvalue_graph.rs` work — `struct_codec!` already // generated `Measure`'s `TextValue` impl alongside its `Codec`. Measure, } #[cfg(test)] mod value_codec_tests { use super::*; use crate::generators::{valid_score, valid_score_rich}; /// Every `CanonicalValue` round-trips, and its per-value bytes are exactly /// the bytes the whole-score codec embeds for that value (no new layout). fn assert_value_round_trips(v: &T) where T: CanonicalValue + Codec + PartialEq + core::fmt::Debug, { let bytes = v.canonical_bytes(); // Per-value bytes equal what the internal Codec emits (the embedded form). let mut embedded = Vec::new(); Codec::enc(v, &mut embedded); assert_eq!(bytes, embedded, "CanonicalValue diverged from Codec layout"); let decoded = T::decode_canonical(&bytes).expect("value decodes"); assert_eq!(&decoded, v, "value round-trip changed the value"); assert_eq!( decoded.canonical_bytes(), bytes, "value re-encode not byte-identical" ); } #[test] fn value_types_round_trip_over_generator_corpus() { for seed in 0..64u64 { let s = valid_score_rich(seed.wrapping_mul(0x0100_0193).wrapping_add(7)); for region in &s.canvas.regions { assert_value_round_trips(®ion.time_model); assert_value_round_trips(region); for instance in region.staff_instances() { assert_value_round_trips(instance); for voice in &instance.voices { assert_value_round_trips(voice); } } } for tie in &s.cross_cutting.ties { assert_value_round_trips(tie); } for slur in &s.cross_cutting.slurs { assert_value_round_trips(slur); } for beam in &s.cross_cutting.beams { assert_value_round_trips(beam); } for spanner in &s.cross_cutting.spanners { assert_value_round_trips(spanner); } } for seed in 0..200u64 { let s = valid_score(seed.wrapping_mul(0x9E37_79B9).wrapping_add(1)); for ev in s.events.iter() { assert_value_round_trips(ev); let mut ips = Vec::new(); ev.collect_identified_pitches(&mut ips); for ip in ips { assert_value_round_trips(ip); assert_value_round_trips(&ip.pitch); } } } } } #[cfg(test)] mod tests { use super::*; use crate::event::{IndeterminateEvent, Rest, UnpitchedEvent}; use crate::generators::{valid_score, valid_score_rich}; use crate::ids::EventId; use crate::time::{EventDuration, EventPosition}; /// encode → decode → equal, and re-encode byte-identical. fn assert_round_trips(score: &Score) { let bytes = score.canonical_bytes(); let decoded = Score::decode_canonical(&bytes).expect("score decodes"); assert_eq!(&decoded, score, "round-trip changed the score"); assert_eq!( decoded.canonical_bytes(), bytes, "re-encode not byte-identical" ); } #[test] fn clef_and_key_signature_codecs_round_trip() { use crate::graph::{Clef, ClefChange, ClefShape, KeySignature, KeySignatureChange}; use crate::time::{TimeAnchor, WallClockTime}; fn rt(v: &T) { let mut out = Vec::new(); v.enc(&mut out); let mut r = Reader::new(&out); let decoded = T::dec(&mut r).expect("decodes"); assert_eq!(&decoded, v, "round-trip changed the value"); } for shape in [ ClefShape::G, ClefShape::F, ClefShape::C, ClefShape::Percussion, ] { rt(&shape); for line in [-3i8, 1, 2, 3, 4, 5] { for octave_shift in [-1i8, 0, 1] { rt(&Clef { shape, line, octave_shift, }); } } } for shape_clef in [Clef::treble(), Clef::bass(), Clef::alto(), Clef::tenor()] { rt(&shape_clef); } for fifths in -7i8..=7 { rt(&KeySignature::new(fifths).expect("fifths is in range")); } let anchor = TimeAnchor::WallClock { time: WallClockTime(0), }; rt(&ClefChange { anchor: anchor.clone(), clef: Clef::bass(), }); rt(&KeySignatureChange { anchor, key: KeySignature::new(-3).expect("fifths is in range"), }); } #[test] fn key_signature_rejects_out_of_range_fifths_on_decode() { let decode = |fifths: i8| { let mut bytes = Vec::new(); fifths.enc(&mut bytes); KeySignature::dec(&mut Reader::new(&bytes)) }; assert!(decode(-7).is_ok()); assert!(decode(7).is_ok()); for fifths in [-8i8, 8] { assert!( matches!(decode(fifths), Err(ScoreDecodeError::Reconstruct(_))), "out-of-range fifth count {fifths} must be rejected" ); } } #[test] fn score_tuning_context_smufl_and_overrides_reach_the_wire_accidental_extensions_do_not() { // Schema major 3 (Push 4b tranche 3b-i, `spec/CONTRACT_PUSH4B_3BI_WIRE.md`) // moves `smufl` and `overrides` onto the wire — the direct // **inversion** of the pre-major-3 proof this test replaces (formerly // two tests: `score_tuning_context_overrides_do_not_reach_the_wire`, // Push 4b tranche 2; and // `score_tuning_context_accidental_extensions_smufl_and_overrides_do_not_reach_the_wire`, // tranche 3a). `accidental_extensions` stays in memory only (staged to // a later major, appended after `overrides` when it lands), so this // is now a **staging-boundary** test: it proves the line falls // exactly between `overrides` and `accidental_extensions`, not that // nothing reaches the wire. use crate::accidental::{PitchSpaceModification, SmuflVersion, SmuflVersionRequirement}; use crate::graph::ScoreTuningContext; use crate::ids::{ReplicaId, VoiceId}; use crate::pitch::TuningSystemId; use crate::tuning::{TuningOverride, TuningScope}; let bare = ScoreTuningContext::default(); let mut loaded = ScoreTuningContext::default(); loaded .accidental_extensions .push(crate::accidental::fixture_extensions( "heji", PitchSpaceModification::CmnChromatic(1), )); loaded.smufl = SmuflVersionRequirement { minimum: SmuflVersion::from_decimal(1, "12").unwrap(), authored_against: SmuflVersion::from_decimal(1, "18").unwrap(), }; loaded.overrides.push(TuningOverride { scope: TuningScope::Voice(VoiceId::new(ReplicaId(1), 1)), pitch_space: None, tuning_system: Some(TuningSystemId::new("tet-19")), reference: None, }); // Sanity: the fixture actually differs in memory in all three // fields, so neither assertion below is vacuous. assert_ne!(loaded, bare); assert!(!loaded.accidental_extensions.is_empty()); assert_ne!(loaded.smufl, SmuflVersionRequirement::default()); assert!(!loaded.overrides.is_empty()); let mut bytes_loaded = Vec::new(); loaded.enc(&mut bytes_loaded); let mut bytes_bare = Vec::new(); bare.enc(&mut bytes_bare); // `smufl` and `overrides` differ in memory, so — unlike before major // 3 — the two encodings must now differ too. assert_ne!( bytes_loaded, bytes_bare, "smufl and overrides must reach canonical bytes as of schema major 3" ); let decoded = ScoreTuningContext::dec(&mut Reader::new(&bytes_loaded)).expect("decodes"); // smufl and overrides round-trip exactly... assert_eq!(decoded.smufl, loaded.smufl); assert_eq!(decoded.overrides, loaded.overrides); // ...but accidental_extensions is still dropped: never on the wire. assert!(decoded.accidental_extensions.is_empty()); assert_eq!( decoded, ScoreTuningContext { accidental_extensions: Vec::new(), ..loaded.clone() } ); } /// Pins the **exact frozen bytes** of the schema-major-3 /// `ScoreTuningContext` wire form (Push 4b tranche 3b-i). /// /// Why a byte literal and not just a round-trip: the cross-implementation /// decode corpus (`spec/vectors/decode_vectors.txt`, /// `req:binfmt:decode-vectors`) covers only the **operation** and /// **bundle** surfaces — there is no `epiphany-core` vectors module, so /// the core score wire has no vector pinning it. Every other test here /// round-trips `enc` against `dec`, which stays green under a /// *self-consistent* reordering of the fields: swapping `smufl` and /// `overrides` in both halves passes the entire workspace suite. That /// would silently move a layout `req:binfmt:frozen-layout` freezes /// permanently, and a second implementation would disagree. This golden /// is what makes such a swap fail. /// /// If this test fails, the wire format changed. That is a schema-major /// event — do not "fix" it by re-copying the bytes. #[test] fn schema_major_3_tuning_context_wire_bytes_are_frozen() { use crate::accidental::{PitchSpaceModification, SmuflVersion, SmuflVersionRequirement}; use crate::graph::ScoreTuningContext; use crate::ids::{ReplicaId, VoiceId}; use crate::pitch::TuningSystemId; use crate::tuning::{TuningOverride, TuningScope}; fn hex(bytes: &[u8]) -> String { bytes.iter().map(|b| format!("{b:02x}")).collect() } // --- the default context: what every score with untouched tuning // encodes to, so this literal is the one that appears in practice. // // 06000000 636d6e2d3132 default_pitch_space = "cmn-12" // 06000000 7465742d3132 default_tuning_system = "tet-12" // 00050004080000000000000000807b40 reference (frozen since major 0) // 01002800 smufl.minimum = {major 1, minor_centi 40} = 1.4 // 01002800 smufl.authored_against = 1.4 // 00000000 overrides = u32 count 0 // // Note the S12 normalization is visible on the wire: 1.4 stores // `minor_centi` 40 (0x28), NOT a literal minor 4 — that is what makes // 1.12 < 1.3 order correctly. let mut bare = Vec::new(); ScoreTuningContext::default().enc(&mut bare); assert_eq!( hex(&bare), "06000000636d6e2d3132060000007465742d3132000500040800000000000000 0080 7b40010028000100280000000000" .replace(' ', ""), "the default major-3 tuning-context wire form moved" ); assert_eq!(bare.len(), 48); // --- a fully loaded context: non-default smufl, one override, and a // populated `accidental_extensions` that MUST contribute zero bytes // (staged to a later major). // // ...prefix and reference as above... // 01000c00 smufl.minimum = {1, 12} = 1.12 // 01001200 smufl.authored_against = {1, 18} = 1.18 // 01000000 overrides = u32 count 1 // <34 bytes> the TuningOverride body: TuningScope::Voice // (discriminant 0) + VoiceId, then the three Options // (pitch_space None, tuning_system Some("tet-19"), // reference None). let mut loaded = ScoreTuningContext { smufl: SmuflVersionRequirement { minimum: SmuflVersion::from_decimal(1, "12").unwrap(), authored_against: SmuflVersion::from_decimal(1, "18").unwrap(), }, ..ScoreTuningContext::default() }; loaded.overrides.push(TuningOverride { scope: TuningScope::Voice(VoiceId::new(ReplicaId(1), 7)), pitch_space: None, tuning_system: Some(TuningSystemId::new("tet-19")), reference: None, }); loaded .accidental_extensions .push(crate::accidental::fixture_extensions( "heji", PitchSpaceModification::CmnChromatic(1), )); assert!(!loaded.accidental_extensions.is_empty()); let mut got = Vec::new(); loaded.enc(&mut got); assert_eq!( hex(&got), "06000000636d6e2d3132060000007465742d31320005000408000000000000000080 7b4001000c000100120001000000001000000000000000000000010000000000000007 0001060000007465742d313900" .replace(' ', ""), "the loaded major-3 tuning-context wire form moved" ); assert_eq!(got.len(), 82); // The golden is bidirectional: the frozen bytes decode back to the // value, with `accidental_extensions` dropped (it never encoded). let back = ScoreTuningContext::dec(&mut Reader::new(&got)).expect("golden decodes"); assert_eq!(back.smufl, loaded.smufl); assert_eq!(back.overrides, loaded.overrides); assert!(back.accidental_extensions.is_empty()); } /// Genesis tranche G3b (`CONTRACT_GENESIS_G3B_MEASURE.md` pin 4, M12): /// pins the exact frozen bytes of `Measure`'s `struct_codec!` wire form /// (`id, start, time_signature, explicit_number, number_visibility`). /// /// The five field values are chosen so their encodings are MUTUALLY /// DISTINCT in length (contract §3's `valuegen` trap, from G3a's /// `analysis_layer` mistake where a same-width field pair made a /// field-swap byte-invisible): `id` is a 20-byte id-leaf, `start` (a /// `WallClock` anchor) is 13 bytes, `time_signature` (`Some`) is a /// 21-byte id-leaf, `explicit_number` (`Some`) is a bare 5-byte `u32`, /// and `number_visibility` is a 1-byte C-style enum tag. /// /// **Mutation:** reorder `struct_codec!(Measure { ... })`'s field list in /// this file; must fail — a `struct_codec!` field reorder changes encode /// and decode symmetrically and stays green under a round-trip-only /// assertion, which is why this pins literal bytes instead. #[test] fn genesis_g3b_measure_wire_bytes_are_frozen() { use crate::graph::{Measure, MeasureNumberVisibility}; use crate::ids::{MeasureId, ReplicaId, TimeSignatureId}; use crate::time::{TimeAnchor, WallClockTime}; fn hex(bytes: &[u8]) -> String { bytes.iter().map(|b| format!("{b:02x}")).collect() } let measure = Measure { id: MeasureId::new(ReplicaId(1), 2), start: TimeAnchor::WallClock { time: WallClockTime(3), }, time_signature: Some(TimeSignatureId::new(ReplicaId(1), 4)), explicit_number: Some(5), number_visibility: MeasureNumberVisibility::Always, }; let mut bytes = Vec::new(); measure.enc(&mut bytes); let expected = "100000000000000000000001000000000000000203080000000300000000000000011000000000000000000000010000000000000004010500000001"; assert_eq!(hex(&bytes), expected, "the Measure wire form moved"); assert!(!bytes.is_empty()); let decoded = Measure::dec(&mut Reader::new(&bytes)).expect("golden decodes"); assert_eq!(decoded, measure); } /// Genesis tranche G3b (M14): the strict per-value re-encode comparison /// in `canonical_value!` is the ONLY thing standing between an /// unreduced `RationalTime` spelling and acceptance. /// /// `Measure.start` carries an `Event`-anchored `Musical` offset whose /// `RationalTime` is spelled UNREDUCED, `2/4`. `RationalTime::dec` ends in /// `BigRational::new`, which reduces on construction, so the decoded /// value is `1/2`; re-encoding it then emits `1/2`'s magnitudes, which /// differ from the fed-in `2/4` bytes. A structurally-decodable but /// NONCANONICAL encoding — exactly what the strict per-value check /// (decode → `finish()` → re-encode → reject on mismatch) exists to /// reject. /// /// **Mutation:** in `canonical_value!`, remove the /// `v.canonical_bytes() != bytes` check (return `Ok(v)` unconditionally); /// must fail. NOT "remove `Measure` from `canonical_value!`" — that does /// not compile: the ops-crate envelope decode arm calls `value::`, /// bound by `T: CanonicalValue`. #[test] fn genesis_g3b_measure_start_rejects_an_unreduced_rational_time() { use crate::graph::{Measure, MeasureNumberVisibility}; use crate::ids::{EventId, MeasureId, ReplicaId}; use crate::time::{AnchorOffset, MusicalDuration, RationalTime, TimeAnchor}; // The canonical (reduced) 1/2 value this rung's fixture decodes to. let reduced = Measure { id: MeasureId::new(ReplicaId(1), 2), start: TimeAnchor::Event { id: EventId::new(ReplicaId(1), 3), offset: AnchorOffset::Musical(MusicalDuration( RationalTime::new(1, 2).expect("1/2 is representable"), )), }, time_signature: None, explicit_number: None, number_visibility: MeasureNumberVisibility::Auto, }; let mut good_bytes = Vec::new(); reduced.enc(&mut good_bytes); // The RationalTime wire pattern for 1/2 (Plus sign, 1-byte numerator // magnitude `01`, 1-byte denominator magnitude `02`) and its // UNREDUCED 2/4 counterpart — same byte length (11), so substituting // one for the other does not disturb any surrounding length prefix. let pattern_1_2: [u8; 11] = [1, 1, 0, 0, 0, 0x01, 1, 0, 0, 0, 0x02]; let pattern_2_4: [u8; 11] = [1, 1, 0, 0, 0, 0x02, 1, 0, 0, 0, 0x04]; let occurrences = good_bytes .windows(pattern_1_2.len()) .filter(|w| *w == pattern_1_2) .count(); assert_eq!( occurrences, 1, "the 1/2 RationalTime pattern must appear exactly once, to substitute unambiguously" ); let at = good_bytes .windows(pattern_1_2.len()) .position(|w| w == pattern_1_2) .unwrap(); let mut bad_bytes = good_bytes.clone(); bad_bytes[at..at + pattern_2_4.len()].copy_from_slice(&pattern_2_4); assert_eq!(bad_bytes.len(), good_bytes.len()); // The reduced (canonical) bytes round-trip. assert_eq!( Measure::decode_canonical(&good_bytes).as_ref(), Ok(&reduced) ); // The unreduced (2/4) bytes are structurally decodable (RationalTime // itself never rejects an unreduced input — it normalizes), but the // outer strict re-encode comparison MUST reject them. assert!( Measure::decode_canonical(&bad_bytes).is_err(), "an unreduced RationalTime spelling must be rejected by the strict re-encode check" ); } /// Genesis tranche G2b (`CONTRACT_GENESIS_G2B_TUNING.md` §1, pin/touch-row /// 3): `TuningContextSettings`'s canonical encoding is **byte-identical** /// to `ScoreTuningContext`'s existing five-field walk — a type-level /// narrowing, not a new wire form. Proven directly against non-default /// values in all five fields, not just the default. /// /// **Mutation:** reorder two fields in `TuningContextSettings::enc` (e.g. /// swap `smufl` and `overrides`) without making the matching change in /// `ScoreTuningContext::enc`; must fail. #[test] fn tuning_context_settings_canonical_bytes_match_score_tuning_context() { use crate::accidental::{SmuflVersion, SmuflVersionRequirement}; use crate::graph::{ScoreTuningContext, TuningContextSettings}; use crate::ids::{ReplicaId, VoiceId}; use crate::pitch::TuningSystemId; use crate::tuning::{TuningOverride, TuningScope}; let full = ScoreTuningContext { smufl: SmuflVersionRequirement { minimum: SmuflVersion::from_decimal(1, "12").unwrap(), authored_against: SmuflVersion::from_decimal(1, "18").unwrap(), }, overrides: vec![TuningOverride { scope: TuningScope::Voice(VoiceId::new(ReplicaId(1), 7)), pitch_space: None, tuning_system: Some(TuningSystemId::new("tet-19")), reference: None, }], ..ScoreTuningContext::default() }; let settings = TuningContextSettings { default_pitch_space: full.default_pitch_space.clone(), default_tuning_system: full.default_tuning_system.clone(), reference: full.reference.clone(), smufl: full.smufl, overrides: full.overrides.clone(), }; let mut full_bytes = Vec::new(); full.enc(&mut full_bytes); let mut settings_bytes = Vec::new(); settings.enc(&mut settings_bytes); assert_eq!( settings_bytes, full_bytes, "TuningContextSettings must encode byte-identically to ScoreTuningContext's five-field walk" ); let decoded = TuningContextSettings::dec(&mut Reader::new(&settings_bytes)).expect("decodes"); assert_eq!(decoded.default_pitch_space, settings.default_pitch_space); assert_eq!( decoded.default_tuning_system, settings.default_tuning_system ); assert_eq!(decoded.reference, settings.reference); assert_eq!(decoded.smufl, settings.smufl); assert_eq!(decoded.overrides, settings.overrides); } #[test] fn generator_scores_round_trip() { for seed in 0..200u64 { assert_round_trips(&valid_score(seed.wrapping_mul(0x9E37_79B9).wrapping_add(1))); } for seed in 0..64u64 { assert_round_trips(&valid_score_rich( seed.wrapping_mul(0x0100_0193).wrapping_add(7), )); } } /// A C-in-`cmn-12` pitch at the given octave, for a non-default /// [`PitchRange`] (the generators never populate `Instrument.range`). fn cmn_c(octave: i8) -> Pitch { Pitch { scale_position: ScalePosition { space: PitchSpaceId::new("cmn-12"), position: PitchSpacePosition::Cmn { nominal: CmnNominal::C, alteration: 0, octave, }, }, acoustic: AcousticPitch { tuning: crate::pitch::TuningReference::Inherit, realization: AcousticRealization::Implicit, }, } } #[test] fn v1_round_trips_non_default_values_for_every_new_field() { // The three schema-major-1 fields must survive a v1 round-trip at // *non-default* values — not just vacuously when they equal the default // (which is all the generators produce). This is what makes them real // wire content rather than always-default padding. let mut score = valid_score(5); assert!(!score.instruments.is_empty() && !score.canvas.regions.is_empty()); let ss = |v: f64| CanonicalF64::new(v).expect("finite"); let custom = CanvasLayoutDefaults { // US Letter (216 × 279.4 mm) in staff spaces, distinct margins. page_size: CanvasSize { width: ss(216.0), height: ss(279.4), }, margins: CanvasMargins { top: ss(9.0), right: ss(10.0), bottom: ss(11.0), left: ss(12.0), }, }; assert_ne!(custom, CanvasLayoutDefaults::default()); score.canvas.layout_defaults = custom; score.canvas.regions[0].permits_spanning_slurs = true; score.instruments[0].range = Some(PitchRange { lowest: cmn_c(2), highest: cmn_c(6), }); let bytes = score.canonical_bytes(); assert_eq!(Score::decode_canonical(&bytes).unwrap(), score); assert_eq!(Score::decode_canonical_versioned(&bytes, 3).unwrap(), score); // The non-default major-1 values are representable at the frozen v1 // wire form too: the v1 encoding migrates back to the same score // (its major-2 and major-3 fields are all defaults here). let v1 = encode_v1_score(&score); assert_eq!(Score::decode_canonical_versioned(&v1, 1).unwrap(), score); } #[test] fn v0_score_migrates_default_filling_all_three_new_fields() { // Schema major 1 grew three fields — Canvas.layout_defaults, // Instrument.range, Region.permits_spanning_slurs — so a major-0 score // has none of them, and migrate-on-read must reconstruct each with its // canonical default. We synthesize *genuine* v0 bytes via a mirror v0 // encoder (the byte-exact inverse of decode_v0_score) and check the // versioned decoder rebuilds the original score, whose three fields ARE // the defaults. let ld_len = { let mut b = Vec::new(); CanvasLayoutDefaults::default().enc(&mut b); b.len() }; for seed in 0..64u64 { let score = valid_score(seed.wrapping_mul(0x9E37_79B9).wrapping_add(1)); // Precondition: the generator produces the v0-equivalent defaults, so // the original IS what a v0→v1 migration should reconstruct. assert_eq!( score.canvas.layout_defaults, CanvasLayoutDefaults::default() ); assert!(score.instruments.iter().all(|i| i.range.is_none())); assert!(score .canvas .regions .iter() .all(|r| !r.permits_spanning_slurs)); let current = score.canonical_bytes(); let v1 = encode_v1_score(&score); let v0 = encode_v0_score(&score); // Size anchor (independent of the v0 encoder's field order): v0 is // exactly v1 minus the appended default bytes — layout_defaults once, // plus one byte each for every instrument's range=None (Option tag) // and every region's permits_spanning_slurs=false (bool). let expected_removed = ld_len + score.instruments.len() + score.canvas.regions.len(); assert_eq!( v1.len() - v0.len(), expected_removed, "v0 omits exactly the three new fields' default bytes" ); // Major 0: the shorter v0 bytes migrate up, default-filling all three // (and, composed, every schema-major-2 default, and — composed // further — the schema-major-3 `smufl`/`overrides` defaults). let migrated = Score::decode_canonical_versioned(&v0, 0).unwrap(); assert_eq!(migrated, score); // The migrated score re-encodes to the production (major-3) bytes. assert_eq!(migrated.canonical_bytes(), current); // Major 1: the frozen v1 bytes migrate to the same score. assert_eq!(Score::decode_canonical_versioned(&v1, 1).unwrap(), score); // Major 3: the current bytes decode unchanged. assert_eq!( Score::decode_canonical_versioned(¤t, 3).unwrap(), score ); } // A major outside {0, 1, 2, 3} is a defensive decode error (the gate // rejects it upstream in practice). assert!(Score::decode_canonical_versioned(&valid_score(1).canonical_bytes(), 4).is_err()); } #[test] fn v1_score_migrates_default_filling_the_major_2_fields() { // Schema major 2 filled nine type bodies. A major-1 score carries none // of the appended fields; migrate-on-read reconstructs each at its // canonical default (Binary Format §Schema Major 2 migration table). // Genuine v1 bytes come from the mirror v1 encoder; the size anchor // pins that v1 omits exactly the appended default bytes, so the frozen // encoder cannot silently drift from the pre-major-2 wire form. for seed in 0..64u64 { let score = valid_score(seed.wrapping_mul(0x9E37_79B9).wrapping_add(3)); let current = score.canonical_bytes(); let v1 = encode_v1_score(&score); let c = &score.cross_cutting; let override_sites: usize = score .canvas .regions .iter() .flat_map(|r| r.content.staff_instances()) .filter(|si| si.staff_lines_override.is_some()) .count(); // Appended default bytes per component: slur 4 (kind 1 + // curvature presence 1 + style line 1 + thickness presence 1); // tie 2 (style); beam 5 (sub_beams count 4 + geometry presence 1); // spanner 3 (kind tag 1 + style 2); repeat 9 (SimpleRepeat tag 1 + // count 4 + voltas count 4); staff 17 (clef 3 + config appends // 14 = spacing 12 + style 1 + bracket 1); instrument 28 // (abbreviation 1 + sound count 4 + transposition 1 + clef 3 + // full config 15 + members count 4); an overridden staff-instance // config 14; metadata 23 (three presence bytes + two i64 // timestamps + additional count 4); plus a flat 12 // (schema-major-3 `tuning_context.smufl`/`overrides`: `smufl` is // two bare `SmuflVersion`s = 4 x u16 = 8, `overrides` is an empty // Vec = a bare u32 count = 4 — present at every score regardless // of content, since `v1` is frozen at the pre-major-3 3-field // `tuning_context` while `current` is now major 3). let expected_removed = c.slurs.len() * 4 + c.ties.len() * 2 + c.beams.len() * 5 + c.spanners.len() * 3 + c.repeats.len() * 9 + score.staves.len() * 17 + score.instruments.len() * 28 + override_sites * 14 + 23 + 12; assert_eq!( current.len() - v1.len(), expected_removed, "v1 omits exactly the appended major-2 default bytes (plus the flat \ major-3 tuning-context default bytes, present regardless of content)" ); let migrated = Score::decode_canonical_versioned(&v1, 1).unwrap(); assert_eq!(migrated, score); assert_eq!(migrated.canonical_bytes(), current); } } #[test] fn v2_score_migrates_default_filling_smufl_and_overrides() { // Schema major 3 (Push 4b tranche 3b-i, `spec/CONTRACT_PUSH4B_3BI_WIRE.md`) // appends `tuning_context.smufl` and `.overrides`; a major-2 score // carries neither, so migrate-on-read must reconstruct both at their // canonical defaults. `accidental_extensions` is unaffected — it was // never on the wire, at any major, so there is nothing for a v2->v3 // migration to default-fill for it. Genuine v2 bytes come from the // mirror v2 encoder; the size anchor pins that v2 omits exactly the // flat 12 default bytes (`smufl` 8 + `overrides` count 4), the same // constant every score pays regardless of its other content, since // the generator leaves `tuning_context` at its default. for seed in 0..64u64 { let score = valid_score(seed.wrapping_mul(0x9E37_79B9).wrapping_add(5)); // Precondition: the generator leaves tuning_context's smufl/overrides // at their defaults, so a v2->v3 migration reconstructing the // default IS reconstructing the original. assert_eq!( score.tuning_context.smufl, crate::accidental::SmuflVersionRequirement::default() ); assert!(score.tuning_context.overrides.is_empty()); let current = score.canonical_bytes(); let v2 = encode_v2_score(&score); assert_eq!( current.len() - v2.len(), 12, "v2 omits exactly the flat smufl(8)+overrides-count(4) default bytes" ); let migrated = Score::decode_canonical_versioned(&v2, 2).unwrap(); assert_eq!(migrated, score); assert_eq!(migrated.canonical_bytes(), current); } } #[test] fn current_major_round_trips_non_default_values_for_every_major_2_field() { // One score carrying a non-default value for EVERY schema-major-2 // field (and every payload-carrying SpannerKind variant), so the live // codec arms are exercised as real wire content, not always-default // padding. Byte round-trip only — the standalone cross-cutting values // reference fixture events for hygiene but invariants are not the // subject here. use crate::ids::{BeamId, RepeatStructureId, SlurId, SpannerId, TieId}; let mut score = valid_score(11); let events: Vec = score .voices() .flat_map(|(_, _, v)| v.events.clone()) .collect(); assert!(events.len() >= 2); let ss = |v: f64| SpaceUnit(CanonicalF64::new(v).expect("finite")); let anchor = |n: i64| crate::time::TimeAnchor::WallClock { time: crate::time::WallClockTime(n), }; score.cross_cutting.slurs.push(Slur { id: SlurId::new(ReplicaId(9), 1), start_event: events[0], end_event: events[1], kind: SlurKind::Phrase, curvature_override: Some(CurvatureOverride { direction: Some(CurveDirection::Below), height: Some(ss(2.5)), }), style: SpanStyle { line: LineStyle::Dashed, thickness: Some(ss(0.25)), }, }); score.cross_cutting.ties.push(Tie { id: TieId::new(ReplicaId(9), 2), start_event: events[0], end_event: events[1], pitch_pairing: None, class: TieClass::Editorial, style: SpanStyle { line: LineStyle::Dotted, thickness: None, }, }); score.cross_cutting.beams.push(Beam { id: BeamId::new(ReplicaId(9), 3), events: events.clone(), level: 1, sub_beams: vec![SubBeam { level: 2, events: vec![events[0]], }], geometry_override: Some(BeamGeometryOverride { slope: Some(CanonicalF64::new(0.5).expect("finite")), offset: Some(ss(-1.0)), }), }); for (n, kind) in [ SpannerKind::Hairpin(HairpinDirection::Crescendo), SpannerKind::OctaveLine(OctaveOffset(-1)), SpannerKind::PedalLine(PedalKind::Sostenuto), SpannerKind::TextLine(TextLineDefinition { text: String::from("rit."), }), SpannerKind::Bracket(BracketKind::Square), SpannerKind::TrillExtension, ] .into_iter() .enumerate() { score.cross_cutting.spanners.push(Spanner { id: SpannerId::new(ReplicaId(9), 10 + n as u64), start: anchor(0), end: anchor(1000), staves: score.staves.iter().map(|st| st.id).collect(), kind, style: SpanStyle { line: LineStyle::Dashed, thickness: Some(ss(0.1)), }, }); } score.cross_cutting.repeats.push(RepeatStructure { id: RepeatStructureId::new(ReplicaId(9), 20), start: anchor(0), end: anchor(500), kind: RepeatKind::DalSegno { segno: anchor(10), end_target: anchor(400), }, voltas: vec![Volta { endings: vec![1, 2], start: anchor(300), end: anchor(500), }], }); // Every RepeatKind wire arm as real content: DaCapo (tag 1) and the // payload-less Volta (tag 3); SimpleRepeat is the migration default // exercised everywhere else. score.cross_cutting.repeats.push(RepeatStructure { id: RepeatStructureId::new(ReplicaId(9), 21), start: anchor(500), end: anchor(700), kind: RepeatKind::DaCapo { end_target: anchor(600), }, voltas: vec![], }); score.cross_cutting.repeats.push(RepeatStructure { id: RepeatStructureId::new(ReplicaId(9), 22), start: anchor(700), end: anchor(900), kind: RepeatKind::Volta, voltas: vec![Volta { endings: vec![3], start: anchor(800), end: anchor(900), }], }); score.staves[0].default_clef = Clef::bass(); score.staves[0].default_staff_lines = StaffLineConfiguration { line_count: 1, line_spacing: ss(0.6), line_style: LineStyle::Dotted, bracket: Some(StaffBracketKind::Brace), }; let si = score.canvas.regions[0] .content .staff_instances_mut() .expect("staff-based region") .first_mut() .expect("an instance"); si.staff_lines_override = Some(StaffLineConfiguration { line_count: 4, line_spacing: ss(0.8), line_style: LineStyle::Dashed, bracket: Some(StaffBracketKind::Bracket), }); score.instruments[0].abbreviation = Some(String::from("Vln.")); score.instruments[0].sound_config = SoundConfiguration(vec![1, 2, 3]); score.instruments[0].transposition = Some(TranspositionInterval { diatonic_steps: -1, chromatic_steps: -2, }); score.instruments[0].default_clef = Clef::alto(); score.instruments[0].default_staff_lines = StaffLineConfiguration { line_count: 5, line_spacing: ss(1.2), line_style: LineStyle::Solid, bracket: None, }; score.instruments[0].unpitched_members = vec![UnpitchedMember { member: UnpitchedMemberId(3), name: String::from("snare"), staff_position: StaffPosition(2), }]; score.metadata.subtitle = Some(String::from("a subtitle")); score.metadata.lyricist = Some(String::from("a lyricist")); score.metadata.arranger = Some(String::from("an arranger")); score.metadata.creation_timestamp = Timestamp(1_700_000_000_000_000_000); score.metadata.modification_timestamp = Timestamp(1_700_000_100_000_000_000); score.metadata.additional = vec![ MetadataEntry { key: String::from("opus"), value: MetadataValue::Integer(27), }, MetadataEntry { key: String::from("dedication"), value: MetadataValue::Text(String::from("f\u{fc}r Elise")), }, MetadataEntry { key: String::from("urtext"), value: MetadataValue::Flag(true), }, ]; let bytes = score.canonical_bytes(); assert_eq!(Score::decode_canonical(&bytes).unwrap(), score); assert_eq!(Score::decode_canonical_versioned(&bytes, 3).unwrap(), score); // The per-value seam carries the filled bodies too. let slur = &score.cross_cutting.slurs[0]; assert_eq!( Slur::decode_canonical(&slur.canonical_bytes()).unwrap(), *slur ); } #[test] fn v0_regions_inside_canvas_decode_after_region_grew() { // The nested-Vec case the struct decoder exists for: multiple v0 Region // elements inside Canvas.regions must each decode and default-fill // permits_spanning_slurs. A wrong per-element size would desync the Vec // after the first element (every later region would misparse), so a // multi-region canvas is the discriminating fixture — valid_score_rich // carries three regions. let score = valid_score_rich(9); assert!( score.canvas.regions.len() >= 2, "need a multi-region canvas to exercise the Vec walk" ); let v0 = encode_v0_score(&score); let migrated = Score::decode_canonical_versioned(&v0, 0).unwrap(); assert_eq!(migrated, score); assert_eq!(migrated.canvas.regions.len(), score.canvas.regions.len()); assert!(migrated .canvas .regions .iter() .all(|r| !r.permits_spanning_slurs)); } #[test] fn v0_decode_is_strictly_canonical_over_the_v0_wire_form() { // The major-0 migration is strict, like the major-1 decoder: a canonical // v0 encoding is accepted and re-encodes to itself in the frozen v0 form // (`decode_v0_score` compares against `encode_v0_score`), while trailing // or truncated bytes are rejected. This closes the injectivity gap for // major-0 snapshots (content-addressed like major-1 ones). for seed in 0..32u64 { let score = valid_score(seed.wrapping_mul(0x9E37_79B9).wrapping_add(1)); let v0 = encode_v0_score(&score); // Canonical v0 is accepted and re-encodes to the same v0 bytes. let migrated = Score::decode_canonical_versioned(&v0, 0).unwrap(); assert_eq!(encode_v0_score(&migrated), v0); // Trailing garbage is rejected. let mut trailed = v0.clone(); trailed.push(0); assert!(Score::decode_canonical_versioned(&trailed, 0).is_err()); // A truncation is rejected. if !v0.is_empty() { assert!(Score::decode_canonical_versioned(&v0[..v0.len() - 1], 0).is_err()); } } } #[test] fn distinct_scores_serialize_differently() { assert_ne!( valid_score(1).canonical_bytes(), valid_score(2).canonical_bytes(), "distinct scores must produce distinct canonical bytes" ); // The rich generator differs from the simple one, too. assert_ne!( valid_score(3).canonical_bytes(), valid_score_rich(3).canonical_bytes() ); } #[test] fn degenerate_tuplet_ratio_is_rejected_on_decode() { // Guards the TupletRatio::dec re-validation (Pass 11 item 3.5): a // hand-crafted byte stream must not be able to inject a degenerate ratio // that TupletRatio::new would reject at construction. Without the // `.ok_or(Reconstruct)` in dec, these would decode into an // unconstructible-by-API value. let decode = |actual: u32, notated: u32| { let mut bytes = Vec::new(); actual.enc(&mut bytes); notated.enc(&mut bytes); TupletRatio::dec(&mut Reader::new(&bytes)) }; for (a, n) in [(0u32, 0u32), (2, 0), (0, 2), (4, 4)] { assert!( matches!(decode(a, n), Err(ScoreDecodeError::Reconstruct(_))), "degenerate ratio {a}:{n} must be rejected on decode" ); } // A well-formed ratio still decodes. let ok = decode(3, 2).expect("non-degenerate ratio decodes"); assert_eq!((ok.actual(), ok.notated()), (3, 2)); } #[test] fn exotic_event_and_pitch_variants_round_trip() { // Round-trip is structural, so this need not satisfy graph invariants — // it exists to exercise every event/pitch variant the generators omit. let mut score = valid_score(0xE0E0); let voice = score.canvas.regions[0].staff_instances()[0].voices[0].id; let replica = score.identity.replica_id; let mut next = 9_000u64; let mut mint = || { let id = EventId::new(replica, next); next += 1; id }; let pos = EventPosition::Musical(crate::time::MusicalPosition( crate::time::RationalTime::from_int(40), )); let dur = EventDuration::Musical(crate::time::MusicalDuration( crate::time::RationalTime::from_int(1), )); score .events .insert(Event::Rest(Rest { id: mint(), voice, position: pos.clone(), duration: dur.clone(), vertical_position: Some(crate::event::StaffPosition(-3)), visible: false, })) .expect("rest inserts"); score .events .insert(Event::Unpitched(UnpitchedEvent { id: mint(), voice, position: pos.clone(), duration: dur.clone(), staff_position: crate::event::StaffPosition(2), instrument_member: crate::event::UnpitchedMemberId(7), articulations: vec![crate::event::ArticulationMark], dynamic: Some(crate::event::DynamicMark), stem: crate::event::StemConfiguration, grace: Some(GraceKind::MeasuredFraction(crate::time::MusicalDuration( crate::time::RationalTime::new(1, 8).unwrap(), ))), })) .expect("unpitched inserts"); score .events .insert(Event::Indeterminate(IndeterminateEvent { id: mint(), voice, position: pos, duration: EventDuration::Indeterminate(crate::time::DurationBounds { lower: Some(crate::time::ConcreteDuration::Musical( crate::time::MusicalDuration(crate::time::RationalTime::from_int(1)), )), upper: None, }), indeterminacy: IndeterminacyKind::Compound(vec![ IndeterminacyKind::Pitch, IndeterminacyKind::Duration, ]), hints: IndeterminacyHints { duration_bounds: None, alternatives: vec![EventId::new(replica, 1)], textual_instruction: Some("ad lib.".to_owned()), }, })) .expect("indeterminate inserts"); // A spelling attachment exercising the pitch-spelling tree. score.spelling_attachments.push(SpellingAttachment { scope: SpellingScope::Pitch(crate::ids::PitchId::new(replica, 1)), directive: SpellingDirective::Explicit(PitchSpelling { nominal: SpellingNominal::Cmn(CmnNominal::F), accidentals: vec![AccidentalId::new("sharp")], octave: 4, render_hints: SpellingRenderHints { parenthesized: true, cautionary: false, editorial: true, small_print: false, }, }), source: SpellingSource::Imported { format: ForeignFormatId::new("musicxml"), }, priority: -2, layer: None, }); // Exotic metadata strings (preserved byte-exactly). score.metadata.title = Some("Étude café".to_owned()); score.metadata.composer = Some("composer".to_owned()); assert_round_trips(&score); } #[test] fn trailing_and_truncated_bytes_are_rejected() { let bytes = valid_score(7).canonical_bytes(); let mut trailing = bytes.clone(); trailing.push(0); assert_eq!( Score::decode_canonical(&trailing), Err(ScoreDecodeError::TrailingBytes) ); let truncated = &bytes[..bytes.len() - 1]; assert!( Score::decode_canonical(truncated).is_err(), "a truncated score must be rejected" ); // An empty buffer is not a score. assert!(Score::decode_canonical(&[]).is_err()); } }