use alloc::vec::Vec;
use crate::error::Error;
const U32: usize = 4;
pub(crate) fn encode(pairs: &[(&str, &str)]) -> Result<Vec<u8>, Error> {
let mut order: Vec<usize> = (0..pairs.len()).collect();
order.sort_by(|&a, &b| pairs[a].0.as_bytes().cmp(pairs[b].0.as_bytes()));
for w in order.windows(2) {
if pairs[w[0]].0 == pairs[w[1]].0 {
return Err(Error::Invalid("duplicate metadata key"));
}
}
let mut out = Vec::new();
out.extend_from_slice(&(pairs.len() as u32).to_le_bytes());
for &i in &order {
let (k, v) = pairs[i];
out.extend_from_slice(&(k.len() as u32).to_le_bytes());
out.extend_from_slice(k.as_bytes());
out.extend_from_slice(&(v.len() as u32).to_le_bytes());
out.extend_from_slice(v.as_bytes());
}
Ok(out)
}
pub(crate) fn decode<'a>(bytes: &'a [u8], out: &mut Vec<(&'a str, &'a str)>) -> Result<(), Error> {
out.clear();
let mut at = 0usize;
let count = take_u32(bytes, &mut at)?;
let mut prev: Option<&str> = None;
for _ in 0..count {
let k = take_str(bytes, &mut at)?;
let v = take_str(bytes, &mut at)?;
match prev {
Some(p) if k.as_bytes() <= p.as_bytes() => {
return Err(Error::Corrupt("metadata keys are not strictly ascending"));
}
_ => {}
}
prev = Some(k);
out.push((k, v));
}
if at != bytes.len() {
return Err(Error::Corrupt("metadata blob has trailing bytes"));
}
Ok(())
}
fn take_u32(bytes: &[u8], at: &mut usize) -> Result<u32, Error> {
let end = at
.checked_add(U32)
.filter(|&e| e <= bytes.len())
.ok_or(Error::Corrupt("metadata blob truncated inside a field"))?;
let v = u32::from_le_bytes(bytes[*at..end].try_into().unwrap());
*at = end;
Ok(v)
}
fn take_str<'a>(bytes: &'a [u8], at: &mut usize) -> Result<&'a str, Error> {
let len = take_u32(bytes, at)? as usize;
let end = at
.checked_add(len)
.filter(|&e| e <= bytes.len())
.ok_or(Error::Corrupt("metadata string overruns its blob"))?;
let s = core::str::from_utf8(&bytes[*at..end])
.map_err(|_| Error::Corrupt("metadata string is not UTF-8"))?;
*at = end;
Ok(s)
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
#[test]
fn encode_sorts_keys_and_round_trips() {
let pairs = [
("uri", "s3://b/x"),
("mime", "application/pdf"),
("page", "3"),
];
let bytes = encode(&pairs).unwrap();
let mut out = Vec::new();
decode(&bytes, &mut out).unwrap();
assert_eq!(
out,
vec![
("mime", "application/pdf"),
("page", "3"),
("uri", "s3://b/x"),
]
);
}
#[test]
fn empty_map_encodes_to_a_count_of_zero() {
let bytes = encode(&[]).unwrap();
assert_eq!(bytes, 0u32.to_le_bytes());
let mut out = vec![("stale", "stale")];
decode(&bytes, &mut out).unwrap();
assert!(out.is_empty());
}
#[test]
fn duplicate_key_is_rejected() {
let err = encode(&[("k", "a"), ("k", "b")]).unwrap_err();
assert!(matches!(err, Error::Invalid(_)));
}
#[test]
fn decode_rejects_unsorted_or_duplicate_keys() {
let mut bad = Vec::new();
bad.extend_from_slice(&2u32.to_le_bytes());
for (k, v) in [("b", "1"), ("a", "2")] {
bad.extend_from_slice(&(k.len() as u32).to_le_bytes());
bad.extend_from_slice(k.as_bytes());
bad.extend_from_slice(&(v.len() as u32).to_le_bytes());
bad.extend_from_slice(v.as_bytes());
}
let mut out = Vec::new();
assert!(matches!(decode(&bad, &mut out), Err(Error::Corrupt(_))));
}
#[test]
fn decode_rejects_truncation_and_trailing_bytes() {
let bytes = encode(&[("k", "v")]).unwrap();
let mut out = Vec::new();
assert!(matches!(
decode(&bytes[..bytes.len() - 1], &mut out),
Err(Error::Corrupt(_))
));
let mut extra = bytes.clone();
extra.push(0);
assert!(matches!(decode(&extra, &mut out), Err(Error::Corrupt(_))));
}
#[test]
fn decode_rejects_non_utf8_key() {
let mut bad = Vec::new();
bad.extend_from_slice(&1u32.to_le_bytes());
bad.extend_from_slice(&1u32.to_le_bytes());
bad.push(0xff); bad.extend_from_slice(&0u32.to_le_bytes());
let mut out = Vec::new();
assert!(matches!(decode(&bad, &mut out), Err(Error::Corrupt(_))));
}
}