Skip to main content

prikk_object/payload/
refs.rs

1//! Reference payload types.
2
3use prikk_error::{PrikkError, Result};
4
5use crate::canonical::{WireType, is_strictly_sorted};
6use crate::{CanonicalEncode, CanonicalWriter, ObjectId};
7
8/// Ref kind.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
10#[repr(u16)]
11pub enum RefKind {
12    /// Branch ref.
13    Branch = 1,
14    /// Tag ref.
15    Tag = 2,
16}
17
18impl RefKind {
19    /// Stable code.
20    #[must_use]
21    pub const fn code(self) -> u16 {
22        self as u16
23    }
24
25    /// Parse a stable code.
26    pub fn from_code(code: u32) -> Result<Self> {
27        match code {
28            1 => Ok(Self::Branch),
29            2 => Ok(Self::Tag),
30            other => Err(PrikkError::MalformedData(format!(
31                "unknown ref kind code: {other}"
32            ))),
33        }
34    }
35}
36
37/// RefState schema version at which the `closed` field (tag 7) is meaningful. A schema-1 payload
38/// carrying tag 7 is malformed — see `decode_canonical`.
39pub const REF_STATE_CLOSED_SCHEMA: u32 = 2;
40
41/// RefState payload stored as a content-addressed object.
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct RefStatePayload {
44    /// Human-readable ref name.
45    pub ref_name: String,
46    /// Ref kind.
47    pub kind: RefKind,
48    /// Target object ID.
49    pub target_object_id: ObjectId,
50    /// Monotonic sequence number.
51    pub update_seq: u64,
52    /// Previous ref-state object ID.
53    pub previous_ref_state_id: Option<ObjectId>,
54    /// Required attestation IDs that justified this state.
55    pub required_attestation_ids: Vec<ObjectId>,
56    /// Whether this ref state closes the ref (DC-61). Tag 7. **Encoded only when `true`** — an
57    /// open ref state must remain byte-identical to the pre-DC-61 six-field encoding, so this must
58    /// never be emitted as an explicit `false`. Schema-2 only; a schema-1 payload carrying tag 7 at
59    /// all (`true` or `false`) is malformed. Closure carries no other information: the pointer,
60    /// target, and history are unchanged, so reopening is an ordinary CAS update to a new state
61    /// with this field simply absent again.
62    pub closed: bool,
63}
64
65impl RefStatePayload {
66    /// Decode a RefState payload from Prikk canonical TLV bytes. `schema_version` comes from the
67    /// object envelope carrying these bytes — decoding is schema-aware because tag 7 is legal only
68    /// at `REF_STATE_CLOSED_SCHEMA` and above.
69    pub fn decode_canonical(bytes: &[u8], schema_version: u32) -> Result<Self> {
70        let mut cursor = CanonicalCursor::new(bytes);
71        let mut ref_name = None;
72        let mut kind = None;
73        let mut target_object_id = None;
74        let mut update_seq = None;
75        let mut previous_ref_state_id = None;
76        let mut required_attestation_ids = Vec::new();
77        let mut closed = false;
78        while let Some(field) = cursor.next_field()? {
79            match field.tag {
80                1 => ref_name = Some(field.read_string()?),
81                2 => target_object_id = Some(field.read_object_id()?),
82                3 => update_seq = Some(field.read_u64()?),
83                4 => previous_ref_state_id = Some(field.read_object_id()?),
84                5 => required_attestation_ids.push(field.read_object_id()?),
85                6 => kind = Some(RefKind::from_code(u32::from(field.read_enum_u16()?))?),
86                7 => {
87                    if schema_version < REF_STATE_CLOSED_SCHEMA {
88                        return Err(PrikkError::MalformedData(format!(
89                            "RefState schema {schema_version} must not carry a closed field; \
90                             requires schema {REF_STATE_CLOSED_SCHEMA}"
91                        )));
92                    }
93                    let value = field.read_bool()?;
94                    if !value {
95                        return Err(PrikkError::MalformedData(
96                            "RefState closed field must be absent when open, never encoded as \
97                             false"
98                                .to_string(),
99                        ));
100                    }
101                    closed = true;
102                }
103                other => {
104                    return Err(PrikkError::MalformedData(format!(
105                        "unknown RefState field tag: {other}"
106                    )));
107                }
108            }
109        }
110        let payload = Self {
111            ref_name: ref_name.ok_or_else(|| {
112                PrikkError::MalformedData("RefState missing ref_name".to_string())
113            })?,
114            kind: kind
115                .ok_or_else(|| PrikkError::MalformedData("RefState missing kind".to_string()))?,
116            target_object_id: target_object_id.ok_or_else(|| {
117                PrikkError::MalformedData("RefState missing target_object_id".to_string())
118            })?,
119            update_seq: update_seq.ok_or_else(|| {
120                PrikkError::MalformedData("RefState missing update_seq".to_string())
121            })?,
122            previous_ref_state_id,
123            required_attestation_ids,
124            closed,
125        };
126        if !is_strictly_sorted(&payload.required_attestation_ids) {
127            return Err(PrikkError::MalformedData(
128                "RefState attestation IDs are not sorted and unique".to_string(),
129            ));
130        }
131        Ok(payload)
132    }
133}
134
135impl CanonicalEncode for RefStatePayload {
136    fn encode_canonical(&self, writer: &mut CanonicalWriter) -> Result<()> {
137        if !is_strictly_sorted(&self.required_attestation_ids) {
138            return Err(PrikkError::CanonicalEncoding(
139                "required_attestation_ids must be sorted and unique".to_string(),
140            ));
141        }
142        writer.field_string(1, &self.ref_name)?;
143        writer.field_object_id(2, &self.target_object_id)?;
144        writer.field_u64(3, self.update_seq)?;
145        if let Some(previous) = self.previous_ref_state_id {
146            writer.field_object_id(4, &previous)?;
147        }
148        writer.repeated_object_id(5, &self.required_attestation_ids)?;
149        writer.field_enum_u16(6, self.kind.code())?;
150        if self.closed {
151            writer.field_bool(7, true)?;
152        }
153        Ok(())
154    }
155}
156
157/// Ref-update event payload stored inline in ref logs.
158#[derive(Debug, Clone, PartialEq, Eq)]
159pub struct RefUpdatePayload {
160    /// Ref name.
161    pub ref_name: String,
162    /// Previous RefState ID.
163    pub old_ref_state_id: Option<ObjectId>,
164    /// New RefState ID.
165    pub new_ref_state_id: ObjectId,
166    /// New target object ID.
167    pub new_target_object_id: ObjectId,
168    /// Update sequence.
169    pub update_seq: u64,
170    /// Schema-1 no-clock sentinel; production writes require zero.
171    /// Retained nonzero format-1 values are legacy diagnostic data, never authoritative time.
172    pub created_at: u64,
173    /// Author key ID.
174    pub author_key_id: String,
175}
176
177impl CanonicalEncode for RefUpdatePayload {
178    fn encode_canonical(&self, writer: &mut CanonicalWriter) -> Result<()> {
179        writer.field_string(1, &self.ref_name)?;
180        if let Some(old) = self.old_ref_state_id {
181            writer.field_object_id(2, &old)?;
182        }
183        writer.field_object_id(3, &self.new_ref_state_id)?;
184        writer.field_object_id(4, &self.new_target_object_id)?;
185        writer.field_u64(5, self.update_seq)?;
186        writer.field_u64(6, self.created_at)?;
187        writer.field_string(7, &self.author_key_id)?;
188        Ok(())
189    }
190}
191
192impl RefUpdatePayload {
193    /// Decode a RefUpdate payload from Prikk canonical TLV bytes.
194    pub fn decode_canonical(bytes: &[u8]) -> Result<Self> {
195        let mut cursor = CanonicalCursor::new(bytes);
196        let mut ref_name = None;
197        let mut old_ref_state_id = None;
198        let mut new_ref_state_id = None;
199        let mut new_target_object_id = None;
200        let mut update_seq = None;
201        let mut created_at = None;
202        let mut author_key_id = None;
203        while let Some(field) = cursor.next_field()? {
204            match field.tag {
205                1 => ref_name = Some(field.read_string()?),
206                2 => old_ref_state_id = Some(field.read_object_id()?),
207                3 => new_ref_state_id = Some(field.read_object_id()?),
208                4 => new_target_object_id = Some(field.read_object_id()?),
209                5 => update_seq = Some(field.read_u64()?),
210                6 => created_at = Some(field.read_u64()?),
211                7 => author_key_id = Some(field.read_string()?),
212                other => {
213                    return Err(PrikkError::MalformedData(format!(
214                        "unknown RefUpdate field tag: {other}"
215                    )));
216                }
217            }
218        }
219        Ok(Self {
220            ref_name: ref_name.ok_or_else(|| {
221                PrikkError::MalformedData("RefUpdate missing ref_name".to_string())
222            })?,
223            old_ref_state_id,
224            new_ref_state_id: new_ref_state_id.ok_or_else(|| {
225                PrikkError::MalformedData("RefUpdate missing new_ref_state_id".to_string())
226            })?,
227            new_target_object_id: new_target_object_id.ok_or_else(|| {
228                PrikkError::MalformedData("RefUpdate missing new_target_object_id".to_string())
229            })?,
230            update_seq: update_seq.ok_or_else(|| {
231                PrikkError::MalformedData("RefUpdate missing update_seq".to_string())
232            })?,
233            created_at: created_at.ok_or_else(|| {
234                PrikkError::MalformedData("RefUpdate missing created_at".to_string())
235            })?,
236            author_key_id: author_key_id.ok_or_else(|| {
237                PrikkError::MalformedData("RefUpdate missing author_key_id".to_string())
238            })?,
239        })
240    }
241}
242
243struct CanonicalCursor<'a> {
244    bytes: &'a [u8],
245    pos: usize,
246    last_tag: Option<u16>,
247}
248
249impl<'a> CanonicalCursor<'a> {
250    const fn new(bytes: &'a [u8]) -> Self {
251        Self {
252            bytes,
253            pos: 0,
254            last_tag: None,
255        }
256    }
257
258    fn next_field(&mut self) -> Result<Option<CanonicalField<'a>>> {
259        if self.pos == self.bytes.len() {
260            return Ok(None);
261        }
262        let tag = u16::from_be_bytes(self.read_array::<2>()?);
263        if tag == 0 {
264            return Err(PrikkError::MalformedData(
265                "field tag 0 is reserved".to_string(),
266            ));
267        }
268        if let Some(last) = self.last_tag {
269            if tag < last {
270                return Err(PrikkError::MalformedData(format!(
271                    "field tag order violation: {tag} after {last}"
272                )));
273            }
274        }
275        self.last_tag = Some(tag);
276        let wire_type = self.read_u8()?;
277        let len = usize::try_from(u64::from_be_bytes(self.read_array::<8>()?)).map_err(|_| {
278            PrikkError::MalformedData("canonical field length does not fit usize".to_string())
279        })?;
280        let value = self.read_exact(len)?;
281        Ok(Some(CanonicalField {
282            tag,
283            wire_type,
284            value,
285        }))
286    }
287
288    fn read_u8(&mut self) -> Result<u8> {
289        let value = self.read_exact(1)?;
290        let Some(byte) = value.first() else {
291            return Err(PrikkError::MalformedData(
292                "unexpected empty byte".to_string(),
293            ));
294        };
295        Ok(*byte)
296    }
297
298    fn read_array<const N: usize>(&mut self) -> Result<[u8; N]> {
299        let bytes = self.read_exact(N)?;
300        let mut out = [0_u8; N];
301        out.copy_from_slice(bytes);
302        Ok(out)
303    }
304
305    fn read_exact(&mut self, len: usize) -> Result<&'a [u8]> {
306        let end = self
307            .pos
308            .checked_add(len)
309            .ok_or_else(|| PrikkError::MalformedData("canonical range overflow".to_string()))?;
310        let Some(slice) = self.bytes.get(self.pos..end) else {
311            return Err(PrikkError::MalformedData(
312                "unexpected end of canonical payload".to_string(),
313            ));
314        };
315        self.pos = end;
316        Ok(slice)
317    }
318}
319
320struct CanonicalField<'a> {
321    tag: u16,
322    wire_type: u8,
323    value: &'a [u8],
324}
325
326impl<'a> CanonicalField<'a> {
327    fn read_string(&self) -> Result<String> {
328        self.require_wire(WireType::String)?;
329        String::from_utf8(self.value.to_vec())
330            .map_err(|err| PrikkError::MalformedData(format!("invalid UTF-8 string: {err}")))
331    }
332
333    fn read_u64(&self) -> Result<u64> {
334        self.require_wire(WireType::U64)?;
335        Ok(u64::from_be_bytes(self.read_array::<8>()?))
336    }
337
338    fn read_object_id(&self) -> Result<ObjectId> {
339        self.require_wire(WireType::ObjectId)?;
340        Ok(ObjectId::from_bytes(self.read_array::<32>()?))
341    }
342
343    fn read_enum_u16(&self) -> Result<u16> {
344        self.require_wire(WireType::EnumU16)?;
345        Ok(u16::from_be_bytes(self.read_array::<2>()?))
346    }
347
348    fn read_bool(&self) -> Result<bool> {
349        self.require_wire(WireType::Bool)?;
350        match self.read_array::<1>()?[0] {
351            0 => Ok(false),
352            1 => Ok(true),
353            other => Err(PrikkError::MalformedData(format!(
354                "field {} has invalid bool byte: {other}",
355                self.tag
356            ))),
357        }
358    }
359
360    fn require_wire(&self, expected: WireType) -> Result<()> {
361        if self.wire_type == expected as u8 {
362            return Ok(());
363        }
364        Err(PrikkError::MalformedData(format!(
365            "field {} has wrong wire type: expected {}, got {}",
366            self.tag, expected as u8, self.wire_type
367        )))
368    }
369
370    fn read_array<const N: usize>(&self) -> Result<[u8; N]> {
371        if self.value.len() != N {
372            return Err(PrikkError::MalformedData(format!(
373                "field {} expected {N} bytes, got {}",
374                self.tag,
375                self.value.len()
376            )));
377        }
378        let mut out = [0_u8; N];
379        out.copy_from_slice(self.value);
380        Ok(out)
381    }
382}