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