673 lines
24 KiB
Rust
673 lines
24 KiB
Rust
//! Decoder for the canonical materialized-state snapshot format.
|
||
//!
|
||
//! The operation layer owns this inverse because only it understands the
|
||
//! semantic types embedded in [`MaterializedState`](crate::MaterializedState).
|
||
//! Bundle storage deliberately treats snapshot payloads as opaque bytes.
|
||
|
||
use std::collections::BTreeMap;
|
||
|
||
use epiphany_core::{
|
||
CanonicalValue, MusicalPosition, OperationId, PitchId, PitchSpelling, RegionId, TypedObjectId,
|
||
};
|
||
use epiphany_determinism::CanonicalDecode;
|
||
|
||
use crate::{
|
||
ConflictId, ConflictKind, ConflictKindRegistryId, ConflictRecord, ConflictRegistry,
|
||
ConflictResolutionState, ExtensionPreconditionId, IntegrityAnomaly, IntegrityAnomalyKind,
|
||
IntegrityAnomalyRegistryId, MaterializedState, NoOpReason, ObjectKind, ObjectState,
|
||
OperationEffect, PendingReason, PreconditionFailureReason, PreconditionFailureRegistryId,
|
||
ReanchorReason, ReanchorReasonRegistryId, RepairKind, RepairKindRegistryId, RepairRecord,
|
||
ResolutionAction, ResolutionRegistryId, SerializedCanonicalInputs, TupletCompensationKind,
|
||
};
|
||
|
||
/// Failure to decode a canonical [`MaterializedState`] snapshot.
|
||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||
pub enum MaterializedDecodeError {
|
||
/// A field ended before its declared or fixed width.
|
||
UnexpectedEof,
|
||
/// A length prefix cannot be represented safely by this process.
|
||
LengthOverflow,
|
||
/// A tagged union carried an unknown discriminant.
|
||
InvalidTag { kind: &'static str, tag: u8 },
|
||
/// A primitive value failed its own canonical decoder.
|
||
InvalidValue(&'static str),
|
||
/// A canonical boolean was not encoded as zero or one.
|
||
InvalidBoolean(u8),
|
||
/// A canonical text field was not UTF-8.
|
||
InvalidUtf8,
|
||
/// Bytes remained after the complete state was decoded.
|
||
TrailingBytes,
|
||
/// The bytes decoded structurally but were not in canonical order/form.
|
||
NonCanonical,
|
||
}
|
||
|
||
impl core::fmt::Display for MaterializedDecodeError {
|
||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||
match self {
|
||
Self::UnexpectedEof => f.write_str("unexpected end of materialized-state bytes"),
|
||
Self::LengthOverflow => f.write_str("materialized-state 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(value) => write!(f, "invalid canonical boolean {value}"),
|
||
Self::InvalidUtf8 => f.write_str("invalid UTF-8 in canonical text"),
|
||
Self::TrailingBytes => f.write_str("trailing bytes after materialized state"),
|
||
Self::NonCanonical => f.write_str("materialized-state bytes are not canonical"),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl std::error::Error for MaterializedDecodeError {}
|
||
|
||
type Result<T> = core::result::Result<T, MaterializedDecodeError>;
|
||
|
||
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(MaterializedDecodeError::LengthOverflow)?;
|
||
if end > self.bytes.len() {
|
||
return Err(MaterializedDecodeError::UnexpectedEof);
|
||
}
|
||
let out = &self.bytes[self.pos..end];
|
||
self.pos = end;
|
||
Ok(out)
|
||
}
|
||
|
||
fn byte(&mut self) -> Result<u8> {
|
||
Ok(self.take(1)?[0])
|
||
}
|
||
|
||
fn u32_le(&mut self) -> Result<u32> {
|
||
Ok(u32::from_le_bytes(
|
||
self.take(4)?.try_into().expect("fixed width"),
|
||
))
|
||
}
|
||
|
||
fn u64_le(&mut self) -> Result<u64> {
|
||
Ok(u64::from_le_bytes(
|
||
self.take(8)?.try_into().expect("fixed width"),
|
||
))
|
||
}
|
||
|
||
fn u128_be(&mut self) -> Result<u128> {
|
||
Ok(u128::from_be_bytes(
|
||
self.take(16)?.try_into().expect("fixed width"),
|
||
))
|
||
}
|
||
|
||
fn len(&mut self) -> Result<usize> {
|
||
usize::try_from(self.u32_le()?).map_err(|_| MaterializedDecodeError::LengthOverflow)
|
||
}
|
||
|
||
fn lp_bytes(&mut self) -> Result<&'a [u8]> {
|
||
let len = self.len()?;
|
||
self.take(len)
|
||
}
|
||
|
||
fn seq<T>(&mut self, mut decode: impl FnMut(&[u8]) -> Result<T>) -> Result<Vec<T>> {
|
||
let count = self.len()?;
|
||
let mut values = Vec::with_capacity(count.min(1024));
|
||
for _ in 0..count {
|
||
values.push(decode(self.lp_bytes()?)?);
|
||
}
|
||
Ok(values)
|
||
}
|
||
|
||
fn finish(self) -> Result<()> {
|
||
if self.pos == self.bytes.len() {
|
||
Ok(())
|
||
} else {
|
||
Err(MaterializedDecodeError::TrailingBytes)
|
||
}
|
||
}
|
||
}
|
||
|
||
fn exact<T>(bytes: &[u8], decode: impl FnOnce(&mut Reader<'_>) -> Result<T>) -> Result<T> {
|
||
let mut reader = Reader::new(bytes);
|
||
let value = decode(&mut reader)?;
|
||
reader.finish()?;
|
||
Ok(value)
|
||
}
|
||
|
||
fn fixed<T: CanonicalDecode>(reader: &mut Reader<'_>, n: usize, name: &'static str) -> Result<T> {
|
||
T::decode_canonical(reader.take(n)?).map_err(|_| MaterializedDecodeError::InvalidValue(name))
|
||
}
|
||
|
||
fn operation_id(reader: &mut Reader<'_>) -> Result<OperationId> {
|
||
fixed(reader, 16, "OperationId")
|
||
}
|
||
|
||
fn typed_object_id(reader: &mut Reader<'_>) -> Result<TypedObjectId> {
|
||
let tag_bytes = reader
|
||
.bytes
|
||
.get(reader.pos..reader.pos.saturating_add(2))
|
||
.ok_or(MaterializedDecodeError::UnexpectedEof)?;
|
||
let tag = u16::from_be_bytes(tag_bytes.try_into().expect("two bytes"));
|
||
let width = if tag == 27 { 34 } else { 18 };
|
||
fixed(reader, width, "TypedObjectId")
|
||
}
|
||
|
||
fn musical_position(reader: &mut Reader<'_>) -> Result<MusicalPosition> {
|
||
let start = reader.pos;
|
||
reader.byte()?;
|
||
let numer_len = reader.len()?;
|
||
reader.take(numer_len)?;
|
||
let denom_len = reader.len()?;
|
||
reader.take(denom_len)?;
|
||
MusicalPosition::decode_canonical(&reader.bytes[start..reader.pos])
|
||
.map_err(|_| MaterializedDecodeError::InvalidValue("MusicalPosition"))
|
||
}
|
||
|
||
fn registry_id<T>(reader: &mut Reader<'_>, ctor: impl FnOnce(u128) -> T) -> Result<T> {
|
||
Ok(ctor(reader.u128_be()?))
|
||
}
|
||
|
||
fn object_state(reader: &mut Reader<'_>) -> Result<ObjectState> {
|
||
match reader.byte()? {
|
||
0 => Ok(ObjectState::Live),
|
||
1 => Ok(ObjectState::Tombstoned {
|
||
deleted_by: operation_id(reader)?,
|
||
minted_by: operation_id(reader)?,
|
||
}),
|
||
tag => Err(MaterializedDecodeError::InvalidTag {
|
||
kind: "ObjectState",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
|
||
fn pending_reason(reader: &mut Reader<'_>) -> Result<PendingReason> {
|
||
let tag = reader.byte()?;
|
||
let blocker = operation_id(reader)?;
|
||
match tag {
|
||
0 => Ok(PendingReason::MissingCausalPredecessor { missing: blocker }),
|
||
1 => Ok(PendingReason::DependsOnEquivocated { on: blocker }),
|
||
2 => Ok(PendingReason::DependsOnExcluded { on: blocker }),
|
||
3 => Ok(PendingReason::DependsOnPending { on: blocker }),
|
||
4 => Ok(PendingReason::HaltedBySystemCollision { at: blocker }),
|
||
tag => Err(MaterializedDecodeError::InvalidTag {
|
||
kind: "PendingReason",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
|
||
fn precondition_reason(reader: &mut Reader<'_>) -> Result<PreconditionFailureReason> {
|
||
match reader.byte()? {
|
||
0 => Ok(PreconditionFailureReason::TargetMissing),
|
||
1 => Ok(PreconditionFailureReason::TargetTombstoned),
|
||
2 => Ok(PreconditionFailureReason::WrongRegionTimeModel),
|
||
3 => Ok(PreconditionFailureReason::TupletCompensationInvalid),
|
||
4 => Ok(PreconditionFailureReason::EventDurationInvalid),
|
||
5 => Ok(PreconditionFailureReason::PositionOutsideRegion),
|
||
6 => Ok(PreconditionFailureReason::PitchSpaceMismatch),
|
||
7 => Ok(PreconditionFailureReason::VoiceMissing),
|
||
8 => Ok(PreconditionFailureReason::ExtensionPrecondition(
|
||
registry_id(reader, ExtensionPreconditionId)?,
|
||
)),
|
||
9 => Ok(PreconditionFailureReason::Registered(registry_id(
|
||
reader,
|
||
PreconditionFailureRegistryId,
|
||
)?)),
|
||
10 => Ok(PreconditionFailureReason::ContainerNotEmpty),
|
||
11 => Ok(PreconditionFailureReason::TempoMapMalformed),
|
||
12 => Ok(PreconditionFailureReason::SystemDerivedContentImmutable),
|
||
13 => Ok(PreconditionFailureReason::RecreateContentMismatch),
|
||
// Additive (Push 4a); a pre-existing decoder hole (P13-S20) — these
|
||
// two encode but could not decode until this rung.
|
||
14 => Ok(PreconditionFailureReason::AcousticRealizationPinned),
|
||
15 => Ok(PreconditionFailureReason::TranspositionOutOfRange),
|
||
// Additive (Genesis tranche G3b).
|
||
16 => Ok(PreconditionFailureReason::MeasureMeterMismatch),
|
||
17 => Ok(PreconditionFailureReason::MeasureOutOfOrder),
|
||
18 => Ok(PreconditionFailureReason::MeasureOrderUnverifiable),
|
||
tag => Err(MaterializedDecodeError::InvalidTag {
|
||
kind: "PreconditionFailureReason",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
|
||
fn no_op_reason(reader: &mut Reader<'_>) -> Result<NoOpReason> {
|
||
match reader.byte()? {
|
||
0 => Ok(NoOpReason::TargetTombstoned),
|
||
1 => Ok(NoOpReason::AlreadyApplied),
|
||
2 => Ok(NoOpReason::SupersededByLaterOperation {
|
||
superseder: operation_id(reader)?,
|
||
}),
|
||
3 => Ok(NoOpReason::PreconditionFailedUnderReduction {
|
||
reason: precondition_reason(reader)?,
|
||
}),
|
||
4 => Ok(NoOpReason::TransactionConflict),
|
||
tag => Err(MaterializedDecodeError::InvalidTag {
|
||
kind: "NoOpReason",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
|
||
fn reanchor_reason(reader: &mut Reader<'_>) -> Result<ReanchorReason> {
|
||
match reader.byte()? {
|
||
0 => Ok(ReanchorReason::SameVoiceNearer),
|
||
1 => Ok(ReanchorReason::SameStaffInstanceNearer),
|
||
2 => Ok(ReanchorReason::SameStaffNearer),
|
||
3 => Ok(ReanchorReason::SameRegionNearer),
|
||
4 => Ok(ReanchorReason::ExplicitFallback),
|
||
5 => Ok(ReanchorReason::DeclaredByExtension(registry_id(
|
||
reader,
|
||
ReanchorReasonRegistryId,
|
||
)?)),
|
||
6 => Ok(ReanchorReason::SameCanvasNearer),
|
||
tag => Err(MaterializedDecodeError::InvalidTag {
|
||
kind: "ReanchorReason",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
|
||
fn repair_kind(reader: &mut Reader<'_>) -> Result<RepairKind> {
|
||
match reader.byte()? {
|
||
0 => Ok(RepairKind::Reanchored {
|
||
from: typed_object_id(reader)?,
|
||
to: typed_object_id(reader)?,
|
||
reason: reanchor_reason(reader)?,
|
||
}),
|
||
1 => Ok(RepairKind::SpannerTruncated {
|
||
removed_members: reader.seq(|bytes| exact(bytes, typed_object_id))?,
|
||
}),
|
||
2 => Ok(RepairKind::Orphaned),
|
||
3 => Ok(RepairKind::CascadeDeleted),
|
||
4 => Ok(RepairKind::AttachmentTombstoned),
|
||
5 => Ok(RepairKind::VoicePromoted {
|
||
from: fixed(reader, 16, "VoiceId")?,
|
||
to: fixed(reader, 16, "VoiceId")?,
|
||
}),
|
||
6 => {
|
||
let compensation_kind = match reader.byte()? {
|
||
0 => TupletCompensationKind::ReplaceWithRest,
|
||
1 => TupletCompensationKind::RewriteTuplets,
|
||
2 => TupletCompensationKind::CascadeDeleteTuplets,
|
||
tag => {
|
||
return Err(MaterializedDecodeError::InvalidTag {
|
||
kind: "TupletCompensationKind",
|
||
tag,
|
||
})
|
||
}
|
||
};
|
||
Ok(RepairKind::TupletCompensated { compensation_kind })
|
||
}
|
||
7 => Ok(RepairKind::Registered(registry_id(
|
||
reader,
|
||
RepairKindRegistryId,
|
||
)?)),
|
||
tag => Err(MaterializedDecodeError::InvalidTag {
|
||
kind: "RepairKind",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
|
||
fn repair_record(reader: &mut Reader<'_>) -> Result<RepairRecord> {
|
||
Ok(RepairRecord {
|
||
kind: repair_kind(reader)?,
|
||
target: typed_object_id(reader)?,
|
||
})
|
||
}
|
||
|
||
fn operation_effect(reader: &mut Reader<'_>) -> Result<OperationEffect> {
|
||
match reader.byte()? {
|
||
0 => Ok(OperationEffect::Applied),
|
||
1 => Ok(OperationEffect::AppliedWithRepair {
|
||
repairs: reader.seq(|bytes| exact(bytes, repair_record))?,
|
||
}),
|
||
2 => Ok(OperationEffect::Conflicted {
|
||
conflict: ConflictId(reader.u128_be()?),
|
||
}),
|
||
3 => Ok(OperationEffect::TombstonedTarget {
|
||
target: typed_object_id(reader)?,
|
||
}),
|
||
4 => Ok(OperationEffect::NoOp {
|
||
reason: no_op_reason(reader)?,
|
||
}),
|
||
tag => Err(MaterializedDecodeError::InvalidTag {
|
||
kind: "OperationEffect",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
|
||
fn resolution_action(reader: &mut Reader<'_>) -> Result<ResolutionAction> {
|
||
match reader.byte()? {
|
||
0 => Ok(ResolutionAction::AcceptLoser),
|
||
1 => Ok(ResolutionAction::KeepWinner),
|
||
2 => Ok(ResolutionAction::Override {
|
||
override_operation: operation_id(reader)?,
|
||
}),
|
||
3 => Ok(ResolutionAction::Reanchor {
|
||
new_target: typed_object_id(reader)?,
|
||
}),
|
||
4 => Ok(ResolutionAction::Dismiss),
|
||
5 => Ok(ResolutionAction::Registered(registry_id(
|
||
reader,
|
||
ResolutionRegistryId,
|
||
)?)),
|
||
tag => Err(MaterializedDecodeError::InvalidTag {
|
||
kind: "ResolutionAction",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
|
||
fn conflict_resolution(reader: &mut Reader<'_>) -> Result<ConflictResolutionState> {
|
||
match reader.byte()? {
|
||
0 => Ok(ConflictResolutionState::Unresolved),
|
||
1 => Ok(ConflictResolutionState::Resolved {
|
||
by: operation_id(reader)?,
|
||
action: resolution_action(reader)?,
|
||
}),
|
||
2 => Ok(ConflictResolutionState::Dismissed {
|
||
by: operation_id(reader)?,
|
||
}),
|
||
tag => Err(MaterializedDecodeError::InvalidTag {
|
||
kind: "ConflictResolutionState",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
|
||
fn conflict_kind(reader: &mut Reader<'_>) -> Result<ConflictKind> {
|
||
match reader.byte()? {
|
||
0 => Ok(ConflictKind::StructuralFieldCollision {
|
||
winner: operation_id(reader)?,
|
||
loser: operation_id(reader)?,
|
||
field: crate::FieldPath(
|
||
std::str::from_utf8(reader.lp_bytes()?)
|
||
.map_err(|_| MaterializedDecodeError::InvalidUtf8)?
|
||
.to_owned(),
|
||
),
|
||
}),
|
||
1 => Ok(ConflictKind::TransactionConflict {
|
||
transaction: fixed(reader, 16, "TransactionId")?,
|
||
failed_members: reader.seq(|bytes| exact(bytes, operation_id))?,
|
||
}),
|
||
2 => Ok(ConflictKind::TombstonedTarget {
|
||
target: typed_object_id(reader)?,
|
||
operation: operation_id(reader)?,
|
||
}),
|
||
3 => Ok(ConflictKind::ReanchorFailure {
|
||
original_referent: typed_object_id(reader)?,
|
||
referencing_object: typed_object_id(reader)?,
|
||
}),
|
||
4 => Ok(ConflictKind::TimeModelMigrationFailure {
|
||
region: fixed(reader, 16, "RegionId")?,
|
||
incompatible_events: reader.seq(|bytes| exact(bytes, typed_object_id))?,
|
||
}),
|
||
5 => Ok(ConflictKind::ExtensionConflict {
|
||
kind_id: registry_id(reader, ConflictKindRegistryId)?,
|
||
details: reader.lp_bytes()?.to_vec(),
|
||
}),
|
||
tag => Err(MaterializedDecodeError::InvalidTag {
|
||
kind: "ConflictKind",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
|
||
fn conflict_record(reader: &mut Reader<'_>) -> Result<ConflictRecord> {
|
||
let record = ConflictRecord {
|
||
id: ConflictId(reader.u128_be()?),
|
||
caused_by: reader.seq(|bytes| exact(bytes, operation_id))?,
|
||
kind: conflict_kind(reader)?,
|
||
affected_objects: reader.seq(|bytes| exact(bytes, typed_object_id))?,
|
||
resolution_state: conflict_resolution(reader)?,
|
||
};
|
||
let ordered_ops = record.caused_by.windows(2).all(|pair| pair[0] < pair[1]);
|
||
let ordered_objects = record
|
||
.affected_objects
|
||
.windows(2)
|
||
.all(|pair| pair[0] < pair[1]);
|
||
let expected_id =
|
||
crate::derive_conflict_id(&record.kind, &record.caused_by, &record.affected_objects);
|
||
if !ordered_ops || !ordered_objects || record.id != expected_id {
|
||
return Err(MaterializedDecodeError::NonCanonical);
|
||
}
|
||
Ok(record)
|
||
}
|
||
|
||
fn object_kind(reader: &mut Reader<'_>) -> Result<ObjectKind> {
|
||
match reader.byte()? {
|
||
0 => Ok(ObjectKind::Voice),
|
||
1 => Ok(ObjectKind::Pitch),
|
||
2 => Ok(ObjectKind::Registered(registry_id(
|
||
reader,
|
||
crate::OperationKindRegistryId,
|
||
)?)),
|
||
tag => Err(MaterializedDecodeError::InvalidTag {
|
||
kind: "ObjectKind",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
|
||
fn integrity_anomaly_kind(reader: &mut Reader<'_>) -> Result<IntegrityAnomalyKind> {
|
||
match reader.byte()? {
|
||
0 => Ok(IntegrityAnomalyKind::SystemIdentifierCollision {
|
||
kind: object_kind(reader)?,
|
||
colliding_counter: reader.u64_le()?,
|
||
input_set_a: SerializedCanonicalInputs(reader.lp_bytes()?.to_vec()),
|
||
input_set_b: SerializedCanonicalInputs(reader.lp_bytes()?.to_vec()),
|
||
}),
|
||
1 => Ok(IntegrityAnomalyKind::OperationSlotEquivocated {
|
||
operation_id: operation_id(reader)?,
|
||
}),
|
||
2 => Ok(IntegrityAnomalyKind::ReplicaStreamQuarantined {
|
||
replica: fixed(reader, 8, "ReplicaId")?,
|
||
first_bad_counter: reader.u64_le()?,
|
||
}),
|
||
3 => Ok(IntegrityAnomalyKind::Registered(registry_id(
|
||
reader,
|
||
IntegrityAnomalyRegistryId,
|
||
)?)),
|
||
tag => Err(MaterializedDecodeError::InvalidTag {
|
||
kind: "IntegrityAnomalyKind",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
|
||
fn integrity_anomaly(reader: &mut Reader<'_>) -> Result<IntegrityAnomaly> {
|
||
let anomaly = IntegrityAnomaly {
|
||
id: fixed(reader, 16, "IntegrityAnomalyId")?,
|
||
kind: integrity_anomaly_kind(reader)?,
|
||
};
|
||
if IntegrityAnomaly::new(anomaly.kind.clone()).id != anomaly.id {
|
||
return Err(MaterializedDecodeError::NonCanonical);
|
||
}
|
||
Ok(anomaly)
|
||
}
|
||
|
||
pub(crate) fn decode_materialized_state(bytes: &[u8]) -> Result<MaterializedState> {
|
||
let mut reader = Reader::new(bytes);
|
||
|
||
let effect_count = reader.len()?;
|
||
let mut effects = Vec::with_capacity(effect_count.min(1024));
|
||
for _ in 0..effect_count {
|
||
let id = operation_id(&mut reader)?;
|
||
let effect = exact(reader.lp_bytes()?, operation_effect)?;
|
||
effects.push((id, effect));
|
||
}
|
||
|
||
let records = reader.seq(|bytes| exact(bytes, conflict_record))?;
|
||
let mut conflicts = ConflictRegistry::new();
|
||
for record in records {
|
||
conflicts.insert(record);
|
||
}
|
||
|
||
let anomaly_count = reader.len()?;
|
||
let mut anomalies = Vec::with_capacity(anomaly_count.min(1024));
|
||
for _ in 0..anomaly_count {
|
||
anomalies.push(exact(reader.lp_bytes()?, integrity_anomaly)?);
|
||
}
|
||
if !anomalies.windows(2).all(|pair| pair[0].id < pair[1].id) {
|
||
return Err(MaterializedDecodeError::NonCanonical);
|
||
}
|
||
|
||
let object_count = reader.len()?;
|
||
let mut objects = BTreeMap::new();
|
||
for _ in 0..object_count {
|
||
objects.insert(typed_object_id(&mut reader)?, object_state(&mut reader)?);
|
||
}
|
||
|
||
let spelling_count = reader.len()?;
|
||
let mut spellings = BTreeMap::new();
|
||
for _ in 0..spelling_count {
|
||
let pitch = fixed::<PitchId>(&mut reader, 16, "PitchId")?;
|
||
// The value is the full PitchSpelling (v1), encoded behind a u32 length
|
||
// prefix via its canonical value bytes.
|
||
let spelling = PitchSpelling::decode_canonical(reader.lp_bytes()?)
|
||
.map_err(|_| MaterializedDecodeError::InvalidValue("PitchSpelling"))?;
|
||
spellings.insert(pitch, spelling);
|
||
}
|
||
|
||
let break_count = reader.len()?;
|
||
let mut breaks = BTreeMap::new();
|
||
for _ in 0..break_count {
|
||
let region = fixed::<RegionId>(&mut reader, 16, "RegionId")?;
|
||
let anchor = musical_position(&mut reader)?;
|
||
let present = match reader.byte()? {
|
||
0 => false,
|
||
1 => true,
|
||
value => return Err(MaterializedDecodeError::InvalidBoolean(value)),
|
||
};
|
||
breaks.insert((region, anchor), present);
|
||
}
|
||
|
||
let page_break_count = reader.len()?;
|
||
let mut page_breaks = BTreeMap::new();
|
||
for _ in 0..page_break_count {
|
||
let region = fixed::<RegionId>(&mut reader, 16, "RegionId")?;
|
||
let anchor = musical_position(&mut reader)?;
|
||
let present = match reader.byte()? {
|
||
0 => false,
|
||
1 => true,
|
||
value => return Err(MaterializedDecodeError::InvalidBoolean(value)),
|
||
};
|
||
page_breaks.insert((region, anchor), present);
|
||
}
|
||
|
||
let pending_count = reader.len()?;
|
||
let mut pending = Vec::with_capacity(pending_count.min(1024));
|
||
for _ in 0..pending_count {
|
||
pending.push((operation_id(&mut reader)?, pending_reason(&mut reader)?));
|
||
}
|
||
if !pending.windows(2).all(|pair| pair[0].0 < pair[1].0) {
|
||
return Err(MaterializedDecodeError::NonCanonical);
|
||
}
|
||
reader.finish()?;
|
||
|
||
let state = MaterializedState {
|
||
effects,
|
||
conflicts,
|
||
anomalies,
|
||
objects,
|
||
spellings,
|
||
breaks,
|
||
page_breaks,
|
||
pending,
|
||
};
|
||
if state.canonical_bytes() != bytes {
|
||
return Err(MaterializedDecodeError::NonCanonical);
|
||
}
|
||
Ok(state)
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use epiphany_determinism::fuzz::SplitMix64;
|
||
|
||
#[test]
|
||
fn reduced_states_decode_and_reencode() {
|
||
let mut rng = SplitMix64::new(0xDEC0_DED5);
|
||
for _ in 0..500 {
|
||
let envelopes = crate::fuzz::gen_envelope_set(&mut rng, 20);
|
||
let mut set = crate::OperationSet::new();
|
||
set.accept_all(envelopes);
|
||
let state = set.reduce();
|
||
let bytes = state.canonical_bytes();
|
||
let decoded = MaterializedState::decode_canonical(&bytes).unwrap();
|
||
assert_eq!(decoded.canonical_bytes(), bytes);
|
||
assert_eq!(decoded, state);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn pass12_appended_discriminants_decode() {
|
||
assert_eq!(
|
||
exact(&[12], precondition_reason).unwrap(),
|
||
PreconditionFailureReason::SystemDerivedContentImmutable
|
||
);
|
||
assert_eq!(
|
||
exact(&[13], precondition_reason).unwrap(),
|
||
PreconditionFailureReason::RecreateContentMismatch
|
||
);
|
||
assert_eq!(
|
||
exact(&[6], reanchor_reason).unwrap(),
|
||
ReanchorReason::SameCanvasNearer
|
||
);
|
||
// The vocabularies stay bounded: one past the append rejects.
|
||
assert!(exact(&[19], precondition_reason).is_err());
|
||
assert!(exact(&[7], reanchor_reason).is_err());
|
||
}
|
||
|
||
/// Genesis tranche G3b (contract row 12a / M64–M66): reasons 14 and 15
|
||
/// (Push 4a's `AcousticRealizationPinned`/`TranspositionOutOfRange`) were
|
||
/// a pre-existing decoder hole — they encoded but could not decode
|
||
/// (P13-S20) — and this rung's own 16–18 must decode too.
|
||
#[test]
|
||
fn genesis_g3b_and_the_p13_s20_hole_decode() {
|
||
assert_eq!(
|
||
exact(&[14], precondition_reason).unwrap(),
|
||
PreconditionFailureReason::AcousticRealizationPinned
|
||
);
|
||
assert_eq!(
|
||
exact(&[15], precondition_reason).unwrap(),
|
||
PreconditionFailureReason::TranspositionOutOfRange
|
||
);
|
||
assert_eq!(
|
||
exact(&[16], precondition_reason).unwrap(),
|
||
PreconditionFailureReason::MeasureMeterMismatch
|
||
);
|
||
assert_eq!(
|
||
exact(&[17], precondition_reason).unwrap(),
|
||
PreconditionFailureReason::MeasureOutOfOrder
|
||
);
|
||
assert_eq!(
|
||
exact(&[18], precondition_reason).unwrap(),
|
||
PreconditionFailureReason::MeasureOrderUnverifiable
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn decoder_rejects_truncation_and_trailing_bytes() {
|
||
let bytes = MaterializedState::default().canonical_bytes();
|
||
assert!(MaterializedState::decode_canonical(&bytes[..bytes.len() - 1]).is_err());
|
||
let mut trailing = bytes;
|
||
trailing.push(0);
|
||
assert_eq!(
|
||
MaterializedState::decode_canonical(&trailing),
|
||
Err(MaterializedDecodeError::TrailingBytes)
|
||
);
|
||
}
|
||
}
|