Skip to main content

capsule_lib/
capsule.rs

1// SPDX-FileCopyrightText: 2026 Alexander R. Croft
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4use crate::ascii_header::{encode_ascii_header_kv, parse_ascii_header_kv, HeaderField};
5use crate::crc::compute_crc32_iso_hdlc;
6use crate::encoding::Encoding;
7use crate::error::{CapsuleError, CapsuleResult};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
10pub struct Version(pub u16);
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct Prelude {
14    pub version: Version,
15    pub encoding: Encoding,
16    pub header_len: u16,
17    pub body_crc: u32,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct Capsule {
22    pub prelude: Prelude,
23    pub header_encoded: Vec<u8>,
24    pub payload_encoded: Vec<u8>,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct CapsuleDecoded {
29    pub prelude: Prelude,
30    pub header_encoded: Vec<u8>,
31    pub payload_encoded: Vec<u8>,
32    pub header_decoded: Vec<u8>,
33    pub payload_decoded: Vec<u8>,
34    pub header_fields: Option<Vec<HeaderField>>,
35}
36
37#[derive(Debug, Clone, Copy)]
38pub struct ParseOptions {
39    pub verify_crc: bool,
40    pub validate_encoding: bool,
41}
42
43impl Default for ParseOptions {
44    fn default() -> Self {
45        Self { verify_crc: true, validate_encoding: true }
46    }
47}
48
49fn parse_upper_hex_u16(bytes: &[u8]) -> Option<u16> {
50    if bytes.len() != 4 { return None; }
51    let mut v: u16 = 0;
52    for &b in bytes {
53        let d = match b {
54            b'0'..=b'9' => b - b'0',
55            b'A'..=b'F' => b - b'A' + 10,
56            _ => return None,
57        };
58        v = (v << 4) | d as u16;
59    }
60    Some(v)
61}
62
63fn parse_upper_hex_u32(bytes: &[u8]) -> Option<u32> {
64    if bytes.len() != 8 { return None; }
65    let mut v: u32 = 0;
66    for &b in bytes {
67        let d = match b {
68            b'0'..=b'9' => b - b'0',
69            b'A'..=b'F' => b - b'A' + 10,
70            _ => return None,
71        };
72        v = (v << 4) | d as u32;
73    }
74    Some(v)
75}
76
77fn ensure_ascii(which: &'static str, bytes: &[u8]) -> CapsuleResult<()> {
78    for (i, &b) in bytes.iter().enumerate() {
79        if b > 0x7F {
80            return Err(CapsuleError::NonAsciiByte { which, offset: i });
81        }
82    }
83    Ok(())
84}
85
86fn decode_base64(which: &'static str, bytes: &[u8]) -> CapsuleResult<Vec<u8>> {
87    // data-encoding rejects non-alphabet chars including whitespace and enforces RFC4648 padding.
88    data_encoding::BASE64
89        .decode(bytes)
90        .map_err(|e| CapsuleError::InvalidBase64 { which, message: e.to_string() })
91}
92
93fn validate_cbor(which: &'static str, bytes: &[u8]) -> CapsuleResult<()> {
94    use serde::de::IgnoredAny;
95
96    if bytes.is_empty() {
97        return Ok(());
98    }
99
100    // Accept a CBOR sequence (0..N concatenated data items). This performs
101    // well-formedness validation only and does not impose any schema.
102    let mut it = serde_cbor::Deserializer::from_slice(bytes).into_iter::<IgnoredAny>();
103    while let Some(item) = it.next() {
104        item.map_err(|e| CapsuleError::InvalidCbor {
105            which,
106            message: e.to_string(),
107        })?;
108    }
109
110    if it.byte_offset() != bytes.len() {
111        return Err(CapsuleError::InvalidCbor {
112            which,
113            message: "trailing bytes after CBOR sequence".to_string(),
114        });
115    }
116
117    Ok(())
118}
119
120impl Capsule {
121    pub fn parse(bytes: &[u8]) -> CapsuleResult<CapsuleDecoded> {
122        Self::parse_with_options(bytes, ParseOptions::default())
123    }
124
125    pub fn parse_with_options(bytes: &[u8], options: ParseOptions) -> CapsuleResult<CapsuleDecoded> {
126        if bytes.len() < 24 {
127            return Err(CapsuleError::FileTooShort(bytes.len()));
128        }
129
130        if &bytes[..7] != b"CAPSULE" {
131            return Err(CapsuleError::InvalidMagic);
132        }
133
134        let version_u16 = parse_upper_hex_u16(&bytes[7..11]).ok_or(CapsuleError::InvalidVersionField)?;
135        if version_u16 == 0 {
136            return Err(CapsuleError::ReservedVersion);
137        }
138
139        let encoding = Encoding::from_byte(bytes[11])?;
140        let header_len = parse_upper_hex_u16(&bytes[12..16]).ok_or(CapsuleError::InvalidHeaderLengthField)?;
141        let body_crc = parse_upper_hex_u32(&bytes[16..24]).ok_or(CapsuleError::InvalidBodyCrcField)?;
142
143        let header_len_usize = header_len as usize;
144        let available = bytes.len() - 24;
145        if header_len_usize > available {
146            return Err(CapsuleError::HeaderLengthExceedsAvailable { declared: header_len_usize, available });
147        }
148
149        let header_start = 24;
150        let header_end = 24 + header_len_usize;
151        let header_encoded = bytes[header_start..header_end].to_vec();
152        let payload_encoded = bytes[header_end..].to_vec();
153
154        if options.verify_crc {
155            let computed = compute_crc32_iso_hdlc(&bytes[24..]);
156            if computed != body_crc {
157                return Err(CapsuleError::CrcMismatch { declared: body_crc, computed });
158            }
159        }
160
161        let (header_decoded, payload_decoded, header_fields) = match encoding {
162            Encoding::Ascii => {
163                if options.validate_encoding {
164                    ensure_ascii("header", &header_encoded)?;
165                    ensure_ascii("payload", &payload_encoded)?;
166                }
167                let fields = parse_ascii_header_kv(&header_encoded)?;
168                (header_encoded.clone(), payload_encoded.clone(), Some(fields))
169            }
170            Encoding::Base64 => {
171                let header = decode_base64("header", &header_encoded)?;
172                let payload = decode_base64("payload", &payload_encoded)?;
173                (header, payload, None)
174            }
175            Encoding::Cbor => {
176                if options.validate_encoding {
177                    validate_cbor("header", &header_encoded)?;
178                    validate_cbor("payload", &payload_encoded)?;
179                }
180                (header_encoded.clone(), payload_encoded.clone(), None)
181            }
182        };
183
184        Ok(CapsuleDecoded {
185            prelude: Prelude {
186                version: Version(version_u16),
187                encoding,
188                header_len,
189                body_crc,
190            },
191            header_encoded,
192            payload_encoded,
193            header_decoded,
194            payload_decoded,
195            header_fields,
196        })
197    }
198
199    pub fn to_bytes(&self) -> CapsuleResult<Vec<u8>> {
200        let header_len = self.header_encoded.len();
201        if header_len > u16::MAX as usize {
202            return Err(CapsuleError::InvalidHeaderLengthField);
203        }
204
205        let mut out = Vec::with_capacity(24 + header_len + self.payload_encoded.len());
206        out.extend_from_slice(b"CAPSULE");
207
208        let version = self.prelude.version.0;
209        if version == 0 {
210            return Err(CapsuleError::ReservedVersion);
211        }
212
213        out.extend_from_slice(format!("{:04X}", version).as_bytes());
214        out.push(self.prelude.encoding.to_byte());
215        out.extend_from_slice(format!("{:04X}", header_len as u16).as_bytes());
216
217        let mut body = Vec::with_capacity(header_len + self.payload_encoded.len());
218        body.extend_from_slice(&self.header_encoded);
219        body.extend_from_slice(&self.payload_encoded);
220
221        let crc = compute_crc32_iso_hdlc(&body);
222        out.extend_from_slice(format!("{:08X}", crc).as_bytes());
223        out.extend_from_slice(&body);
224
225        Ok(out)
226    }
227
228    pub fn from_decoded(
229        version: Version,
230        encoding: Encoding,
231        header_fields: Option<&[HeaderField]>,
232        header_decoded: &[u8],
233        payload_decoded: &[u8],
234    ) -> CapsuleResult<Self> {
235        let header_encoded = match encoding {
236            Encoding::Ascii => {
237                let fields = header_fields.ok_or_else(|| {
238                    CapsuleError::InvalidAsciiHeader("missing header fields for ASCII encoding".to_string())
239                })?;
240                encode_ascii_header_kv(fields)?
241            }
242            Encoding::Base64 => data_encoding::BASE64.encode(header_decoded).into_bytes(),
243            Encoding::Cbor => {
244                validate_cbor("header", header_decoded)?;
245                header_decoded.to_vec()
246            }
247        };
248
249        let payload_encoded = match encoding {
250            Encoding::Ascii => {
251                ensure_ascii("payload", payload_decoded)?;
252                payload_decoded.to_vec()
253            }
254            Encoding::Base64 => data_encoding::BASE64.encode(payload_decoded).into_bytes(),
255            Encoding::Cbor => {
256                validate_cbor("payload", payload_decoded)?;
257                payload_decoded.to_vec()
258            }
259        };
260
261        Ok(Self {
262            prelude: Prelude {
263                version,
264                encoding,
265                header_len: header_encoded.len() as u16,
266                body_crc: 0,
267            },
268            header_encoded,
269            payload_encoded,
270        })
271    }
272}