prikk-object 0.18.2

Prikk object identity, canonical encoding, and object payload types.
Documentation
//! Reference payload types.

use prikk_error::{PrikkError, Result};

use crate::canonical::{WireType, is_strictly_sorted};
use crate::{CanonicalEncode, CanonicalWriter, ObjectId};

/// Ref kind.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(u16)]
pub enum RefKind {
    /// Branch ref.
    Branch = 1,
    /// Tag ref.
    Tag = 2,
}

impl RefKind {
    /// Stable code.
    #[must_use]
    pub const fn code(self) -> u16 {
        self as u16
    }

    /// Parse a stable code.
    pub fn from_code(code: u32) -> Result<Self> {
        match code {
            1 => Ok(Self::Branch),
            2 => Ok(Self::Tag),
            other => Err(PrikkError::MalformedData(format!(
                "unknown ref kind code: {other}"
            ))),
        }
    }
}

/// RefState schema version at which the `closed` field (tag 7) is meaningful. A schema-1 payload
/// carrying tag 7 is malformed — see `decode_canonical`.
pub const REF_STATE_CLOSED_SCHEMA: u32 = 2;

/// RefState payload stored as a content-addressed object.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RefStatePayload {
    /// Human-readable ref name.
    pub ref_name: String,
    /// Ref kind.
    pub kind: RefKind,
    /// Target object ID.
    pub target_object_id: ObjectId,
    /// Monotonic sequence number.
    pub update_seq: u64,
    /// Previous ref-state object ID.
    pub previous_ref_state_id: Option<ObjectId>,
    /// Required attestation IDs that justified this state.
    pub required_attestation_ids: Vec<ObjectId>,
    /// Whether this ref state closes the ref (DC-61). Tag 7. **Encoded only when `true`** — an
    /// open ref state must remain byte-identical to the pre-DC-61 six-field encoding, so this must
    /// never be emitted as an explicit `false`. Schema-2 only; a schema-1 payload carrying tag 7 at
    /// all (`true` or `false`) is malformed. Closure carries no other information: the pointer,
    /// target, and history are unchanged, so reopening is an ordinary CAS update to a new state
    /// with this field simply absent again.
    pub closed: bool,
}

impl RefStatePayload {
    /// Decode a RefState payload from Prikk canonical TLV bytes. `schema_version` comes from the
    /// object envelope carrying these bytes — decoding is schema-aware because tag 7 is legal only
    /// at `REF_STATE_CLOSED_SCHEMA` and above.
    pub fn decode_canonical(bytes: &[u8], schema_version: u32) -> Result<Self> {
        let mut cursor = CanonicalCursor::new(bytes);
        let mut ref_name = None;
        let mut kind = None;
        let mut target_object_id = None;
        let mut update_seq = None;
        let mut previous_ref_state_id = None;
        let mut required_attestation_ids = Vec::new();
        let mut closed = false;
        while let Some(field) = cursor.next_field()? {
            match field.tag {
                1 => ref_name = Some(field.read_string()?),
                2 => target_object_id = Some(field.read_object_id()?),
                3 => update_seq = Some(field.read_u64()?),
                4 => previous_ref_state_id = Some(field.read_object_id()?),
                5 => required_attestation_ids.push(field.read_object_id()?),
                6 => kind = Some(RefKind::from_code(u32::from(field.read_enum_u16()?))?),
                7 => {
                    if schema_version < REF_STATE_CLOSED_SCHEMA {
                        return Err(PrikkError::MalformedData(format!(
                            "RefState schema {schema_version} must not carry a closed field; \
                             requires schema {REF_STATE_CLOSED_SCHEMA}"
                        )));
                    }
                    let value = field.read_bool()?;
                    if !value {
                        return Err(PrikkError::MalformedData(
                            "RefState closed field must be absent when open, never encoded as \
                             false"
                                .to_string(),
                        ));
                    }
                    closed = true;
                }
                other => {
                    return Err(PrikkError::MalformedData(format!(
                        "unknown RefState field tag: {other}"
                    )));
                }
            }
        }
        let payload = Self {
            ref_name: ref_name.ok_or_else(|| {
                PrikkError::MalformedData("RefState missing ref_name".to_string())
            })?,
            kind: kind
                .ok_or_else(|| PrikkError::MalformedData("RefState missing kind".to_string()))?,
            target_object_id: target_object_id.ok_or_else(|| {
                PrikkError::MalformedData("RefState missing target_object_id".to_string())
            })?,
            update_seq: update_seq.ok_or_else(|| {
                PrikkError::MalformedData("RefState missing update_seq".to_string())
            })?,
            previous_ref_state_id,
            required_attestation_ids,
            closed,
        };
        if !is_strictly_sorted(&payload.required_attestation_ids) {
            return Err(PrikkError::MalformedData(
                "RefState attestation IDs are not sorted and unique".to_string(),
            ));
        }
        Ok(payload)
    }
}

