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 payload_len = self.payload.len() as u64;
53
54 let mut bytes = Vec::with_capacity(total_len);
55 bytes.extend_from_slice(LEDGER_PAYLOAD_MAGIC);
56 bytes.extend_from_slice(LEDGER_PAYLOAD_FORMAT_MARKER);
57 bytes.extend_from_slice(&LEDGER_PAYLOAD_FORMAT_VERSION.to_le_bytes());
58 bytes.extend_from_slice(&payload_len.to_le_bytes());
59 bytes.extend_from_slice(&self.payload);
60 Ok(bytes)
61 }
62
63 pub fn decode(bytes: &[u8]) -> Result<Self, LedgerPayloadEnvelopeError> {
65 Ok(Self {
66 payload: Self::decode_payload(bytes)?.to_vec(),
67 })
68 }
69
70 pub(super) fn decode_payload(bytes: &[u8]) -> Result<&[u8], LedgerPayloadEnvelopeError> {
73 if bytes.len() < LEDGER_PAYLOAD_MAGIC.len() {
74 return Err(LedgerPayloadEnvelopeError::Truncated {
75 actual: bytes.len(),
76 minimum: LEDGER_PAYLOAD_HEADER_LEN,
77 });
78 }
79
80 let Some(magic) = bytes.get(0..8).and_then(|bytes| bytes.try_into().ok()) else {
81 return Err(LedgerPayloadEnvelopeError::Truncated {
82 actual: bytes.len(),
83 minimum: LEDGER_PAYLOAD_HEADER_LEN,
84 });
85 };
86 if &magic != LEDGER_PAYLOAD_MAGIC {
87 return Err(LedgerPayloadEnvelopeError::BadMagic { found: magic });
88 }
89
90 let Some(format_marker) = bytes.get(8..12).and_then(|bytes| bytes.try_into().ok()) else {
91 return Err(LedgerPayloadEnvelopeError::Truncated {
92 actual: bytes.len(),
93 minimum: LEDGER_PAYLOAD_HEADER_LEN,
94 });
95 };
96 if &format_marker != LEDGER_PAYLOAD_FORMAT_MARKER {
97 return Err(LedgerPayloadEnvelopeError::UnsupportedFormat {
98 marker: format_marker,
99 version: None,
100 });
101 }
102
103 let Some(format_version) = bytes.get(12..16).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 let format_version = u32::from_le_bytes(format_version);
110 if format_version != LEDGER_PAYLOAD_FORMAT_VERSION {
111 return Err(LedgerPayloadEnvelopeError::UnsupportedFormat {
112 marker: format_marker,
113 version: Some(format_version),
114 });
115 }
116
117 let Some(payload_len) = bytes.get(16..24).and_then(|bytes| bytes.try_into().ok()) else {
118 return Err(LedgerPayloadEnvelopeError::Truncated {
119 actual: bytes.len(),
120 minimum: LEDGER_PAYLOAD_HEADER_LEN,
121 });
122 };
123 let payload_len = u64::from_le_bytes(payload_len);
124 let payload_len = usize::try_from(payload_len)
125 .map_err(|_| LedgerPayloadEnvelopeError::PayloadTooLarge { len: payload_len })?;
126 if payload_len > crate::constants::MAX_LEDGER_BYTES {
127 return Err(LedgerPayloadEnvelopeError::PayloadTooLarge {
128 len: payload_len as u64,
129 });
130 }
131 let expected_len = LEDGER_PAYLOAD_HEADER_LEN + payload_len;
132 if bytes.len() != expected_len {
133 return Err(LedgerPayloadEnvelopeError::LengthMismatch {
134 declared: payload_len,
135 actual: bytes.len().saturating_sub(LEDGER_PAYLOAD_HEADER_LEN),
136 });
137 }
138
139 Ok(&bytes[LEDGER_PAYLOAD_HEADER_LEN..])
140 }
141
142 #[must_use]
144 pub fn payload(&self) -> &[u8] {
145 &self.payload
146 }
147}
148
149#[non_exhaustive]
156#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
157pub enum LedgerPayloadEnvelopeError {
158 #[error("ledger payload envelope is truncated: {actual} bytes, need at least {minimum}")]
160 Truncated {
161 actual: usize,
163 minimum: usize,
165 },
166 #[error("ledger payload envelope has bad magic {found:?}")]
168 BadMagic {
169 found: [u8; 8],
171 },
172 #[error("unsupported ic-memory ledger payload format (marker={marker:?}, version={version:?})")]
175 UnsupportedFormat {
176 marker: [u8; 4],
178 version: Option<u32>,
180 },
181 #[error("ledger payload envelope length {len} is too large")]
184 PayloadTooLarge {
185 len: u64,
187 },
188 #[error("ledger payload envelope declared {declared} payload bytes but contained {actual}")]
190 LengthMismatch {
191 declared: usize,
193 actual: usize,
195 },
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 #[test]
203 fn oversized_payload_is_rejected_before_encoding() {
204 let len = crate::constants::MAX_LEDGER_BYTES + 1;
205 assert_eq!(
206 LedgerPayloadEnvelope::current(vec![0; len]).try_encode(),
207 Err(LedgerPayloadEnvelopeError::PayloadTooLarge { len: len as u64 })
208 );
209 }
210
211 #[test]
212 fn malformed_lengths_are_classified_before_payload_copy() {
213 let mut bytes = LedgerPayloadEnvelope::current(Vec::new()).encode();
214 for len in 0..LEDGER_PAYLOAD_HEADER_LEN {
215 assert_eq!(
216 LedgerPayloadEnvelope::decode(&bytes[..len]),
217 Err(LedgerPayloadEnvelopeError::Truncated {
218 actual: len,
219 minimum: LEDGER_PAYLOAD_HEADER_LEN,
220 })
221 );
222 }
223 for len in [crate::constants::MAX_LEDGER_BYTES as u64 + 1, u64::MAX] {
224 bytes[16..24].copy_from_slice(&len.to_le_bytes());
225 assert_eq!(
226 LedgerPayloadEnvelope::decode(&bytes),
227 Err(LedgerPayloadEnvelopeError::PayloadTooLarge { len })
228 );
229 }
230 bytes[16..24].copy_from_slice(&1_u64.to_le_bytes());
231 assert_eq!(
232 LedgerPayloadEnvelope::decode(&bytes),
233 Err(LedgerPayloadEnvelopeError::LengthMismatch {
234 declared: 1,
235 actual: 0,
236 })
237 );
238 bytes[16..24].copy_from_slice(&0_u64.to_le_bytes());
239 bytes.push(42);
240 assert_eq!(
241 LedgerPayloadEnvelope::decode(&bytes),
242 Err(LedgerPayloadEnvelopeError::LengthMismatch {
243 declared: 0,
244 actual: 1,
245 })
246 );
247 }
248
249 #[test]
250 fn borrowed_payload_decode_shares_storage_through_the_byte_ceiling() {
251 for len in [0, 3, crate::constants::MAX_LEDGER_BYTES] {
252 let bytes = LedgerPayloadEnvelope::current(vec![42; len])
253 .try_encode()
254 .expect("bounded envelope");
255 let payload = LedgerPayloadEnvelope::decode_payload(&bytes).expect("valid envelope");
256
257 assert_eq!(payload.len(), len);
258 assert_eq!(
259 payload.as_ptr(),
260 bytes[LEDGER_PAYLOAD_HEADER_LEN..].as_ptr()
261 );
262 assert!(payload.iter().all(|byte| *byte == 42));
263 }
264 }
265}