Skip to main content

urna_format/reader/
parse.rs

1//! `UrnaView::from_bytes` - header, section table, manifest, footer
2//! parsing. Validation hooks are in `super::validate` and run after the
3//! basic structure has been recognized.
4
5use super::UrnaView;
6use crate::error::UrnaError;
7use crate::layout::{
8    LEGACY_MAGIC, SECTION_ALIGNMENT, SectionEntry, URNA_FOOTER_SIZE, URNA_HEADER_SIZE, URNA_MAGIC,
9    URNA_SECTION_ENTRY_SIZE, URNA_VERSION_MAJOR, URNA_VERSION_MINOR, UrnaFooter, UrnaHeader,
10};
11use crate::manifest::Manifest;
12
13impl<'a> UrnaView<'a> {
14    pub fn from_bytes(data: &'a [u8]) -> crate::Result<Self> {
15        if data.len() < URNA_HEADER_SIZE + URNA_FOOTER_SIZE {
16            return Err(UrnaError::FileTruncated);
17        }
18
19        // ---- header ----
20        let mut header = UrnaHeader::default();
21        header
22            .as_bytes_mut()
23            .copy_from_slice(&data[..URNA_HEADER_SIZE]);
24
25        if &header.magic != URNA_MAGIC && &header.magic != LEGACY_MAGIC {
26            return Err(UrnaError::MagicMismatch {
27                expected: *URNA_MAGIC,
28                got: header.magic,
29            });
30        }
31        if header.version_major != URNA_VERSION_MAJOR || header.version_minor > URNA_VERSION_MINOR {
32            return Err(UrnaError::UnsupportedVersion(
33                header.version_major,
34                header.version_minor,
35            ));
36        }
37        header.validate_checksum()?;
38
39        if header.file_size as usize != data.len() {
40            return Err(UrnaError::FileSizeMismatch {
41                expected: header.file_size,
42                got: data.len() as u64,
43            });
44        }
45
46        // ---- section table ----
47        let section_table_offset = header.section_table_offset as usize;
48        let section_table_count = header.section_table_count as usize;
49        let section_table_end = section_table_offset
50            .checked_add(
51                section_table_count
52                    .checked_mul(URNA_SECTION_ENTRY_SIZE)
53                    .ok_or(UrnaError::SectionOffsetOutOfBounds {
54                        section_id: 0,
55                        offset: 0,
56                    })?,
57            )
58            .ok_or(UrnaError::SectionOffsetOutOfBounds {
59                section_id: 0,
60                offset: 0,
61            })?;
62        if section_table_end > data.len().saturating_sub(URNA_FOOTER_SIZE) {
63            return Err(UrnaError::FileTruncated);
64        }
65
66        let mut section_table = Vec::with_capacity(section_table_count);
67        for i in 0..section_table_count {
68            let off = section_table_offset + i * URNA_SECTION_ENTRY_SIZE;
69            let mut entry = SectionEntry::new(0, 0, 0);
70            entry
71                .as_bytes_mut()
72                .copy_from_slice(&data[off..off + URNA_SECTION_ENTRY_SIZE]);
73            section_table.push(entry);
74        }
75
76        // ---- section bounds + alignment + encoding + checksums ----
77        let body_end = data.len() - URNA_FOOTER_SIZE;
78        for entry in &section_table {
79            super::validate::validate_encoding_for_section(entry.section_id, entry.encoding)?;
80            if entry.offset % SECTION_ALIGNMENT != 0 {
81                return Err(UrnaError::SectionMisaligned {
82                    section_id: entry.section_id,
83                    offset: entry.offset,
84                    alignment: SECTION_ALIGNMENT,
85                });
86            }
87            let start = entry.offset as usize;
88            let end = start.checked_add(entry.size as usize).ok_or(
89                UrnaError::SectionOffsetOutOfBounds {
90                    section_id: entry.section_id,
91                    offset: entry.offset,
92                },
93            )?;
94            if end > body_end {
95                return Err(UrnaError::SectionOffsetOutOfBounds {
96                    section_id: entry.section_id,
97                    offset: entry.offset,
98                });
99            }
100            entry.validate_checksum(&data[start..end])?;
101        }
102
103        // ---- manifest ----
104        let manifest_offset = header.manifest_offset as usize;
105        let manifest_size = header.manifest_size as usize;
106        if manifest_offset
107            .checked_add(manifest_size)
108            .map(|e| e > body_end)
109            .unwrap_or(true)
110        {
111            return Err(UrnaError::FileTruncated);
112        }
113        let manifest_data = &data[manifest_offset..manifest_offset + manifest_size];
114        let manifest: Manifest = serde_json::from_slice(manifest_data)?;
115        manifest.validate()?;
116
117        // ---- footer hash ----
118        let footer = UrnaFooter::from_bytes(&data[body_end..])?;
119        let computed = UrnaFooter::compute_file_hash(&data[..body_end]);
120        if computed != footer.file_hash {
121            return Err(UrnaError::FooterHashMismatch);
122        }
123
124        // ---- coherence ----
125        if manifest.embedding_dim != header.embedding_dim {
126            return Err(UrnaError::ManifestInvalid(
127                "manifest embedding_dim disagrees with header".into(),
128            ));
129        }
130        if manifest.n_chunks != header.n_chunks {
131            return Err(UrnaError::ManifestInvalid(
132                "manifest n_chunks disagrees with header".into(),
133            ));
134        }
135
136        let view = Self {
137            data,
138            header,
139            section_table,
140            manifest,
141            footer,
142        };
143
144        // ---- semantic validation (delegates to super::validate) ----
145        view.check_required_sections()?;
146        view.validate_embeddings_layout()?;
147        view.validate_space_bands()?;
148        view.validate_search_contract()?;
149
150        Ok(view)
151    }
152}