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