impl CanonicalEncode for RefStatePayload {
    fn encode_canonical(&self, writer: &mut CanonicalWriter) -> Result<()> {
        if !is_strictly_sorted(&self.required_attestation_ids) {
            return Err(PrikkError::CanonicalEncoding(
                "required_attestation_ids must be sorted and unique".to_string(),
            ));
        }
        writer.field_string(1, &self.ref_name)?;
        writer.field_object_id(2, &self.target_object_id)?;
        writer.field_u64(3, self.update_seq)?;
        if let Some(previous) = self.previous_ref_state_id {
            writer.field_object_id(4, &previous)?;
        }
        writer.repeated_object_id(5, &self.required_attestation_ids)?;
        writer.field_enum_u16(6, self.kind.code())?;
        if self.closed {
            writer.field_bool(7, true)?;
        }
        Ok(())
    }
}

/// Ref-update event payload stored inline in ref logs.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RefUpdatePayload {
    /// Ref name.
    pub ref_name: String,
    /// Previous RefState ID.
    pub old_ref_state_id: Option<ObjectId>,
    /// New RefState ID.
    pub new_ref_state_id: ObjectId,
    /// New target object ID.
    pub new_target_object_id: ObjectId,
    /// Update sequence.
    pub update_seq: u64,
    /// Schema-1 no-clock sentinel; production writes require zero.
    /// Retained nonzero format-1 values are legacy diagnostic data, never authoritative time.
    pub created_at: u64,
    /// Author key ID.
    pub author_key_id: String,
}

impl CanonicalEncode for RefUpdatePayload {
    fn encode_canonical(&self, writer: &mut CanonicalWriter) -> Result<()> {
        writer.field_string(1, &self.ref_name)?;
        if let Some(old) = self.old_ref_state_id {
            writer.field_object_id(2, &old)?;
        }
        writer.field_object_id(3, &self.new_ref_state_id)?;
        writer.field_object_id(4, &self.new_target_object_id)?;
        writer.field_u64(5, self.update_seq)?;
        writer.field_u64(6, self.created_at)?;
        writer.field_string(7, &self.author_key_id)?;
        Ok(())
    }
}

impl RefUpdatePayload {
    /// Decode a RefUpdate payload from Prikk canonical TLV bytes.
    pub fn decode_canonical(bytes: &[u8]) -> Result<Self> {
        let mut cursor = CanonicalCursor::new(bytes);
        let mut ref_name = None;
        let mut old_ref_state_id = None;
        let mut new_ref_state_id = None;
        let mut new_target_object_id = None;
        let mut update_seq = None;
        let mut created_at = None;
        let mut author_key_id = None;
        while let Some(field) = cursor.next_field()? {
            match field.tag {
                1 => ref_name = Some(field.read_string()?),
                2 => old_ref_state_id = Some(field.read_object_id()?),
                3 => new_ref_state_id = Some(field.read_object_id()?),
                4 => new_target_object_id = Some(field.read_object_id()?),
                5 => update_seq = Some(field.read_u64()?),
                6 => created_at = Some(field.read_u64()?),
                7 => author_key_id = Some(field.read_string()?),
                other => {
                    return Err(PrikkError::MalformedData(format!(
                        "unknown RefUpdate field tag: {other}"
                    )));
                }
            }
        }
        Ok(Self {
            ref_name: ref_name.ok_or_else(|| {
                PrikkError::MalformedData("RefUpdate missing ref_name".to_string())
            })?,
            old_ref_state_id,
            new_ref_state_id: new_ref_state_id.ok_or_else(|| {
                PrikkError::MalformedData("RefUpdate missing new_ref_state_id".to_string())
            })?,
            new_target_object_id: new_target_object_id.ok_or_else(|| {
                PrikkError::MalformedData("RefUpdate missing new_target_object_id".to_string())
            })?,
            update_seq: update_seq.ok_or_else(|| {
                PrikkError::MalformedData("RefUpdate missing update_seq".to_string())
            })?,
            created_at: created_at.ok_or_else(|| {
                PrikkError::MalformedData("RefUpdate missing created_at".to_string())
            })?,
            author_key_id: author_key_id.ok_or_else(|| {
                PrikkError::MalformedData("RefUpdate missing author_key_id".to_string())
            })?,
        })
    }
}

