use super::UrnaView;
use crate::error::UrnaError;
use crate::layout::{
LEGACY_MAGIC, SECTION_ALIGNMENT, SectionEntry, URNA_FOOTER_SIZE, URNA_HEADER_SIZE, URNA_MAGIC,
URNA_SECTION_ENTRY_SIZE, URNA_VERSION_MAJOR, URNA_VERSION_MINOR, UrnaFooter, UrnaHeader,
};
use crate::manifest::Manifest;
impl<'a> UrnaView<'a> {
pub fn from_bytes(data: &'a [u8]) -> crate::Result<Self> {
if data.len() < URNA_HEADER_SIZE + URNA_FOOTER_SIZE {
return Err(UrnaError::FileTruncated);
}
let mut header = UrnaHeader::default();
header
.as_bytes_mut()
.copy_from_slice(&data[..URNA_HEADER_SIZE]);
if &header.magic != URNA_MAGIC && &header.magic != LEGACY_MAGIC {
return Err(UrnaError::MagicMismatch {
expected: *URNA_MAGIC,
got: header.magic,
});
}
if header.version_major != URNA_VERSION_MAJOR || header.version_minor > URNA_VERSION_MINOR {
return Err(UrnaError::UnsupportedVersion(
header.version_major,
header.version_minor,
));
}
header.validate_checksum()?;
if header.file_size as usize != data.len() {
return Err(UrnaError::FileSizeMismatch {
expected: header.file_size,
got: data.len() as u64,
});
}
let section_table_offset = header.section_table_offset as usize;
let section_table_count = header.section_table_count as usize;
let section_table_end = section_table_offset
.checked_add(
section_table_count
.checked_mul(URNA_SECTION_ENTRY_SIZE)
.ok_or(UrnaError::SectionOffsetOutOfBounds {
section_id: 0,
offset: 0,
})?,
)
.ok_or(UrnaError::SectionOffsetOutOfBounds {
section_id: 0,
offset: 0,
})?;
if section_table_end > data.len().saturating_sub(URNA_FOOTER_SIZE) {
return Err(UrnaError::FileTruncated);
}
let mut section_table = Vec::with_capacity(section_table_count);
for i in 0..section_table_count {
let off = section_table_offset + i * URNA_SECTION_ENTRY_SIZE;
let mut entry = SectionEntry::new(0, 0, 0);
entry
.as_bytes_mut()
.copy_from_slice(&data[off..off + URNA_SECTION_ENTRY_SIZE]);
section_table.push(entry);
}
let body_end = data.len() - URNA_FOOTER_SIZE;
for entry in §ion_table {
super::validate::validate_encoding_for_section(entry.section_id, entry.encoding)?;
if entry.offset % SECTION_ALIGNMENT != 0 {
return Err(UrnaError::SectionMisaligned {
section_id: entry.section_id,
offset: entry.offset,
alignment: SECTION_ALIGNMENT,
});
}
let start = entry.offset as usize;
let end = start.checked_add(entry.size as usize).ok_or(
UrnaError::SectionOffsetOutOfBounds {
section_id: entry.section_id,
offset: entry.offset,
},
)?;
if end > body_end {
return Err(UrnaError::SectionOffsetOutOfBounds {
section_id: entry.section_id,
offset: entry.offset,
});
}
entry.validate_checksum(&data[start..end])?;
}
let manifest_offset = header.manifest_offset as usize;
let manifest_size = header.manifest_size as usize;
if manifest_offset
.checked_add(manifest_size)
.map(|e| e > body_end)
.unwrap_or(true)
{
return Err(UrnaError::FileTruncated);
}
let manifest_data = &data[manifest_offset..manifest_offset + manifest_size];
let manifest: Manifest = serde_json::from_slice(manifest_data)?;
manifest.validate()?;
let footer = UrnaFooter::from_bytes(&data[body_end..])?;
let computed = UrnaFooter::compute_file_hash(&data[..body_end]);
if computed != footer.file_hash {
return Err(UrnaError::FooterHashMismatch);
}
if manifest.embedding_dim != header.embedding_dim {
return Err(UrnaError::ManifestInvalid(
"manifest embedding_dim disagrees with header".into(),
));
}
if manifest.n_chunks != header.n_chunks {
return Err(UrnaError::ManifestInvalid(
"manifest n_chunks disagrees with header".into(),
));
}
let view = Self {
data,
header,
section_table,
manifest,
footer,
};
view.check_required_sections()?;
view.validate_embeddings_layout()?;
view.validate_space_bands()?;
view.validate_search_contract()?;
Ok(view)
}
}