Skip to main content

cpop_protocol/codec/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Codec module for RFC-compliant serialization.
4//!
5//! Supports both CBOR (primary, RFC 8949) and JSON (legacy) encoding
6//! for Proof-of-Process evidence packets.
7
8pub mod cbor;
9pub mod json;
10
11use serde::{de::DeserializeOwned, Serialize};
12use std::io::{Read, Write};
13
14/// CBOR semantic tag for Compact Proof-of-Process (CPOP) evidence packet.
15/// Tag value: 1129336656 (0x43504F50 = "CPOP" in ASCII)
16/// Per draft-condrey-rats-pop CDDL and IANA CBOR tag registry.
17pub const CBOR_TAG_CPOP: u64 = 1129336656;
18
19/// CBOR semantic tag for Compact Writers Attestation Result (CWAR).
20/// Tag value: 1129791826 (0x43574152 = "CWAR" in ASCII)
21/// Per draft-condrey-rats-pop CDDL and IANA CBOR tag registry.
22pub const CBOR_TAG_CWAR: u64 = 1129791826;
23
24/// CBOR semantic tag for Compact Evidence Reference.
25/// Tag value: 1129336657 (0x43504F51 = "CPOQ")
26pub const CBOR_TAG_COMPACT_REF: u64 = 1129336657;
27
28/// IANA Private Enterprise Number for WritersLogic Inc.
29pub const IANA_PEN: u32 = 65074;
30
31/// Wire serialization format selector.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
33pub enum Format {
34    /// CBOR encoding for evidence packets (RFC 8949 deterministic)
35    #[default]
36    Cbor,
37    /// CBOR encoding for attestation results (CWAR)
38    CborWar,
39    /// JSON encoding (legacy, for human readability)
40    Json,
41}
42
43impl Format {
44    /// Return the MIME type for this format.
45    pub fn mime_type(&self) -> &'static str {
46        match self {
47            Format::Cbor => "application/cpop+cbor",
48            Format::CborWar => "application/cwar+cbor",
49            Format::Json => "application/json",
50        }
51    }
52
53    /// Return the file extension for this format.
54    pub fn extension(&self) -> &'static str {
55        match self {
56            Format::Cbor => "cpop",
57            Format::CborWar => "cwar",
58            Format::Json => "json",
59        }
60    }
61
62    /// Detect format from the first byte of encoded data.
63    pub fn detect(data: &[u8]) -> Option<Self> {
64        if data.is_empty() {
65            return None;
66        }
67        // CBOR map starts with 0xA (major type 5) or tagged value 0xD9/0xDA/0xDB
68        // JSON starts with '{' (0x7B) or '[' (0x5B)
69        match data[0] {
70            0x7B | 0x5B => Some(Format::Json),
71            0xA0..=0xBF | 0xD9 | 0xDA | 0xDB => Some(Format::Cbor),
72            _ => None,
73        }
74    }
75}
76
77/// Encoding/decoding errors for CBOR and JSON codecs.
78#[derive(Debug, thiserror::Error)]
79pub enum CodecError {
80    #[error("CBOR encoding error: {0}")]
81    CborEncode(String),
82    #[error("CBOR decoding error: {0}")]
83    CborDecode(String),
84    #[error("JSON encoding error: {0}")]
85    JsonEncode(String),
86    #[error("JSON decoding error: {0}")]
87    JsonDecode(String),
88    #[error("io error: {0}")]
89    Io(#[from] std::io::Error),
90    #[error("invalid format: {0}")]
91    InvalidFormat(String),
92    #[error("missing semantic tag")]
93    MissingTag,
94    #[error("invalid semantic tag: expected {expected}, got {actual}")]
95    InvalidTag { expected: u64, actual: u64 },
96    #[error("validation error: {0}")]
97    Validation(String),
98}
99
100pub type Result<T> = std::result::Result<T, CodecError>;
101
102/// Serialize a value in the specified format.
103pub fn encode<T: Serialize>(value: &T, format: Format) -> Result<Vec<u8>> {
104    match format {
105        Format::Cbor | Format::CborWar => cbor::encode(value),
106        Format::Json => json::encode(value),
107    }
108}
109
110/// Deserialize a value from the specified format.
111pub fn decode<T: DeserializeOwned>(data: &[u8], format: Format) -> Result<T> {
112    match format {
113        Format::Cbor | Format::CborWar => cbor::decode(data),
114        Format::Json => json::decode(data),
115    }
116}
117
118/// Auto-detect format and deserialize.
119pub fn decode_auto<T: DeserializeOwned>(data: &[u8]) -> Result<T> {
120    let format = Format::detect(data)
121        .ok_or_else(|| CodecError::InvalidFormat("unable to detect format".to_string()))?;
122    decode(data, format)
123}
124
125/// Serialize a value into a writer in the specified format.
126pub fn encode_to<T: Serialize, W: Write>(value: &T, writer: W, format: Format) -> Result<()> {
127    match format {
128        Format::Cbor | Format::CborWar => cbor::encode_to(value, writer),
129        Format::Json => json::encode_to(value, writer),
130    }
131}
132
133/// Deserialize a value from a reader in the specified format.
134pub fn decode_from<T: DeserializeOwned, R: Read>(reader: R, format: Format) -> Result<T> {
135    match format {
136        Format::Cbor | Format::CborWar => cbor::decode_from(reader),
137        Format::Json => json::decode_from(reader),
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use serde::{Deserialize, Serialize};
145
146    #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
147    struct TestStruct {
148        name: String,
149        value: i32,
150        data: Vec<u8>,
151    }
152
153    #[test]
154    fn test_format_detection() {
155        // JSON object
156        assert_eq!(Format::detect(b"{\"key\":\"value\"}"), Some(Format::Json));
157        // JSON array
158        assert_eq!(Format::detect(b"[1,2,3]"), Some(Format::Json));
159        // CBOR map (small, 0xA0-0xB7)
160        assert_eq!(Format::detect(&[0xA1, 0x01, 0x02]), Some(Format::Cbor));
161        // Empty
162        assert_eq!(Format::detect(&[]), None);
163    }
164
165    #[test]
166    fn test_roundtrip_cbor() {
167        let original = TestStruct {
168            name: "test".to_string(),
169            value: 42,
170            data: vec![1, 2, 3, 4],
171        };
172
173        let encoded = encode(&original, Format::Cbor).unwrap();
174        let decoded: TestStruct = decode(&encoded, Format::Cbor).unwrap();
175
176        assert_eq!(original, decoded);
177    }
178
179    #[test]
180    fn test_roundtrip_json() {
181        let original = TestStruct {
182            name: "test".to_string(),
183            value: 42,
184            data: vec![1, 2, 3, 4],
185        };
186
187        let encoded = encode(&original, Format::Json).unwrap();
188        let decoded: TestStruct = decode(&encoded, Format::Json).unwrap();
189
190        assert_eq!(original, decoded);
191    }
192
193    #[test]
194    fn test_auto_detect_decode() {
195        let original = TestStruct {
196            name: "auto".to_string(),
197            value: 100,
198            data: vec![5, 6, 7],
199        };
200
201        let cbor_encoded = encode(&original, Format::Cbor).unwrap();
202        let cbor_decoded: TestStruct = decode_auto(&cbor_encoded).unwrap();
203        assert_eq!(original, cbor_decoded);
204
205        let json_encoded = encode(&original, Format::Json).unwrap();
206        let json_decoded: TestStruct = decode_auto(&json_encoded).unwrap();
207        assert_eq!(original, json_decoded);
208    }
209
210    #[test]
211    fn test_format_mime_type() {
212        assert_eq!(Format::Cbor.mime_type(), "application/cpop+cbor");
213        assert_eq!(Format::CborWar.mime_type(), "application/cwar+cbor");
214        assert_eq!(Format::Json.mime_type(), "application/json");
215    }
216
217    #[test]
218    fn test_format_extension() {
219        assert_eq!(Format::Cbor.extension(), "cpop");
220        assert_eq!(Format::CborWar.extension(), "cwar");
221        assert_eq!(Format::Json.extension(), "json");
222    }
223
224    #[test]
225    fn test_format_default_is_cbor() {
226        assert_eq!(Format::default(), Format::Cbor);
227    }
228
229    #[test]
230    fn test_format_detect_tagged_cbor() {
231        // 0xD9 = 2-byte tag header (CBOR major type 6)
232        assert_eq!(Format::detect(&[0xD9, 0x01, 0x02]), Some(Format::Cbor));
233        // 0xDA = 4-byte tag header
234        assert_eq!(
235            Format::detect(&[0xDA, 0x00, 0x00, 0x00, 0x01]),
236            Some(Format::Cbor)
237        );
238        // 0xDB = 8-byte tag header
239        assert_eq!(Format::detect(&[0xDB]), Some(Format::Cbor));
240    }
241
242    #[test]
243    fn test_format_detect_unknown_byte() {
244        // A byte that doesn't match JSON or CBOR patterns
245        assert_eq!(Format::detect(&[0x00]), None);
246        assert_eq!(Format::detect(&[0x42]), None);
247        assert_eq!(Format::detect(&[0xFF]), None);
248    }
249
250    #[test]
251    fn test_decode_auto_empty_data() {
252        let result = decode_auto::<TestStruct>(&[]);
253        assert!(matches!(result, Err(CodecError::InvalidFormat(_))));
254    }
255
256    #[test]
257    fn test_encode_to_decode_from_cbor() {
258        let original = TestStruct {
259            name: "writer".to_string(),
260            value: -7,
261            data: vec![0xFF, 0x00],
262        };
263
264        let mut buf = Vec::new();
265        encode_to(&original, &mut buf, Format::Cbor).unwrap();
266        let decoded: TestStruct = decode_from(&buf[..], Format::Cbor).unwrap();
267        assert_eq!(original, decoded);
268    }
269
270    #[test]
271    fn test_encode_to_decode_from_json() {
272        let original = TestStruct {
273            name: "writer".to_string(),
274            value: -7,
275            data: vec![0xFF, 0x00],
276        };
277
278        let mut buf = Vec::new();
279        encode_to(&original, &mut buf, Format::Json).unwrap();
280        let decoded: TestStruct = decode_from(&buf[..], Format::Json).unwrap();
281        assert_eq!(original, decoded);
282    }
283}
284
285// Backward-compatible convenience functions from v0.1
286/// Alias for engine-era constant names.
287pub const CBOR_TAG_EVIDENCE_PACKET: u64 = CBOR_TAG_CPOP;
288/// Alias for engine-era constant names.
289pub const CBOR_TAG_ATTESTATION_RESULT: u64 = CBOR_TAG_CWAR;
290
291/// Serialize an EvidencePacket to CBOR with the registered tag.
292pub fn encode_evidence(packet: &crate::rfc::EvidencePacket) -> crate::error::Result<Vec<u8>> {
293    cbor::encode_cpop(packet).map_err(|e| crate::error::Error::Serialization(e.to_string()))
294}
295
296/// Deserialize CBOR-tagged bytes into an EvidencePacket.
297pub fn decode_evidence(bytes: &[u8]) -> crate::error::Result<crate::rfc::EvidencePacket> {
298    cbor::decode_cpop(bytes).map_err(|e| crate::error::Error::Serialization(e.to_string()))
299}
300
301/// Serialize an AttestationResult to CBOR with the registered tag.
302pub fn encode_attestation(result: &crate::rfc::AttestationResult) -> crate::error::Result<Vec<u8>> {
303    cbor::encode_cwar(result).map_err(|e| crate::error::Error::Serialization(e.to_string()))
304}
305
306/// Deserialize CBOR-tagged bytes into an AttestationResult.
307pub fn decode_attestation(bytes: &[u8]) -> crate::error::Result<crate::rfc::AttestationResult> {
308    cbor::decode_cwar(bytes).map_err(|e| crate::error::Error::Serialization(e.to_string()))
309}