use crate::gid::Gid;
use crate::store::Error;
pub const MAGIC: &[u8; 4] = b"GMLP";
pub const FORMAT_VERSION: u8 = 1;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PackRecord {
pub id: Gid,
pub envelope: Vec<u8>,
}
pub fn pack_id(bytes: &[u8]) -> [u8; 32] {
crate::hash::blake3_256(bytes)
}
pub fn encode_pack(records: &[PackRecord]) -> Result<Vec<u8>, Error> {
let mut sorted: Vec<&PackRecord> = records.iter().collect();
sorted.sort_by_key(|r| r.id.to_bytes());
let mut out = Vec::with_capacity(16 + sorted.len() * 48);
out.extend_from_slice(MAGIC);
out.push(FORMAT_VERSION);
out.extend_from_slice(&(sorted.len() as u64).to_le_bytes());
let total: u64 = sorted.iter().map(|r| r.envelope.len() as u64).sum();
out.extend_from_slice(&total.to_le_bytes());
let mut prev: Option<[u8; 33]> = None;
for r in &sorted {
if prev == Some(r.id.to_bytes()) {
return Err(Error::Invalid(format!(
"gemlpack: duplicate object id {}",
r.id
)));
}
prev = Some(r.id.to_bytes());
out.extend_from_slice(&r.id.to_bytes());
out.extend_from_slice(&(r.envelope.len() as u64).to_le_bytes());
out.extend_from_slice(&r.envelope);
}
Ok(out)
}
#[derive(Debug, Clone, Copy)]
pub struct PackLimits {
pub max_pack_bytes: u64,
pub max_objects: usize,
pub max_object_bytes: u64,
}
impl Default for PackLimits {
fn default() -> Self {
PackLimits {
max_pack_bytes: 4 << 30, max_objects: 10_000_000,
max_object_bytes: 1 << 30, }
}
}
pub fn decode_pack(bytes: &[u8], limits: &PackLimits) -> Result<Vec<PackRecord>, Error> {
if bytes.len() < 16 {
return Err(Error::Invalid("gemlpack: truncated header".into()));
}
if &bytes[0..4] != MAGIC {
return Err(Error::Invalid("gemlpack: bad magic".into()));
}
if bytes[4] != FORMAT_VERSION {
return Err(Error::Invalid(format!(
"gemlpack: unsupported format version {}",
bytes[4]
)));
}
if (bytes.len() as u64) > limits.max_pack_bytes {
return Err(Error::Limit {
kind: "gemlpack pack bytes",
limit: limits.max_pack_bytes,
found: bytes.len() as u64,
});
}
let count = u64::from_le_bytes(bytes[5..13].try_into().unwrap());
if count as usize > limits.max_objects {
return Err(Error::Limit {
kind: "gemlpack object count",
limit: limits.max_objects as u64,
found: count,
});
}
let mut pos = 21usize;
let mut out = Vec::with_capacity(count as usize);
let mut prev: Option<[u8; 33]> = None;
for _ in 0..count {
if bytes.len().saturating_sub(pos) < 33 + 8 {
return Err(Error::Invalid("gemlpack: truncated record header".into()));
}
let id_bytes: [u8; 33] = bytes[pos..pos + 33].try_into().unwrap();
pos += 33;
let len = u64::from_le_bytes(bytes[pos..pos + 8].try_into().unwrap());
pos += 8;
if len > limits.max_object_bytes {
return Err(Error::Limit {
kind: "gemlpack object bytes",
limit: limits.max_object_bytes,
found: len,
});
}
let end = pos
.checked_add(len as usize)
.ok_or_else(|| Error::Invalid("gemlpack: object length overflow".into()))?;
if end > bytes.len() {
return Err(Error::Invalid("gemlpack: truncated object envelope".into()));
}
let id = Gid::from_bytes(&id_bytes)
.ok_or_else(|| Error::Invalid("gemlpack: invalid object id".into()))?;
if prev == Some(id_bytes) {
return Err(Error::Invalid(format!(
"gemlpack: duplicate object id {id}"
)));
}
prev = Some(id_bytes);
let envelope = &bytes[pos..end];
pos = end;
if crate::hash::blake3_256(envelope) != *id.digest() {
return Err(Error::Invalid(format!(
"gemlpack: object id/body mismatch for {id}"
)));
}
match crate::decode::decode_object(envelope, &crate::limits::Limits::default()) {
Ok(obj) if obj.family == id.family() => {}
Ok(_) => {
return Err(Error::Invalid(format!(
"gemlpack: family mismatch for {id}"
)))
}
Err(e) => {
return Err(Error::Invalid(format!(
"gemlpack: invalid canonical envelope for {id}: {e}"
)))
}
}
out.push(PackRecord {
id,
envelope: envelope.to_vec(),
});
}
if pos != bytes.len() {
return Err(Error::Invalid(format!(
"gemlpack: trailing bytes after {} objects",
count
)));
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::family::Family;
fn blob(byte: u8) -> PackRecord {
let obj = crate::value::Object::blob(vec![byte]);
let envelope =
crate::encode::encode_object(&obj, &crate::limits::Limits::default()).unwrap();
let id = Gid::new(Family::Blob, crate::hash::blake3_256(&envelope));
PackRecord { id, envelope }
}
#[test]
fn roundtrip_and_identity_verification() {
let recs = vec![blob(1), blob(2), blob(3)];
let bytes = encode_pack(&recs).unwrap();
assert_eq!(&bytes[0..4], MAGIC);
let decoded = decode_pack(&bytes, &PackLimits::default()).unwrap();
assert_eq!(decoded.len(), 3);
let mut orig: Vec<PackRecord> = recs.clone();
orig.sort_by_key(|r| r.id.to_bytes());
for (a, b) in orig.iter().zip(decoded.iter()) {
assert_eq!(a.id, b.id);
assert_eq!(a.envelope, b.envelope);
}
}
#[test]
fn deterministic_order_and_duplicate_rejection() {
let recs = vec![blob(3), blob(1), blob(2)];
let bytes = encode_pack(&recs).unwrap();
let decoded = decode_pack(&bytes, &PackLimits::default()).unwrap();
let ids: Vec<[u8; 33]> = decoded.iter().map(|r| r.id.to_bytes()).collect();
let mut sorted = ids.clone();
sorted.sort();
assert_eq!(ids, sorted);
let dup = vec![blob(1), blob(1)];
assert!(encode_pack(&dup).is_err());
}
#[test]
fn corruption_is_rejected_whole_pack() {
let bytes = encode_pack(&[blob(1), blob(2)]).unwrap();
let mut corrupt = bytes.clone();
let last = corrupt.len() - 1;
corrupt[last] ^= 0x01;
assert!(decode_pack(&corrupt, &PackLimits::default()).is_err());
assert!(decode_pack(&bytes[..bytes.len() - 5], &PackLimits::default()).is_err());
let mut bad = bytes.clone();
bad[0] = b'X';
assert!(decode_pack(&bad, &PackLimits::default()).is_err());
}
#[test]
fn bounds_are_enforced() {
let bytes = encode_pack(&[blob(1)]).unwrap();
let tight = PackLimits {
max_pack_bytes: 10,
..Default::default()
};
assert!(decode_pack(&bytes, &tight).is_err());
}
}