Skip to main content

urna_format/layout/
header.rs

1//! fixed-size binary header (128 bytes). The header contains
2//! everything a reader needs to discover the section table and the
3//! manifest without reading the file body. The `header_checksum` is
4//! the first 8 bytes of `SHA-256` of the header bytes with the
5//! checksum field zeroed.
6
7use crate::error::UrnaError;
8use sha2::{Digest, Sha256};
9
10use super::{URNA_MAGIC, URNA_VERSION_MAJOR, URNA_VERSION_MINOR};
11
12// `Pod` + `Zeroable` are derived, not asserted: the derive fails to
13// compile if the struct ever gains padding or a field that is not valid
14// for every bit pattern, which is exactly the invariant the on-disk
15// byte view below relies on. No `unsafe` in this file.
16#[repr(C)]
17#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
18pub struct UrnaHeader {
19    pub magic: [u8; 4],
20    pub version_major: u16,
21    pub version_minor: u16,
22    pub flags: u32,
23    pub embedding_dim: u32,
24    pub n_chunks: u64,
25    pub n_embeddings: u64,
26    pub file_size: u64,
27    pub section_table_offset: u64,
28    pub section_table_count: u64,
29    pub manifest_offset: u64,
30    pub manifest_size: u64,
31    pub header_checksum: [u8; 8],
32    pub reserved: [u8; 48],
33}
34
35impl UrnaHeader {
36    #[allow(clippy::too_many_arguments)]
37    pub fn new(
38        embedding_dim: u32,
39        n_chunks: u64,
40        n_embeddings: u64,
41        file_size: u64,
42        section_table_offset: u64,
43        section_table_count: u64,
44        manifest_offset: u64,
45        manifest_size: u64,
46    ) -> Self {
47        let mut h = Self {
48            magic: *URNA_MAGIC,
49            version_major: URNA_VERSION_MAJOR,
50            version_minor: URNA_VERSION_MINOR,
51            flags: 0,
52            embedding_dim,
53            n_chunks,
54            n_embeddings,
55            file_size,
56            section_table_offset,
57            section_table_count,
58            manifest_offset,
59            manifest_size,
60            header_checksum: [0; 8],
61            reserved: [0; 48],
62        };
63        h.compute_checksum();
64        h
65    }
66
67    pub fn compute_checksum(&mut self) {
68        let bytes = self.as_bytes_without_checksum();
69        let hash = Sha256::digest(&bytes);
70        self.header_checksum.copy_from_slice(&hash[..8]);
71    }
72
73    pub fn validate_checksum(&self) -> crate::Result<()> {
74        let mut tmp = *self;
75        tmp.header_checksum = [0; 8];
76        let bytes = tmp.as_bytes_without_checksum();
77        let hash = Sha256::digest(&bytes);
78        if hash[..8] != self.header_checksum[..] {
79            return Err(UrnaError::InvalidHeaderChecksum);
80        }
81        Ok(())
82    }
83
84    /// The exact on-disk bytes of this record (little-endian host only,
85    /// which is every supported target; see `layout::tests`).
86    pub fn as_bytes(&self) -> &[u8] {
87        bytemuck::bytes_of(self)
88    }
89
90    /// Mutable view over the on-disk bytes; `from_bytes`-style readers copy
91    /// a slice in here. Sound for any content because every field accepts
92    /// every bit pattern (`Pod`).
93    pub fn as_bytes_mut(&mut self) -> &mut [u8] {
94        bytemuck::bytes_of_mut(self)
95    }
96
97    fn as_bytes_without_checksum(&self) -> Vec<u8> {
98        let bytes = self.as_bytes();
99        let mut v = bytes[..72].to_vec();
100        v.extend_from_slice(&bytes[80..]);
101        v
102    }
103}
104
105impl Default for UrnaHeader {
106    fn default() -> Self {
107        Self::new(0, 0, 0, 128, 128, 0, 128, 0)
108    }
109}