const LEDGER_PAYLOAD_MAGIC: &[u8; 8] = b"ICMEMLED";
const LEDGER_PAYLOAD_FORMAT_MARKER: &[u8; 4] = b"ICMS";
pub const LEDGER_PAYLOAD_FORMAT_VERSION: u32 = 1;
use crate::constants::LEDGER_PAYLOAD_HEADER_LEN;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LedgerPayloadEnvelope {
payload: Vec<u8>,
}
impl LedgerPayloadEnvelope {
#[must_use]
pub const fn current(payload: Vec<u8>) -> Self {
Self { payload }
}
#[must_use]
pub fn encode(&self) -> Vec<u8> {
self.try_encode()
.expect("payload exceeds the ledger byte ceiling")
}
pub fn try_encode(&self) -> Result<Vec<u8>, LedgerPayloadEnvelopeError> {
if self.payload.len() > crate::constants::MAX_LEDGER_BYTES {
return Err(LedgerPayloadEnvelopeError::PayloadTooLarge {
len: self.payload.len() as u64,
});
}
let total_len = LEDGER_PAYLOAD_HEADER_LEN + self.payload.len();
let mut bytes = Vec::with_capacity(total_len);
bytes.extend_from_slice(&encoded_header(self.payload.len()));
bytes.extend_from_slice(&self.payload);
Ok(bytes)
}
pub(super) fn encode_ledger(
ledger: &super::AllocationLedger,
) -> Result<Vec<u8>, super::LedgerIntegrityError> {
let mut writer = LedgerWriter(vec![0; LEDGER_PAYLOAD_HEADER_LEN]);
match ciborium::into_writer(ledger, &mut writer) {
Ok(()) => (),
Err(ciborium::ser::Error::Io(err)) if err.kind() == std::io::ErrorKind::WriteZero => {
return Err(super::LedgerIntegrityError::LimitExceeded {
resource: "ledger bytes",
limit: crate::constants::MAX_LEDGER_BYTES,
});
}
Err(err) => panic!("allocation ledger serialization failed: {err}"),
}
let mut bytes = writer.0;
let payload_len = bytes.len() - LEDGER_PAYLOAD_HEADER_LEN;
bytes[..LEDGER_PAYLOAD_HEADER_LEN].copy_from_slice(&encoded_header(payload_len));
Ok(bytes)
}
pub fn decode(bytes: &[u8]) -> Result<Self, LedgerPayloadEnvelopeError> {
Ok(Self {
payload: Self::decode_payload(bytes)?.to_vec(),
})
}
pub(super) fn decode_payload(bytes: &[u8]) -> Result<&[u8], LedgerPayloadEnvelopeError> {
let Some(magic) = bytes.get(0..8).and_then(|bytes| bytes.try_into().ok()) else {
return Err(LedgerPayloadEnvelopeError::Truncated {
actual: bytes.len(),
minimum: LEDGER_PAYLOAD_HEADER_LEN,
});
};
if &magic != LEDGER_PAYLOAD_MAGIC {
return Err(LedgerPayloadEnvelopeError::BadMagic { found: magic });
}
let Some(format_marker) = bytes.get(8..12).and_then(|bytes| bytes.try_into().ok()) else {
return Err(LedgerPayloadEnvelopeError::Truncated {
actual: bytes.len(),
minimum: LEDGER_PAYLOAD_HEADER_LEN,
});
};
if &format_marker != LEDGER_PAYLOAD_FORMAT_MARKER {
return Err(LedgerPayloadEnvelopeError::UnsupportedFormat {
marker: format_marker,
version: None,
});
}
let Some(format_version) = bytes.get(12..16).and_then(|bytes| bytes.try_into().ok()) else {
return Err(LedgerPayloadEnvelopeError::Truncated {
actual: bytes.len(),
minimum: LEDGER_PAYLOAD_HEADER_LEN,
});
};
let format_version = u32::from_le_bytes(format_version);
if format_version != LEDGER_PAYLOAD_FORMAT_VERSION {
return Err(LedgerPayloadEnvelopeError::UnsupportedFormat {
marker: format_marker,
version: Some(format_version),
});
}
let Some(payload_len) = bytes.get(16..24).and_then(|bytes| bytes.try_into().ok()) else {
return Err(LedgerPayloadEnvelopeError::Truncated {
actual: bytes.len(),
minimum: LEDGER_PAYLOAD_HEADER_LEN,
});
};
let payload_len = u64::from_le_bytes(payload_len);
let payload_len = usize::try_from(payload_len)
.map_err(|_| LedgerPayloadEnvelopeError::PayloadTooLarge { len: payload_len })?;
if payload_len > crate::constants::MAX_LEDGER_BYTES {
return Err(LedgerPayloadEnvelopeError::PayloadTooLarge {
len: payload_len as u64,
});
}
let expected_len = LEDGER_PAYLOAD_HEADER_LEN + payload_len;
if bytes.len() != expected_len {
return Err(LedgerPayloadEnvelopeError::LengthMismatch {
declared: payload_len,
actual: bytes.len().saturating_sub(LEDGER_PAYLOAD_HEADER_LEN),
});
}
Ok(&bytes[LEDGER_PAYLOAD_HEADER_LEN..])
}
#[must_use]
pub fn payload(&self) -> &[u8] {
&self.payload
}
}
struct LedgerWriter(Vec<u8>);
impl std::io::Write for LedgerWriter {
fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
self.write_all(bytes)?;
Ok(bytes.len())
}
#[inline]
fn write_all(&mut self, bytes: &[u8]) -> std::io::Result<()> {
let limit = crate::constants::MAX_COMMITTED_PAYLOAD_BYTES;
if bytes.len() > limit - self.0.len() {
return Err(std::io::ErrorKind::WriteZero.into());
}
let required = self.0.len() + bytes.len();
if required > self.0.capacity() {
let capacity = self.0.capacity().saturating_mul(2).max(required).min(limit);
self.0.reserve_exact(capacity - self.0.len());
}
self.0.extend_from_slice(bytes);
Ok(())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
fn encoded_header(payload_len: usize) -> [u8; LEDGER_PAYLOAD_HEADER_LEN] {
let mut header = [0; LEDGER_PAYLOAD_HEADER_LEN];
header[..8].copy_from_slice(LEDGER_PAYLOAD_MAGIC);
header[8..12].copy_from_slice(LEDGER_PAYLOAD_FORMAT_MARKER);
header[12..16].copy_from_slice(&LEDGER_PAYLOAD_FORMAT_VERSION.to_le_bytes());
header[16..24].copy_from_slice(&(payload_len as u64).to_le_bytes());
header
}
#[non_exhaustive]
#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
pub enum LedgerPayloadEnvelopeError {
#[error("ledger payload envelope is truncated: {actual} bytes, need at least {minimum}")]
Truncated {
actual: usize,
minimum: usize,
},
#[error("ledger payload envelope has bad magic {found:?}")]
BadMagic {
found: [u8; 8],
},
#[error("unsupported ic-memory ledger payload format (marker={marker:?}, version={version:?})")]
UnsupportedFormat {
marker: [u8; 4],
version: Option<u32>,
},
#[error("ledger payload envelope length {len} is too large")]
PayloadTooLarge {
len: u64,
},
#[error("ledger payload envelope declared {declared} payload bytes but contained {actual}")]
LengthMismatch {
declared: usize,
actual: usize,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ledger_writer_admits_exact_ceiling_and_refuses_without_partial_append() {
use std::io::Write;
let mut writer = LedgerWriter(vec![0; LEDGER_PAYLOAD_HEADER_LEN]);
let chunk = [42; 4096];
for _ in 0..crate::constants::MAX_LEDGER_BYTES / chunk.len() {
writer.write_all(&chunk).unwrap();
}
assert_eq!(
writer.0.len(),
crate::constants::MAX_COMMITTED_PAYLOAD_BYTES
);
writer.write_all(&[]).unwrap();
assert_eq!(
writer.write(&[42]).unwrap_err().kind(),
std::io::ErrorKind::WriteZero
);
assert_eq!(
writer.0.len(),
crate::constants::MAX_COMMITTED_PAYLOAD_BYTES
);
assert!(
writer.0[LEDGER_PAYLOAD_HEADER_LEN..]
.iter()
.all(|&byte| byte == 42)
);
let mut writer = LedgerWriter(vec![0; LEDGER_PAYLOAD_HEADER_LEN]);
let before = writer.0.clone();
assert!(
writer
.write(&vec![42; crate::constants::MAX_LEDGER_BYTES + 1])
.is_err()
);
assert_eq!(writer.0, before);
}
#[test]
fn bounded_ledger_encoding_matches_canonical_cbor() {
for generations in [0, 1, 24, 256, 1024] {
let ledger = super::super::AllocationLedger {
current_generation: generations,
records: Vec::new(),
};
let mut payload = Vec::new();
ciborium::into_writer(&ledger, &mut payload).unwrap();
let expected = LedgerPayloadEnvelope::current(payload)
.try_encode()
.unwrap();
assert_eq!(
LedgerPayloadEnvelope::encode_ledger(&ledger).unwrap(),
expected
);
}
}
#[test]
fn oversized_payload_is_rejected_before_encoding() {
let len = crate::constants::MAX_LEDGER_BYTES + 1;
assert_eq!(
LedgerPayloadEnvelope::current(vec![0; len]).try_encode(),
Err(LedgerPayloadEnvelopeError::PayloadTooLarge { len: len as u64 })
);
}
#[test]
fn malformed_lengths_are_classified_before_payload_copy() {
let mut bytes = LedgerPayloadEnvelope::current(Vec::new()).encode();
for len in 0..LEDGER_PAYLOAD_HEADER_LEN {
assert_eq!(
LedgerPayloadEnvelope::decode(&bytes[..len]),
Err(LedgerPayloadEnvelopeError::Truncated {
actual: len,
minimum: LEDGER_PAYLOAD_HEADER_LEN,
})
);
}
for len in [crate::constants::MAX_LEDGER_BYTES as u64 + 1, u64::MAX] {
bytes[16..24].copy_from_slice(&len.to_le_bytes());
assert_eq!(
LedgerPayloadEnvelope::decode(&bytes),
Err(LedgerPayloadEnvelopeError::PayloadTooLarge { len })
);
}
bytes[16..24].copy_from_slice(&1_u64.to_le_bytes());
assert_eq!(
LedgerPayloadEnvelope::decode(&bytes),
Err(LedgerPayloadEnvelopeError::LengthMismatch {
declared: 1,
actual: 0,
})
);
bytes[16..24].copy_from_slice(&0_u64.to_le_bytes());
bytes.push(42);
assert_eq!(
LedgerPayloadEnvelope::decode(&bytes),
Err(LedgerPayloadEnvelopeError::LengthMismatch {
declared: 0,
actual: 1,
})
);
}
#[test]
fn borrowed_payload_decode_shares_storage_through_the_byte_ceiling() {
for len in [0, 3, crate::constants::MAX_LEDGER_BYTES] {
let bytes = LedgerPayloadEnvelope::current(vec![42; len])
.try_encode()
.expect("bounded envelope");
let payload = LedgerPayloadEnvelope::decode_payload(&bytes).expect("valid envelope");
assert_eq!(payload.len(), len);
assert_eq!(
payload.as_ptr(),
bytes[LEDGER_PAYLOAD_HEADER_LEN..].as_ptr()
);
assert!(payload.iter().all(|byte| *byte == 42));
}
}
}