Skip to main content

packed_spatial_index/persistence/
metadata.rs

1use super::container::{find_chunk, parse_container};
2use super::{ByteWriter, LoadError, TAG_TREE, read_u16_at, read_u32_at};
3
4/// Optional descriptive metadata chunk (CRS / content type / attribution).
5/// Optional — readers that do not care skip it.
6pub(crate) const TAG_META: [u8; 4] = *b"META";
7
8// `META` field ids. The chunk is a flat list of `(id: u16, len: u32, bytes)`
9// fields read until the chunk ends; an unknown id is skipped, so new fields are
10// non-breaking. Values are opaque UTF-8 strings the writer supplied.
11const META_CRS: u16 = 0;
12const META_CONTENT_TYPE: u16 = 1;
13const META_ATTRIBUTION: u16 = 2;
14
15/// Descriptive fields to write into a `META` chunk (borrowed, write side).
16#[derive(Default)]
17pub(crate) struct MetaFields<'a> {
18    pub(crate) crs: Option<&'a str>,
19    pub(crate) content_type: Option<&'a str>,
20    pub(crate) attribution: Option<&'a str>,
21}
22
23impl MetaFields<'_> {
24    pub(crate) fn is_empty(&self) -> bool {
25        self.crs.is_none() && self.content_type.is_none() && self.attribution.is_none()
26    }
27
28    /// Byte length of the `META` chunk content for these fields.
29    pub(crate) fn content_len(&self) -> usize {
30        [self.crs, self.content_type, self.attribution]
31            .into_iter()
32            .flatten()
33            .map(|s| 6 + s.len()) // id(2) + len(4) + bytes
34            .sum()
35    }
36}
37
38/// Descriptive metadata read from a file's `META` chunk. Every field is an opaque
39/// string the writer supplied; this crate does not interpret them (e.g. the CRS
40/// is whatever identifier the producer chose, such as `"EPSG:4326"`).
41#[derive(Clone, Debug, Default, PartialEq, Eq)]
42pub struct FileMetadata {
43    /// Coordinate reference system identifier, if present.
44    pub crs: Option<String>,
45    /// Payload content type (media type), if present.
46    pub content_type: Option<String>,
47    /// Attribution / license string, if present.
48    pub attribution: Option<String>,
49}
50
51impl ByteWriter<'_> {
52    pub(crate) fn write_meta(&mut self, fields: &MetaFields<'_>) {
53        for (id, value) in [
54            (META_CRS, fields.crs),
55            (META_CONTENT_TYPE, fields.content_type),
56            (META_ATTRIBUTION, fields.attribution),
57        ] {
58            if let Some(s) = value {
59                self.write_u16(id);
60                self.write_u32(s.len() as u32);
61                self.write_bytes(s.as_bytes());
62            }
63        }
64    }
65}
66
67/// Read the optional descriptive metadata from a serialized index, without
68/// loading the index. Returns an empty [`FileMetadata`] when there is no `META`
69/// chunk.
70pub fn read_metadata(bytes: &[u8]) -> Result<FileMetadata, LoadError> {
71    let chunks = parse_container(bytes, &[TAG_TREE])?;
72    match find_chunk(&chunks, TAG_META) {
73        Some(m) => parse_meta(&bytes[m.offset..m.offset + m.len]),
74        None => Ok(FileMetadata::default()),
75    }
76}
77
78/// Parse a `META` chunk's flat field list into owned strings.
79fn parse_meta(content: &[u8]) -> Result<FileMetadata, LoadError> {
80    let mut md = FileMetadata::default();
81    let mut off = 0;
82    while off < content.len() {
83        let id = read_u16_at(content, off)?;
84        let len = read_u32_at(content, off + 2)? as usize;
85        let start = off + 6;
86        let end = start.checked_add(len).ok_or(LoadError::IntegerOverflow)?;
87        let bytes = content.get(start..end).ok_or(LoadError::Truncated)?;
88        let s = std::str::from_utf8(bytes).map_err(|_| LoadError::InvalidTree)?;
89        match id {
90            META_CRS => md.crs = Some(s.to_owned()),
91            META_CONTENT_TYPE => md.content_type = Some(s.to_owned()),
92            META_ATTRIBUTION => md.attribution = Some(s.to_owned()),
93            _ => {} // unknown field: skip
94        }
95        off = end;
96    }
97    Ok(md)
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn meta_parses_known_fields_and_skips_unknown() {
106        // A META chunk content with crs(0), an unknown future field(99), and
107        // attribution(2). The unknown field must be skipped, not break parsing.
108        let mut content = Vec::new();
109        let put = |c: &mut Vec<u8>, id: u16, value: &[u8]| {
110            c.extend_from_slice(&id.to_le_bytes());
111            c.extend_from_slice(&(value.len() as u32).to_le_bytes());
112            c.extend_from_slice(value);
113        };
114        put(&mut content, 0, b"EPSG:4326"); // crs
115        put(&mut content, 99, b"from-the-future"); // unknown -> skipped
116        put(&mut content, 2, b"attribution-text"); // attribution
117
118        let md = parse_meta(&content).unwrap();
119        assert_eq!(md.crs.as_deref(), Some("EPSG:4326"));
120        assert_eq!(md.attribution.as_deref(), Some("attribution-text"));
121        assert_eq!(md.content_type, None);
122
123        // Empty content -> all fields absent.
124        assert_eq!(parse_meta(&[]).unwrap(), FileMetadata::default());
125    }
126}