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