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 = crate::bounded_writer::BoundedWriter::new(
78 vec![0; LEDGER_PAYLOAD_HEADER_LEN],
79 crate::constants::MAX_COMMITTED_PAYLOAD_BYTES,
80 );
81 match ciborium::into_writer(ledger, &mut writer) {
82 Ok(()) => (),
83 Err(ciborium::ser::Error::Io(err)) if err.kind() == std::io::ErrorKind::WriteZero => {
84 return Err(super::LedgerIntegrityError::LimitExceeded {
85 resource: "ledger bytes",
86 limit: crate::constants::MAX_LEDGER_BYTES,
87 });
88 }
89 Err(ciborium::ser::Error::Io(_) | ciborium::ser::Error::Value(_)) => {
91 panic!("allocation ledger serialization failed");
92 }
93 }
94 let mut bytes = writer.bytes;
95 let payload_len = bytes.len() - LEDGER_PAYLOAD_HEADER_LEN;
96 bytes[..LEDGER_PAYLOAD_HEADER_LEN].copy_from_slice(&encoded_header(payload_len));
97 Ok(bytes)
98}
99
100pub(super) fn decode_payload(bytes: &[u8]) -> Result<&[u8], LedgerPayloadEnvelopeError> {
103 let Some(magic) = bytes.get(0..8).and_then(|bytes| bytes.try_into().ok()) else {
104 return Err(LedgerPayloadEnvelopeError::Truncated {
105 actual: bytes.len(),
106 minimum: LEDGER_PAYLOAD_HEADER_LEN,
107 });
108 };
109 if &magic != LEDGER_PAYLOAD_MAGIC {
110 return Err(LedgerPayloadEnvelopeError::BadMagic { found: magic });
111 }
112
113 let Some(format_marker) = bytes.get(8..12).and_then(|bytes| bytes.try_into().ok()) else {
114 return Err(LedgerPayloadEnvelopeError::Truncated {
115 actual: bytes.len(),
116 minimum: LEDGER_PAYLOAD_HEADER_LEN,
117 });
118 };
119 if &format_marker != LEDGER_PAYLOAD_FORMAT_MARKER {
120 return Err(LedgerPayloadEnvelopeError::UnsupportedFormat {
121 marker: format_marker,
122 version: None,
123 });
124 }
125
126 let Some(format_version) = bytes.get(12..16).and_then(|bytes| bytes.try_into().ok()) else {
127 return Err(LedgerPayloadEnvelopeError::Truncated {
128 actual: bytes.len(),
129 minimum: LEDGER_PAYLOAD_HEADER_LEN,
130 });
131 };
132 let format_version = u32::from_le_bytes(format_version);
133 if format_version != LEDGER_PAYLOAD_FORMAT_VERSION {
134 return Err(LedgerPayloadEnvelopeError::UnsupportedFormat {
135 marker: format_marker,
136 version: Some(format_version),
137 });
138 }
139
140 let Some(payload_len) = bytes.get(16..24).and_then(|bytes| bytes.try_into().ok()) else {
141 return Err(LedgerPayloadEnvelopeError::Truncated {
142 actual: bytes.len(),
143 minimum: LEDGER_PAYLOAD_HEADER_LEN,
144 });
145 };
146 let payload_len = u64::from_le_bytes(payload_len);
147 let payload_len = usize::try_from(payload_len)
148 .map_err(|_| LedgerPayloadEnvelopeError::PayloadTooLarge { len: payload_len })?;
149 if payload_len > crate::constants::MAX_LEDGER_BYTES {
150 return Err(LedgerPayloadEnvelopeError::PayloadTooLarge {
151 len: payload_len as u64,
152 });
153 }
154 let expected_len = LEDGER_PAYLOAD_HEADER_LEN + payload_len;
155 if bytes.len() != expected_len {
156 return Err(LedgerPayloadEnvelopeError::LengthMismatch {
157 declared: payload_len,
158 actual: bytes.len().saturating_sub(LEDGER_PAYLOAD_HEADER_LEN),
159 });
160 }
161
162 Ok(&bytes[LEDGER_PAYLOAD_HEADER_LEN..])
163}
164
165fn encoded_header(payload_len: usize) -> [u8; LEDGER_PAYLOAD_HEADER_LEN] {
167 let mut header = [0; LEDGER_PAYLOAD_HEADER_LEN];
168 header[..8].copy_from_slice(LEDGER_PAYLOAD_MAGIC);
169 header[8..12].copy_from_slice(LEDGER_PAYLOAD_FORMAT_MARKER);
170 header[12..16].copy_from_slice(&LEDGER_PAYLOAD_FORMAT_VERSION.to_le_bytes());
171 header[16..24].copy_from_slice(&(payload_len as u64).to_le_bytes());
172 header
173}
174
175#[non_exhaustive]
182#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
183pub enum LedgerPayloadEnvelopeError {
184 #[error("ledger payload envelope is truncated: {actual} bytes, need at least {minimum}")]
186 Truncated {
187 actual: usize,
189 minimum: usize,
191 },
192 #[error("ledger payload envelope has bad magic {found:?}")]
194 BadMagic {
195 found: [u8; 8],
197 },
198 #[error("unsupported ic-memory ledger payload format (marker={marker:?}, version={version:?})")]
201 UnsupportedFormat {
202 marker: [u8; 4],
204 version: Option<u32>,
206 },
207 #[error("ledger payload envelope length {len} is too large")]
210 PayloadTooLarge {
211 len: u64,
213 },
214 #[error("ledger payload envelope declared {declared} payload bytes but contained {actual}")]
216 LengthMismatch {
217 declared: usize,
219 actual: usize,
221 },
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 #[test]
229 fn ledger_writer_admits_exact_ceiling_and_refuses_without_partial_append() {
230 use std::io::Write;
231
232 let mut writer = crate::bounded_writer::BoundedWriter::new(
233 vec![0; LEDGER_PAYLOAD_HEADER_LEN],
234 crate::constants::MAX_COMMITTED_PAYLOAD_BYTES,
235 );
236 let chunk = [42; 4096];
237 for _ in 0..crate::constants::MAX_LEDGER_BYTES / chunk.len() {
238 writer.write_all(&chunk).unwrap();
239 }
240 assert_eq!(
241 writer.bytes.len(),
242 crate::constants::MAX_COMMITTED_PAYLOAD_BYTES
243 );
244 writer.write_all(&[]).unwrap();
245 assert_eq!(
246 writer.write(&[42]).unwrap_err().kind(),
247 std::io::ErrorKind::WriteZero
248 );
249 assert_eq!(
250 writer.bytes.len(),
251 crate::constants::MAX_COMMITTED_PAYLOAD_BYTES
252 );
253 assert!(
254 writer.bytes[LEDGER_PAYLOAD_HEADER_LEN..]
255 .iter()
256 .all(|&byte| byte == 42)
257 );
258
259 let mut writer = crate::bounded_writer::BoundedWriter::new(
260 vec![0; LEDGER_PAYLOAD_HEADER_LEN],
261 crate::constants::MAX_COMMITTED_PAYLOAD_BYTES,
262 );
263 let before = writer.bytes.clone();
264 assert!(
265 writer
266 .write(&vec![42; crate::constants::MAX_LEDGER_BYTES + 1])
267 .is_err()
268 );
269 assert_eq!(writer.bytes, before);
270 }
271
272 #[test]
273 fn bounded_ledger_encoding_matches_canonical_cbor() {
274 for generations in [0, 1, 24, 256, 1024] {
275 let ledger = super::super::AllocationLedger {
276 current_generation: generations,
277 records: Vec::new(),
278 };
279 let mut payload = Vec::new();
280 ciborium::into_writer(&ledger, &mut payload).unwrap();
281 let expected = LedgerPayloadEnvelope::current(payload)
282 .try_encode()
283 .unwrap();
284 assert_eq!(encode_ledger(&ledger).unwrap(), expected);
285 }
286 }
287
288 #[test]
289 fn oversized_payload_is_rejected_before_encoding() {
290 let len = crate::constants::MAX_LEDGER_BYTES + 1;
291 assert_eq!(
292 LedgerPayloadEnvelope::current(vec![0; len]).try_encode(),
293 Err(LedgerPayloadEnvelopeError::PayloadTooLarge { len: len as u64 })
294 );
295 }
296
297 #[test]
298 fn malformed_lengths_are_classified_before_payload_copy() {
299 let mut bytes = LedgerPayloadEnvelope::current(Vec::new()).encode();
300 for len in 0..LEDGER_PAYLOAD_HEADER_LEN {
301 assert_eq!(
302 LedgerPayloadEnvelope::decode(&bytes[..len]),
303 Err(LedgerPayloadEnvelopeError::Truncated {
304 actual: len,
305 minimum: LEDGER_PAYLOAD_HEADER_LEN,
306 })
307 );
308 }
309 for len in [crate::constants::MAX_LEDGER_BYTES as u64 + 1, u64::MAX] {
310 bytes[16..24].copy_from_slice(&len.to_le_bytes());
311 assert_eq!(
312 LedgerPayloadEnvelope::decode(&bytes),
313 Err(LedgerPayloadEnvelopeError::PayloadTooLarge { len })
314 );
315 }
316 bytes[16..24].copy_from_slice(&1_u64.to_le_bytes());
317 assert_eq!(
318 LedgerPayloadEnvelope::decode(&bytes),
319 Err(LedgerPayloadEnvelopeError::LengthMismatch {
320 declared: 1,
321 actual: 0,
322 })
323 );
324 bytes[16..24].copy_from_slice(&0_u64.to_le_bytes());
325 bytes.push(42);
326 assert_eq!(
327 LedgerPayloadEnvelope::decode(&bytes),
328 Err(LedgerPayloadEnvelopeError::LengthMismatch {
329 declared: 0,
330 actual: 1,
331 })
332 );
333 }
334
335 #[test]
336 fn borrowed_payload_decode_shares_storage_through_the_byte_ceiling() {
337 for len in [0, 3, crate::constants::MAX_LEDGER_BYTES] {
338 let bytes = LedgerPayloadEnvelope::current(vec![42; len])
339 .try_encode()
340 .expect("bounded envelope");
341 let payload = decode_payload(&bytes).expect("valid envelope");
342
343 assert_eq!(payload.len(), len);
344 assert_eq!(
345 payload.as_ptr(),
346 bytes[LEDGER_PAYLOAD_HEADER_LEN..].as_ptr()
347 );
348 assert!(payload.iter().all(|byte| *byte == 42));
349 }
350 }
351}