Skip to main content

prikk_object/
envelope.rs

1//! Object envelope.
2
3use std::cmp::Ordering;
4
5use prikk_error::{PrikkError, Result};
6
7use crate::{CanonicalEncode, CanonicalWriter, ObjectId, ObjectType, Signature};
8
9/// Non-canonical signature conditions visible during structural legacy decoding.
10#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
11pub struct SignatureEnvelopeIssues {
12    /// At least one signature has a shape invalid for its registered algorithm.
13    pub malformed_shape: bool,
14    /// At least two signatures have the same canonical tuple.
15    pub duplicate: bool,
16    /// At least one non-equal adjacent tuple is descending.
17    pub noncanonical_order: bool,
18}
19
20/// Object envelope containing unsigned canonical payload bytes plus external signatures.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct ObjectEnvelope {
23    /// Object type.
24    pub object_type: ObjectType,
25    /// Schema version.
26    pub schema_version: u32,
27    /// Unsigned canonical payload bytes.
28    pub canonical_payload: Vec<u8>,
29    /// Signatures over the object ID. Signatures are not part of the object ID preimage.
30    pub signatures: Vec<Signature>,
31}
32
33impl ObjectEnvelope {
34    /// Construct a new unsigned envelope.
35    #[must_use]
36    pub fn unsigned(
37        object_type: ObjectType,
38        schema_version: u32,
39        canonical_payload: Vec<u8>,
40    ) -> Self {
41        Self {
42            object_type,
43            schema_version,
44            canonical_payload,
45            signatures: Vec::new(),
46        }
47    }
48
49    /// Compute this envelope's object ID from its unsigned payload.
50    #[must_use]
51    pub fn object_id(&self) -> ObjectId {
52        ObjectId::from_canonical_payload(
53            self.object_type,
54            self.schema_version,
55            &self.canonical_payload,
56        )
57    }
58
59    /// Validate envelope metadata and signatures structurally for legacy decoding.
60    pub fn validate(&self) -> Result<()> {
61        if self.schema_version == 0 {
62            return Err(PrikkError::UnsupportedFormatVersion(0));
63        }
64        for signature in &self.signatures {
65            signature.validate()?;
66        }
67        Ok(())
68    }
69
70    /// Classify signature conditions after structural validation without changing the envelope.
71    pub fn signature_issues(&self) -> Result<SignatureEnvelopeIssues> {
72        self.validate()?;
73        let mut issues = SignatureEnvelopeIssues {
74            malformed_shape: self
75                .signatures
76                .iter()
77                .any(|signature| signature.validate_shape().is_err()),
78            ..SignatureEnvelopeIssues::default()
79        };
80        let mut signatures_by_tuple = self.signatures.iter().collect::<Vec<_>>();
81        signatures_by_tuple.sort_unstable_by(|left, right| left.canonical_cmp(right));
82        issues.duplicate = signatures_by_tuple
83            .windows(2)
84            .any(|pair| matches!(pair, [left, right] if left.canonical_cmp(right).is_eq()));
85        for pair in self.signatures.windows(2) {
86            let [left, right] = pair else {
87                continue;
88            };
89            match left.canonical_cmp(right) {
90                Ordering::Equal => {}
91                Ordering::Greater => issues.noncanonical_order = true,
92                Ordering::Less => {}
93            }
94        }
95        Ok(issues)
96    }
97
98    /// Validate the envelope for canonical serialization, new writes, and current-format reads.
99    pub fn validate_strict(&self) -> Result<()> {
100        let issues = self.signature_issues()?;
101        if issues.malformed_shape {
102            return Err(PrikkError::InvalidSignature(
103                "envelope contains a signature with malformed algorithm shape".to_string(),
104            ));
105        }
106        if issues.duplicate {
107            return Err(PrikkError::InvalidSignature(
108                "envelope contains a duplicate signature tuple".to_string(),
109            ));
110        }
111        if issues.noncanonical_order {
112            return Err(PrikkError::InvalidSignature(
113                "envelope signatures are not in canonical order".to_string(),
114            ));
115        }
116        Ok(())
117    }
118
119    /// Append a signature without changing the object ID.
120    pub fn add_signature(&mut self, signature: Signature) -> Result<()> {
121        self.validate_strict()?;
122        signature.validate()?;
123        signature.validate_shape()?;
124        match self
125            .signatures
126            .binary_search_by(|existing| existing.canonical_cmp(&signature))
127        {
128            Ok(_) => Err(PrikkError::InvalidSignature(
129                "envelope contains a duplicate signature tuple".to_string(),
130            )),
131            Err(index) => {
132                self.signatures.insert(index, signature);
133                Ok(())
134            }
135        }
136    }
137}
138
139impl CanonicalEncode for ObjectEnvelope {
140    fn encode_canonical(&self, writer: &mut CanonicalWriter) -> Result<()> {
141        self.validate_strict()?;
142        writer.field_u32(1, self.object_type.code() as u32)?;
143        writer.field_u32(2, self.schema_version)?;
144        writer.field_bytes(3, &self.canonical_payload)?;
145        writer.repeated_record(4, &self.signatures)?;
146        Ok(())
147    }
148}
149
150#[cfg(test)]
151mod tests;