ic_memory/ledger/
payload.rs1const 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 bytes = vec![0; LEDGER_PAYLOAD_HEADER_LEN];
64 ciborium::into_writer(ledger, &mut bytes).expect("allocation ledger encodes into Vec");
66 let payload_len = bytes.len() - LEDGER_PAYLOAD_HEADER_LEN;
67 if payload_len > crate::constants::MAX_LEDGER_BYTES {
68 return Err(super::LedgerIntegrityError::LimitExceeded {
69 resource: "ledger bytes",
70 limit: crate::constants::MAX_LEDGER_BYTES,
71 });
72 }
73 bytes[..LEDGER_PAYLOAD_HEADER_LEN].copy_from_slice(&encoded_header(payload_len));
74 Ok(bytes)
75 }
76
77 pub fn decode(bytes: &[u8]) -> Result<Self, LedgerPayloadEnvelopeError> {
79 Ok(Self {
80 payload: Self::decode_payload(bytes)?.to_vec(),
81 })
82 }
83
84 pub(super) fn decode_payload(bytes: &[u8]) -> Result<&[u8], LedgerPayloadEnvelopeError> {
87 let Some(magic) = bytes.get(0..8).and_then(|bytes| bytes.try_into().ok()) else {
88 return Err(LedgerPayloadEnvelopeError::Truncated {
89 actual: bytes.len(),
90 minimum: LEDGER_PAYLOAD_HEADER_LEN,
91 });
92 };
93 if &magic != LEDGER_PAYLOAD_MAGIC {
94 return Err(LedgerPayloadEnvelopeError::BadMagic { found: magic });
95 }
96
97 let Some(format_marker) = bytes.get(8..12).and_then(|bytes| bytes.try_into().ok()) else {
98 return Err(LedgerPayloadEnvelopeError::Truncated {
99 actual: bytes.len(),
100 minimum: LEDGER_PAYLOAD_HEADER_LEN,
101 });
102 };
103 if &format_marker != LEDGER_PAYLOAD_FORMAT_MARKER {
104 return Err(LedgerPayloadEnvelopeError::UnsupportedFormat {
105 marker: format_marker,
106 version: None,
107 });
108 }
109
110 let Some(format_version) = bytes.get(12..16).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 let format_version = u32::from_le_bytes(format_version);
117 if format_version != LEDGER_PAYLOAD_FORMAT_VERSION {
118 return Err(LedgerPayloadEnvelopeError::UnsupportedFormat {
119 marker: format_marker,
120 version: Some(format_version),
121 });
122 }
123
124 let Some(payload_len) = bytes.get(16..24).and_then(|bytes| bytes.try_into().ok()) else {
125 return Err(LedgerPayloadEnvelopeError::Truncated {
126 actual: bytes.len(),
127 minimum: LEDGER_PAYLOAD_HEADER_LEN,
128 });
129 };
130 let payload_len = u64::from_le_bytes(payload_len);
131 let payload_len = usize::try_from(payload_len)
132 .map_err(|_| LedgerPayloadEnvelopeError::PayloadTooLarge { len: payload_len })?;
133 if payload_len > crate::constants::MAX_LEDGER_BYTES {
134 return Err(LedgerPayloadEnvelopeError::PayloadTooLarge {
135 len: payload_len as u64,
136 });
137 }
138 let expected_len = LEDGER_PAYLOAD_HEADER_LEN + payload_len;
139 if bytes.len() != expected_len {
140 return Err(LedgerPayloadEnvelopeError::LengthMismatch {
141 declared: payload_len,
142 actual: bytes.len().saturating_sub(LEDGER_PAYLOAD_HEADER_LEN),
143 });
144 }
145
146 Ok(&bytes[LEDGER_PAYLOAD_HEADER_LEN..])
147 }
148
149 #[must_use]
151 pub fn payload(&self) -> &[u8] {
152 &self.payload
153 }
154}
155
156fn encoded_header(payload_len: usize) -> [u8; LEDGER_PAYLOAD_HEADER_LEN] {
158 let mut header = [0; LEDGER_PAYLOAD_HEADER_LEN];
159 header[..8].copy_from_slice(LEDGER_PAYLOAD_MAGIC);
160 header[8..12].copy_from_slice(LEDGER_PAYLOAD_FORMAT_MARKER);
161 header[12..16].copy_from_slice(&LEDGER_PAYLOAD_FORMAT_VERSION.to_le_bytes());
162 header[16..24].copy_from_slice(&(payload_len as u64).to_le_bytes());
163 header
164}
165
166#[non_exhaustive]
173#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
174pub enum LedgerPayloadEnvelopeError {
175 #[error("ledger payload envelope is truncated: {actual} bytes, need at least {minimum}")]
177 Truncated {
178 actual: usize,
180 minimum: usize,
182 },
183 #[error("ledger payload envelope has bad magic {found:?}")]
185 BadMagic {
186 found: [u8; 8],
188 },
189 #[error("unsupported ic-memory ledger payload format (marker={marker:?}, version={version:?})")]
192 UnsupportedFormat {
193 marker: [u8; 4],
195 version: Option<u32>,
197 },
198 #[error("ledger payload envelope length {len} is too large")]
201 PayloadTooLarge {
202 len: u64,
204 },
205 #[error("ledger payload envelope declared {declared} payload bytes but contained {actual}")]
207 LengthMismatch {
208 declared: usize,
210 actual: usize,
212 },
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 #[test]
220 fn oversized_payload_is_rejected_before_encoding() {
221 let len = crate::constants::MAX_LEDGER_BYTES + 1;
222 assert_eq!(
223 LedgerPayloadEnvelope::current(vec![0; len]).try_encode(),
224 Err(LedgerPayloadEnvelopeError::PayloadTooLarge { len: len as u64 })
225 );
226 }
227
228 #[test]
229 fn malformed_lengths_are_classified_before_payload_copy() {
230 let mut bytes = LedgerPayloadEnvelope::current(Vec::new()).encode();
231 for len in 0..LEDGER_PAYLOAD_HEADER_LEN {
232 assert_eq!(
233 LedgerPayloadEnvelope::decode(&bytes[..len]),
234 Err(LedgerPayloadEnvelopeError::Truncated {
235 actual: len,
236 minimum: LEDGER_PAYLOAD_HEADER_LEN,
237 })
238 );
239 }
240 for len in [crate::constants::MAX_LEDGER_BYTES as u64 + 1, u64::MAX] {
241 bytes[16..24].copy_from_slice(&len.to_le_bytes());
242 assert_eq!(
243 LedgerPayloadEnvelope::decode(&bytes),
244 Err(LedgerPayloadEnvelopeError::PayloadTooLarge { len })
245 );
246 }
247 bytes[16..24].copy_from_slice(&1_u64.to_le_bytes());
248 assert_eq!(
249 LedgerPayloadEnvelope::decode(&bytes),
250 Err(LedgerPayloadEnvelopeError::LengthMismatch {
251 declared: 1,
252 actual: 0,
253 })
254 );
255 bytes[16..24].copy_from_slice(&0_u64.to_le_bytes());
256 bytes.push(42);
257 assert_eq!(
258 LedgerPayloadEnvelope::decode(&bytes),
259 Err(LedgerPayloadEnvelopeError::LengthMismatch {
260 declared: 0,
261 actual: 1,
262 })
263 );
264 }
265
266 #[test]
267 fn borrowed_payload_decode_shares_storage_through_the_byte_ceiling() {
268 for len in [0, 3, crate::constants::MAX_LEDGER_BYTES] {
269 let bytes = LedgerPayloadEnvelope::current(vec![42; len])
270 .try_encode()
271 .expect("bounded envelope");
272 let payload = LedgerPayloadEnvelope::decode_payload(&bytes).expect("valid envelope");
273
274 assert_eq!(payload.len(), len);
275 assert_eq!(
276 payload.as_ptr(),
277 bytes[LEDGER_PAYLOAD_HEADER_LEN..].as_ptr()
278 );
279 assert!(payload.iter().all(|byte| *byte == 42));
280 }
281 }
282}