1const LEDGER_PAYLOAD_MAGIC: &[u8; 8] = b"ICMEMLED";
2const LEDGER_PAYLOAD_FORMAT_MARKER: &[u8; 4] = b"ICMF";
3pub const LEDGER_PAYLOAD_FORMAT_VERSION: u32 = 1;
5use crate::constants::LEDGER_PAYLOAD_HEADER_LEN;
6
7#[derive(Clone, Debug, Eq, PartialEq)]
20pub struct LedgerPayloadEnvelope {
21 payload: Vec<u8>,
22}
23
24impl LedgerPayloadEnvelope {
25 #[must_use]
27 pub const fn current(payload: Vec<u8>) -> Self {
28 Self { payload }
29 }
30
31 #[must_use]
38 pub fn encode(&self) -> Vec<u8> {
39 self.try_encode()
40 .expect("payload exceeds the ledger byte ceiling")
41 }
42
43 pub fn try_encode(&self) -> Result<Vec<u8>, LedgerPayloadEnvelopeError> {
45 if self.payload.len() > crate::constants::MAX_LEDGER_BYTES {
46 return Err(LedgerPayloadEnvelopeError::PayloadTooLarge {
47 len: self.payload.len() as u64,
48 });
49 }
50 let total_len = LEDGER_PAYLOAD_HEADER_LEN + self.payload.len();
52 let mut bytes = Vec::with_capacity(total_len);
53 bytes.extend_from_slice(&encoded_header(self.payload.len()));
54 bytes.extend_from_slice(&self.payload);
55 Ok(bytes)
56 }
57
58 pub(super) fn encode_ledger(
61 ledger: &super::AllocationLedger,
62 ) -> Result<Vec<u8>, super::LedgerIntegrityError> {
63 let mut writer = LedgerWriter(vec![0; LEDGER_PAYLOAD_HEADER_LEN]);
64 match ciborium::into_writer(ledger, &mut writer) {
65 Ok(()) => (),
66 Err(ciborium::ser::Error::Io(err)) if err.kind() == std::io::ErrorKind::WriteZero => {
67 return Err(super::LedgerIntegrityError::LimitExceeded {
68 resource: "ledger bytes",
69 limit: crate::constants::MAX_LEDGER_BYTES,
70 });
71 }
72 Err(err) => panic!("allocation ledger serialization failed: {err}"),
74 }
75 let mut bytes = writer.0;
76 let payload_len = bytes.len() - LEDGER_PAYLOAD_HEADER_LEN;
77 bytes[..LEDGER_PAYLOAD_HEADER_LEN].copy_from_slice(&encoded_header(payload_len));
78 Ok(bytes)
79 }
80
81 pub fn decode(bytes: &[u8]) -> Result<Self, LedgerPayloadEnvelopeError> {
83 Ok(Self {
84 payload: Self::decode_payload(bytes)?.to_vec(),
85 })
86 }
87
88 pub(super) fn decode_payload(bytes: &[u8]) -> Result<&[u8], LedgerPayloadEnvelopeError> {
91 let Some(magic) = bytes.get(0..8).and_then(|bytes| bytes.try_into().ok()) else {
92 return Err(LedgerPayloadEnvelopeError::Truncated {
93 actual: bytes.len(),
94 minimum: LEDGER_PAYLOAD_HEADER_LEN,
95 });
96 };
97 if &magic != LEDGER_PAYLOAD_MAGIC {
98 return Err(LedgerPayloadEnvelopeError::BadMagic { found: magic });
99 }
100
101 let Some(format_marker) = bytes.get(8..12).and_then(|bytes| bytes.try_into().ok()) else {
102 return Err(LedgerPayloadEnvelopeError::Truncated {
103 actual: bytes.len(),
104 minimum: LEDGER_PAYLOAD_HEADER_LEN,
105 });
106 };
107 if &format_marker != LEDGER_PAYLOAD_FORMAT_MARKER {
108 return Err(LedgerPayloadEnvelopeError::UnsupportedFormat {
109 marker: format_marker,
110 version: None,
111 });
112 }
113
114 let Some(format_version) = bytes.get(12..16).and_then(|bytes| bytes.try_into().ok()) else {
115 return Err(LedgerPayloadEnvelopeError::Truncated {
116 actual: bytes.len(),
117 minimum: LEDGER_PAYLOAD_HEADER_LEN,
118 });
119 };
120 let format_version = u32::from_le_bytes(format_version);
121 if format_version != LEDGER_PAYLOAD_FORMAT_VERSION {
122 return Err(LedgerPayloadEnvelopeError::UnsupportedFormat {
123 marker: format_marker,
124 version: Some(format_version),
125 });
126 }
127
128 let Some(payload_len) = bytes.get(16..24).and_then(|bytes| bytes.try_into().ok()) else {
129 return Err(LedgerPayloadEnvelopeError::Truncated {
130 actual: bytes.len(),
131 minimum: LEDGER_PAYLOAD_HEADER_LEN,
132 });
133 };
134 let payload_len = u64::from_le_bytes(payload_len);
135 let payload_len = usize::try_from(payload_len)
136 .map_err(|_| LedgerPayloadEnvelopeError::PayloadTooLarge { len: payload_len })?;
137 if payload_len > crate::constants::MAX_LEDGER_BYTES {
138 return Err(LedgerPayloadEnvelopeError::PayloadTooLarge {
139 len: payload_len as u64,
140 });
141 }
142 let expected_len = LEDGER_PAYLOAD_HEADER_LEN + payload_len;
143 if bytes.len() != expected_len {
144 return Err(LedgerPayloadEnvelopeError::LengthMismatch {
145 declared: payload_len,
146 actual: bytes.len().saturating_sub(LEDGER_PAYLOAD_HEADER_LEN),
147 });
148 }
149
150 Ok(&bytes[LEDGER_PAYLOAD_HEADER_LEN..])
151 }
152
153 #[must_use]
155 pub fn payload(&self) -> &[u8] {
156 &self.payload
157 }
158}
159
160struct LedgerWriter(Vec<u8>);
164
165impl std::io::Write for LedgerWriter {
166 fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
167 self.write_all(bytes)?;
168 Ok(bytes.len())
169 }
170
171 #[inline]
174 fn write_all(&mut self, bytes: &[u8]) -> std::io::Result<()> {
175 let limit = crate::constants::MAX_COMMITTED_PAYLOAD_BYTES;
176 if bytes.len() > limit - self.0.len() {
177 return Err(std::io::ErrorKind::WriteZero.into());
178 }
179 let required = self.0.len() + bytes.len();
180 if required > self.0.capacity() {
181 let capacity = self.0.capacity().saturating_mul(2).max(required).min(limit);
182 self.0.reserve_exact(capacity - self.0.len());
183 }
184 self.0.extend_from_slice(bytes);
185 Ok(())
186 }
187
188 fn flush(&mut self) -> std::io::Result<()> {
189 Ok(())
190 }
191}
192
193fn encoded_header(payload_len: usize) -> [u8; LEDGER_PAYLOAD_HEADER_LEN] {
195 let mut header = [0; LEDGER_PAYLOAD_HEADER_LEN];
196 header[..8].copy_from_slice(LEDGER_PAYLOAD_MAGIC);
197 header[8..12].copy_from_slice(LEDGER_PAYLOAD_FORMAT_MARKER);
198 header[12..16].copy_from_slice(&LEDGER_PAYLOAD_FORMAT_VERSION.to_le_bytes());
199 header[16..24].copy_from_slice(&(payload_len as u64).to_le_bytes());
200 header
201}
202
203#[non_exhaustive]
210#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
211pub enum LedgerPayloadEnvelopeError {
212 #[error("ledger payload envelope is truncated: {actual} bytes, need at least {minimum}")]
214 Truncated {
215 actual: usize,
217 minimum: usize,
219 },
220 #[error("ledger payload envelope has bad magic {found:?}")]
222 BadMagic {
223 found: [u8; 8],
225 },
226 #[error("unsupported ic-memory ledger payload format (marker={marker:?}, version={version:?})")]
229 UnsupportedFormat {
230 marker: [u8; 4],
232 version: Option<u32>,
234 },
235 #[error("ledger payload envelope length {len} is too large")]
238 PayloadTooLarge {
239 len: u64,
241 },
242 #[error("ledger payload envelope declared {declared} payload bytes but contained {actual}")]
244 LengthMismatch {
245 declared: usize,
247 actual: usize,
249 },
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255
256 #[test]
257 fn ledger_writer_admits_exact_ceiling_and_refuses_without_partial_append() {
258 use std::io::Write;
259
260 let mut writer = LedgerWriter(vec![0; LEDGER_PAYLOAD_HEADER_LEN]);
261 let chunk = [42; 4096];
262 for _ in 0..crate::constants::MAX_LEDGER_BYTES / chunk.len() {
263 writer.write_all(&chunk).unwrap();
264 }
265 assert_eq!(
266 writer.0.len(),
267 crate::constants::MAX_COMMITTED_PAYLOAD_BYTES
268 );
269 writer.write_all(&[]).unwrap();
270 assert_eq!(
271 writer.write(&[42]).unwrap_err().kind(),
272 std::io::ErrorKind::WriteZero
273 );
274 assert_eq!(
275 writer.0.len(),
276 crate::constants::MAX_COMMITTED_PAYLOAD_BYTES
277 );
278 assert!(
279 writer.0[LEDGER_PAYLOAD_HEADER_LEN..]
280 .iter()
281 .all(|&byte| byte == 42)
282 );
283
284 let mut writer = LedgerWriter(vec![0; LEDGER_PAYLOAD_HEADER_LEN]);
285 let before = writer.0.clone();
286 assert!(
287 writer
288 .write(&vec![42; crate::constants::MAX_LEDGER_BYTES + 1])
289 .is_err()
290 );
291 assert_eq!(writer.0, before);
292 }
293
294 #[test]
295 fn bounded_ledger_encoding_matches_canonical_cbor() {
296 for generations in [0, 1, 24, 256, 1024] {
297 let ledger = super::super::AllocationLedger {
298 current_generation: generations,
299 allocation_history: super::super::AllocationHistory::from_parts(
300 Vec::new(),
301 (1..=generations)
302 .map(|generation| super::super::GenerationRecord {
303 generation,
304 parent_generation: generation - 1,
305 runtime_fingerprint: Some(
306 "x".repeat(usize::try_from(generation % 256 + 1).unwrap()),
307 ),
308 declaration_count: 0,
309 committed_at: Some(generation),
310 })
311 .collect(),
312 ),
313 };
314 let mut payload = Vec::new();
315 ciborium::into_writer(&ledger, &mut payload).unwrap();
316 let expected = LedgerPayloadEnvelope::current(payload)
317 .try_encode()
318 .unwrap();
319 assert_eq!(
320 LedgerPayloadEnvelope::encode_ledger(&ledger).unwrap(),
321 expected
322 );
323 }
324 }
325
326 #[test]
327 fn oversized_payload_is_rejected_before_encoding() {
328 let len = crate::constants::MAX_LEDGER_BYTES + 1;
329 assert_eq!(
330 LedgerPayloadEnvelope::current(vec![0; len]).try_encode(),
331 Err(LedgerPayloadEnvelopeError::PayloadTooLarge { len: len as u64 })
332 );
333 }
334
335 #[test]
336 fn malformed_lengths_are_classified_before_payload_copy() {
337 let mut bytes = LedgerPayloadEnvelope::current(Vec::new()).encode();
338 for len in 0..LEDGER_PAYLOAD_HEADER_LEN {
339 assert_eq!(
340 LedgerPayloadEnvelope::decode(&bytes[..len]),
341 Err(LedgerPayloadEnvelopeError::Truncated {
342 actual: len,
343 minimum: LEDGER_PAYLOAD_HEADER_LEN,
344 })
345 );
346 }
347 for len in [crate::constants::MAX_LEDGER_BYTES as u64 + 1, u64::MAX] {
348 bytes[16..24].copy_from_slice(&len.to_le_bytes());
349 assert_eq!(
350 LedgerPayloadEnvelope::decode(&bytes),
351 Err(LedgerPayloadEnvelopeError::PayloadTooLarge { len })
352 );
353 }
354 bytes[16..24].copy_from_slice(&1_u64.to_le_bytes());
355 assert_eq!(
356 LedgerPayloadEnvelope::decode(&bytes),
357 Err(LedgerPayloadEnvelopeError::LengthMismatch {
358 declared: 1,
359 actual: 0,
360 })
361 );
362 bytes[16..24].copy_from_slice(&0_u64.to_le_bytes());
363 bytes.push(42);
364 assert_eq!(
365 LedgerPayloadEnvelope::decode(&bytes),
366 Err(LedgerPayloadEnvelopeError::LengthMismatch {
367 declared: 0,
368 actual: 1,
369 })
370 );
371 }
372
373 #[test]
374 fn borrowed_payload_decode_shares_storage_through_the_byte_ceiling() {
375 for len in [0, 3, crate::constants::MAX_LEDGER_BYTES] {
376 let bytes = LedgerPayloadEnvelope::current(vec![42; len])
377 .try_encode()
378 .expect("bounded envelope");
379 let payload = LedgerPayloadEnvelope::decode_payload(&bytes).expect("valid envelope");
380
381 assert_eq!(payload.len(), len);
382 assert_eq!(
383 payload.as_ptr(),
384 bytes[LEDGER_PAYLOAD_HEADER_LEN..].as_ptr()
385 );
386 assert!(payload.iter().all(|byte| *byte == 42));
387 }
388 }
389}