struct CanonicalCursor<'a> {
    bytes: &'a [u8],
    pos: usize,
    last_tag: Option<u16>,
}

impl<'a> CanonicalCursor<'a> {
    const fn new(bytes: &'a [u8]) -> Self {
        Self {
            bytes,
            pos: 0,
            last_tag: None,
        }
    }

    fn next_field(&mut self) -> Result<Option<CanonicalField<'a>>> {
        if self.pos == self.bytes.len() {
            return Ok(None);
        }
        let tag = u16::from_be_bytes(self.read_array::<2>()?);
        if tag == 0 {
            return Err(PrikkError::MalformedData(
                "field tag 0 is reserved".to_string(),
            ));
        }
        if let Some(last) = self.last_tag {
            if tag < last {
                return Err(PrikkError::MalformedData(format!(
                    "field tag order violation: {tag} after {last}"
                )));
            }
        }
        self.last_tag = Some(tag);
        let wire_type = self.read_u8()?;
        let len = usize::try_from(u64::from_be_bytes(self.read_array::<8>()?)).map_err(|_| {
            PrikkError::MalformedData("canonical field length does not fit usize".to_string())
        })?;
        let value = self.read_exact(len)?;
        Ok(Some(CanonicalField {
            tag,
            wire_type,
            value,
        }))
    }

    fn read_u8(&mut self) -> Result<u8> {
        let value = self.read_exact(1)?;
        let Some(byte) = value.first() else {
            return Err(PrikkError::MalformedData(
                "unexpected empty byte".to_string(),
            ));
        };
        Ok(*byte)
    }

    fn read_array<const N: usize>(&mut self) -> Result<[u8; N]> {
        let bytes = self.read_exact(N)?;
        let mut out = [0_u8; N];
        out.copy_from_slice(bytes);
        Ok(out)
    }

    fn read_exact(&mut self, len: usize) -> Result<&'a [u8]> {
        let end = self
            .pos
            .checked_add(len)
            .ok_or_else(|| PrikkError::MalformedData("canonical range overflow".to_string()))?;
        let Some(slice) = self.bytes.get(self.pos..end) else {
            return Err(PrikkError::MalformedData(
                "unexpected end of canonical payload".to_string(),
            ));
        };
        self.pos = end;
        Ok(slice)
    }
}

struct CanonicalField<'a> {
    tag: u16,
    wire_type: u8,
    value: &'a [u8],
}

impl<'a> CanonicalField<'a> {
    fn read_string(&self) -> Result<String> {
        self.require_wire(WireType::String)?;
        String::from_utf8(self.value.to_vec())
            .map_err(|err| PrikkError::MalformedData(format!("invalid UTF-8 string: {err}")))
    }

    fn read_u64(&self) -> Result<u64> {
        self.require_wire(WireType::U64)?;
        Ok(u64::from_be_bytes(self.read_array::<8>()?))
    }

    fn read_object_id(&self) -> Result<ObjectId> {
        self.require_wire(WireType::ObjectId)?;
        Ok(ObjectId::from_bytes(self.read_array::<32>()?))
    }

    fn read_enum_u16(&self) -> Result<u16> {
        self.require_wire(WireType::EnumU16)?;
        Ok(u16::from_be_bytes(self.read_array::<2>()?))
    }

    fn read_bool(&self) -> Result<bool> {
        self.require_wire(WireType::Bool)?;
        match self.read_array::<1>()?[0] {
            0 => Ok(false),
            1 => Ok(true),
            other => Err(PrikkError::MalformedData(format!(
                "field {} has invalid bool byte: {other}",
                self.tag
            ))),
        }
    }

    fn require_wire(&self, expected: WireType) -> Result<()> {
        if self.wire_type == expected as u8 {
            return Ok(());
        }
        Err(PrikkError::MalformedData(format!(
            "field {} has wrong wire type: expected {}, got {}",
            self.tag, expected as u8, self.wire_type
        )))
    }

    fn read_array<const N: usize>(&self) -> Result<[u8; N]> {
        if self.value.len() != N {
            return Err(PrikkError::MalformedData(format!(
                "field {} expected {N} bytes, got {}",
                self.tag,
                self.value.len()
            )));
        }
        let mut out = [0_u8; N];
        out.copy_from_slice(self.value);
        Ok(out)
    }
}