epiphany/crates/epiphany-ops/src/decode.rs

637 lines
22 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),
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(&[14], precondition_reason).is_err());
assert!(exact(&[7], reanchor_reason).is_err());
}
#[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)
);
}
}