use crate::cursor::ManifestCursor;
use crate::error::CoreError;
use crate::locator::{
parse_locator_entries_with_ceiling, LocatorEntry, DEFAULT_LOCATOR_MAX_URI_BYTES,
};
pub const METADATA_REFERENCE_SECTION_VERSION: u8 = 1;
pub const METADATA_REFERENCE_SECTION_VERSION_2: u8 = 2;
const CODEC_STORE: u8 = 0x00;
pub const DEFAULT_INLINE_METADATA_MAX_BYTES: u32 = 1024 * 1024;
const V1_PREFIX_LEN: usize = 1 + 32 + 4;
const V2_EXTRA_PREFIX_LEN: usize = 4 + 1;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MetadataReference {
pub metadata_hash: [u8; 32],
pub locators: Vec<LocatorEntry>,
pub inline_metadata: Option<Vec<u8>>,
pub codec: u8,
pub uncompressed_len: u32,
}
impl MetadataReference {
#[must_use]
pub fn is_inlined(&self) -> bool {
self.inline_metadata.is_some()
}
}
impl Default for MetadataReference {
fn default() -> Self {
Self {
metadata_hash: [0u8; 32],
locators: Vec::new(),
inline_metadata: None,
codec: CODEC_STORE,
uncompressed_len: 0,
}
}
}
pub fn parse_metadata_reference(
cursor: &mut ManifestCursor<'_>,
) -> Result<MetadataReference, CoreError> {
parse_metadata_reference_with_ceilings(
cursor,
DEFAULT_LOCATOR_MAX_URI_BYTES,
DEFAULT_INLINE_METADATA_MAX_BYTES,
)
}
pub fn parse_metadata_reference_with_ceilings(
cursor: &mut ManifestCursor<'_>,
max_locator_uri_bytes: u32,
max_inline_metadata_bytes: u32,
) -> Result<MetadataReference, CoreError> {
let section_version = cursor.read_u8()?;
match section_version {
METADATA_REFERENCE_SECTION_VERSION => {
parse_v1(cursor, max_locator_uri_bytes, max_inline_metadata_bytes)
}
METADATA_REFERENCE_SECTION_VERSION_2 => {
parse_v2(cursor, max_locator_uri_bytes, max_inline_metadata_bytes)
}
other => Err(CoreError::UnsupportedFeature {
feature: format!("metadata_reference section version {other} (supported: 1, 2)"),
}),
}
}
fn parse_v1(
cursor: &mut ManifestCursor<'_>,
max_locator_uri_bytes: u32,
max_inline_metadata_bytes: u32,
) -> Result<MetadataReference, CoreError> {
let metadata_hash = read_hash(cursor)?;
let locator_count = cursor.read_u32_le()?;
let locators =
parse_locator_entries_with_ceiling(cursor, locator_count, max_locator_uri_bytes)?;
let inline_metadata_len = cursor.read_u32_le()?;
let inline_metadata = read_inline_blob(
cursor,
inline_metadata_len,
max_inline_metadata_bytes,
inline_metadata_len,
CODEC_STORE,
)?;
if locators.is_empty() && inline_metadata.is_none() {
return Err(unreachable_error(V1_PREFIX_LEN));
}
Ok(MetadataReference {
metadata_hash,
locators,
inline_metadata,
codec: CODEC_STORE,
uncompressed_len: inline_metadata_len,
})
}
fn parse_v2(
cursor: &mut ManifestCursor<'_>,
max_locator_uri_bytes: u32,
max_inline_metadata_bytes: u32,
) -> Result<MetadataReference, CoreError> {
let metadata_hash = read_hash(cursor)?;
let uncompressed_len = cursor.read_u32_le()?;
let codec = cursor.read_u8()?;
let locator_count = cursor.read_u32_le()?;
let locators =
parse_locator_entries_with_ceiling(cursor, locator_count, max_locator_uri_bytes)?;
let inline_data_len = cursor.read_u32_le()?;
let inline_metadata = read_inline_blob(
cursor,
inline_data_len,
max_inline_metadata_bytes,
uncompressed_len,
codec,
)?;
if locators.is_empty() && inline_metadata.is_none() {
return Err(unreachable_error(V1_PREFIX_LEN + V2_EXTRA_PREFIX_LEN));
}
Ok(MetadataReference {
metadata_hash,
locators,
inline_metadata,
codec,
uncompressed_len,
})
}
fn read_hash(cursor: &mut ManifestCursor<'_>) -> Result<[u8; 32], CoreError> {
let hash_bytes = cursor.read_n(32)?;
let mut metadata_hash = [0u8; 32];
metadata_hash.copy_from_slice(hash_bytes);
Ok(metadata_hash)
}
fn read_inline_blob(
cursor: &mut ManifestCursor<'_>,
inline_data_len: u32,
max_inline_metadata_bytes: u32,
uncompressed_len: u32,
codec: u8,
) -> Result<Option<Vec<u8>>, CoreError> {
if inline_data_len == 0 {
return Ok(None);
}
if inline_data_len > max_inline_metadata_bytes {
return Err(CoreError::Corrupt {
reason: format!(
"metadata_reference inline_data_len {inline_data_len} exceeds ceiling {max_inline_metadata_bytes}"
),
});
}
let wire_len = usize::try_from(inline_data_len).map_err(|_| CoreError::Corrupt {
reason: format!("metadata_reference inline_data_len {inline_data_len} exceeds usize"),
})?;
let wire_bytes = cursor.read_n_owned(wire_len)?;
if codec == CODEC_STORE {
return Ok(Some(wire_bytes));
}
let uncompressed =
crate::codec::decompress(codec, &wire_bytes, uncompressed_len).map_err(|e| {
CoreError::Corrupt {
reason: format!("metadata_reference: codec 0x{codec:02X} decompress failed: {e}"),
}
})?;
let got = u32::try_from(uncompressed.len()).unwrap_or(u32::MAX);
if got != uncompressed_len {
return Err(CoreError::Corrupt {
reason: format!(
"metadata_reference: decompressed length {got} does not match declared uncompressed_len {uncompressed_len}"
),
});
}
Ok(Some(uncompressed))
}
fn unreachable_error(prefix_len: usize) -> CoreError {
CoreError::Corrupt {
reason: format!(
"metadata_reference is unreachable: locator_count=0 and inline_data_len=0 (need at least one source for the {prefix_len}-byte metadata blob)"
),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_locator_bytes(uri: &str) -> Vec<u8> {
let length = u32::try_from(uri.len()).expect("test URI fits u32");
let mut bytes = Vec::with_capacity(4 + uri.len());
bytes.extend_from_slice(&length.to_le_bytes());
bytes.extend_from_slice(uri.as_bytes());
bytes
}
fn make_metadata_reference_bytes(
version: u8,
metadata_hash: [u8; 32],
locator_uris: &[&str],
inline_metadata: Option<&[u8]>,
) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.push(version);
bytes.extend_from_slice(&metadata_hash);
let locator_count = u32::try_from(locator_uris.len()).expect("count fits u32");
bytes.extend_from_slice(&locator_count.to_le_bytes());
for uri in locator_uris {
bytes.extend(make_locator_bytes(uri));
}
let inline_len =
inline_metadata.map_or(0u32, |b| u32::try_from(b.len()).expect("len fits u32"));
bytes.extend_from_slice(&inline_len.to_le_bytes());
if let Some(blob) = inline_metadata {
bytes.extend_from_slice(blob);
}
bytes
}
fn sample_hash() -> [u8; 32] {
[0xAA; 32]
}
#[test]
fn parses_external_metadata_single_locator() {
let bytes = make_metadata_reference_bytes(
METADATA_REFERENCE_SECTION_VERSION,
sample_hash(),
&["file:///var/lib/limnifs/metadata.bin"],
None,
);
let mut cursor = ManifestCursor::new(&bytes);
let parsed = parse_metadata_reference(&mut cursor).expect("external parses");
assert_eq!(parsed.metadata_hash, sample_hash());
assert_eq!(parsed.locators.len(), 1);
assert_eq!(parsed.locators[0].scheme(), Some("file"));
assert!(parsed.inline_metadata.is_none());
assert!(!parsed.is_inlined());
assert_eq!(cursor.position(), bytes.len());
}
#[test]
fn parses_inlined_metadata() {
let blob = vec![0xBB; 1024];
let bytes = make_metadata_reference_bytes(
METADATA_REFERENCE_SECTION_VERSION,
sample_hash(),
&[],
Some(&blob),
);
let mut cursor = ManifestCursor::new(&bytes);
let parsed = parse_metadata_reference(&mut cursor).expect("inlined parses");
assert_eq!(parsed.locators.len(), 0);
assert_eq!(parsed.inline_metadata.as_deref(), Some(&blob[..]));
assert!(parsed.is_inlined());
}
#[test]
fn parses_mirrored_with_inline_fallback() {
let blob = vec![0xCC; 4096];
let bytes = make_metadata_reference_bytes(
METADATA_REFERENCE_SECTION_VERSION,
sample_hash(),
&["https://cdn/x.bin", "s3://bucket/x.bin"],
Some(&blob),
);
let mut cursor = ManifestCursor::new(&bytes);
let parsed = parse_metadata_reference(&mut cursor).expect("mirrored parses");
assert_eq!(parsed.locators.len(), 2);
assert_eq!(parsed.locators[0].scheme(), Some("https"));
assert_eq!(parsed.locators[1].scheme(), Some("s3"));
assert!(parsed.inline_metadata.is_some());
}
#[test]
fn rejects_unknown_section_version() {
let bytes = make_metadata_reference_bytes(7, sample_hash(), &["file:///x"], None);
let mut cursor = ManifestCursor::new(&bytes);
match parse_metadata_reference(&mut cursor) {
Err(CoreError::UnsupportedFeature { feature }) => {
assert!(feature.contains("version 7"), "got: {feature}");
}
other => panic!("expected UnsupportedFeature, got {other:?}"),
}
}
#[test]
fn rejects_unreachable_metadata() {
let bytes = make_metadata_reference_bytes(
METADATA_REFERENCE_SECTION_VERSION,
sample_hash(),
&[],
None,
);
let mut cursor = ManifestCursor::new(&bytes);
match parse_metadata_reference(&mut cursor) {
Err(CoreError::Corrupt { reason }) => {
assert!(reason.contains("unreachable"), "got: {reason}");
}
other => panic!("expected Corrupt, got {other:?}"),
}
}
#[test]
fn rejects_inline_above_default_ceiling() {
let oversized = vec![0xDD; (DEFAULT_INLINE_METADATA_MAX_BYTES as usize) + 1];
let bytes = make_metadata_reference_bytes(
METADATA_REFERENCE_SECTION_VERSION,
sample_hash(),
&[],
Some(&oversized),
);
let mut cursor = ManifestCursor::new(&bytes);
match parse_metadata_reference(&mut cursor) {
Err(CoreError::Corrupt { reason }) => {
assert!(reason.contains("ceiling"), "got: {reason}");
}
other => panic!("expected Corrupt, got {other:?}"),
}
}
#[test]
fn custom_ceiling_accepts_oversized_inline() {
let oversized = vec![0xEE; (DEFAULT_INLINE_METADATA_MAX_BYTES as usize) + 1024];
let bytes = make_metadata_reference_bytes(
METADATA_REFERENCE_SECTION_VERSION,
sample_hash(),
&[],
Some(&oversized),
);
let mut cursor = ManifestCursor::new(&bytes);
let parsed = parse_metadata_reference_with_ceilings(
&mut cursor,
DEFAULT_LOCATOR_MAX_URI_BYTES,
2 * DEFAULT_INLINE_METADATA_MAX_BYTES,
)
.expect("custom ceiling accepts");
assert_eq!(parsed.inline_metadata.as_deref(), Some(&oversized[..]));
}
#[test]
fn rejects_truncated_prefix() {
let mut bytes = vec![METADATA_REFERENCE_SECTION_VERSION];
bytes.extend_from_slice(&[0u8; 30]); let mut cursor = ManifestCursor::new(&bytes);
match parse_metadata_reference(&mut cursor) {
Err(CoreError::TooShort { .. }) => {}
other => panic!("expected TooShort, got {other:?}"),
}
}
#[test]
fn rejects_bad_locator_entry_inherited() {
let mut bytes = Vec::new();
bytes.push(METADATA_REFERENCE_SECTION_VERSION);
bytes.extend_from_slice(&sample_hash());
bytes.extend_from_slice(&1u32.to_le_bytes()); bytes.extend_from_slice(&5u32.to_le_bytes()); bytes.extend_from_slice(b"abcde"); bytes.extend_from_slice(&0u32.to_le_bytes()); let mut cursor = ManifestCursor::new(&bytes);
match parse_metadata_reference(&mut cursor) {
Err(CoreError::Corrupt { reason }) => {
assert!(reason.contains("separator"), "got: {reason}");
}
other => panic!("expected Corrupt, got {other:?}"),
}
}
fn make_v2_bytes(
metadata_hash: [u8; 32],
uncompressed_len: u32,
codec: u8,
locator_uris: &[&str],
inline_data: Option<&[u8]>,
) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.push(METADATA_REFERENCE_SECTION_VERSION_2);
bytes.extend_from_slice(&metadata_hash);
bytes.extend_from_slice(&uncompressed_len.to_le_bytes());
bytes.push(codec);
let locator_count = u32::try_from(locator_uris.len()).expect("count fits u32");
bytes.extend_from_slice(&locator_count.to_le_bytes());
for uri in locator_uris {
bytes.extend(make_locator_bytes(uri));
}
let inline_len =
inline_data.map_or(0u32, |b| u32::try_from(b.len()).expect("len fits u32"));
bytes.extend_from_slice(&inline_len.to_le_bytes());
if let Some(blob) = inline_data {
bytes.extend(blob);
}
bytes
}
#[test]
fn v2_store_codec_round_trips() {
let blob = b"hello metadata blob world";
let hash = crate::merkle::hash_section(blob);
let bytes = make_v2_bytes(hash, blob.len() as u32, 0x00, &[], Some(blob));
let mut cursor = ManifestCursor::new(&bytes);
let parsed = parse_metadata_reference(&mut cursor).expect("v2 parses");
assert_eq!(parsed.metadata_hash, hash);
assert_eq!(parsed.codec, 0x00);
assert_eq!(parsed.uncompressed_len, u32::try_from(blob.len()).unwrap());
assert_eq!(parsed.inline_metadata.as_deref(), Some(blob.as_slice()));
}
#[test]
fn v2_brotli_codec_decompresses_inline() {
let blob = b"the quick brown fox jumps over the lazy dog".repeat(50);
let hash = crate::merkle::hash_section(&blob);
let compressed = crate::codec::compress(0x04, &blob).expect("brotli compress");
assert!(
compressed.len() < blob.len(),
"brotli should beat store on repetitive input"
);
let bytes = make_v2_bytes(
hash,
u32::try_from(blob.len()).unwrap(),
0x04,
&[],
Some(&compressed),
);
let mut cursor = ManifestCursor::new(&bytes);
let parsed = parse_metadata_reference(&mut cursor).expect("v2 brotli parses");
assert_eq!(parsed.codec, 0x04);
assert_eq!(parsed.uncompressed_len, u32::try_from(blob.len()).unwrap());
assert_eq!(parsed.inline_metadata.as_deref(), Some(blob.as_slice()));
}
#[test]
fn v2_rejects_uncompressed_length_mismatch() {
let blob = b"hello";
let compressed = crate::codec::compress(0x04, blob).expect("brotli");
let hash = crate::merkle::hash_section(blob);
let bytes = make_v2_bytes(hash, 999, 0x04, &[], Some(&compressed));
let mut cursor = ManifestCursor::new(&bytes);
match parse_metadata_reference(&mut cursor) {
Err(CoreError::Corrupt { reason }) => {
assert!(
reason.contains("length") || reason.contains("decompress"),
"got: {reason}"
);
}
other => panic!("expected Corrupt, got {other:?}"),
}
}
#[test]
fn unknown_section_version_above_2_rejected() {
let bytes = make_metadata_reference_bytes(99, [0u8; 32], &[], Some(b"inline blob"));
let mut cursor = ManifestCursor::new(&bytes);
match parse_metadata_reference(&mut cursor) {
Err(CoreError::UnsupportedFeature { feature }) => {
assert!(feature.contains("version 99"), "got: {feature}");
}
other => panic!("expected UnsupportedFeature, got {other:?}"),
}
}
}