Skip to main content

onelf_format/
footer.rs

1//! Footer structure and serialization
2//!
3//! The footer is located at the end of every ONELF package and contains:
4//! - Magic bytes for identification
5//! - Format version
6//! - Offsets to manifest, payload, and optional dictionary
7//! - Checksums for integrity verification
8//!
9//! # Structure
10//!
11//! The footer is exactly 76 bytes and is organized as follows:
12//!
13//! ```text
14//! Offset  Size    Field
15//! ------  -------  -------------------
16//! 0      8        Magic: "ONELF\0\x01\x00"
17//! 8      2        Format version (u16)
18//! 10     2        Flags (u16)
19//! 12     8        Manifest offset (u64)
20//! 20     8        Manifest compressed size (u64)
21//! 28     8        Manifest original size (u64)
22//! 36     8        Payload offset (u64)
23//! 44     8        Payload total size (u64)
24//! 52     8        Dictionary offset (u64)
25//! 60     4        Dictionary size (u32)
26//! 64     4        Manifest checksum (xxh32)
27//! 68     8        End magic: "FLENONE\x00"
28//! ```
29//!
30//! # Example
31//!
32//! ```ignore
33//! use onelf_format::Footer;
34//!
35//! let footer = Footer {
36//!     format_version: 1,
37//!     // ... other fields
38//! };
39//! ```
40
41use std::io::{self, Read, Write};
42
43pub const FOOTER_SIZE: usize = 76;
44pub const MAGIC: [u8; 8] = *b"ONELF\x00\x01\x00";
45pub const END_MAGIC: [u8; 8] = *b"FLENONE\x00";
46
47bitflags! {
48    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
49    pub struct Flags: u16 {
50        const HAS_DICT       = 1 << 0;
51        const MEMFD_HINT     = 1 << 1;
52        const SHARUN_COMPAT  = 1 << 2;
53        /// Payload blocks are stored raw (no zstd). `compressed_size`
54        /// equals `original_size` for every block; the runtime reads
55        /// payload bytes directly without decompression.
56        const STORED         = 1 << 3;
57        /// The package records an update URL but carries no updater, so
58        /// it is updated by something else (a package manager, a
59        /// deployment system). Set only for that case, which leaves every
60        /// package built before this flag existed correctly describing
61        /// itself as carrying an embedded updater.
62        const EXTERNAL_UPDATER = 1 << 4;
63        /// Do not put the host's library directories on the search path.
64        ///
65        /// The runtime adds them so host GPU drivers stay reachable, but
66        /// they hold the whole system's libraries, so any soname the
67        /// bundle is missing is silently satisfied from the host and
68        /// loaded next to the bundled libc. Set for packages that need
69        /// nothing from the host.
70        ///
71        /// Polarity is deliberate: unset means "expose", so every package
72        /// built before this flag existed keeps its behaviour.
73        const NO_HOST_LIB_DIRS = 1 << 5;
74    }
75}
76
77#[derive(Debug, Clone)]
78pub struct Footer {
79    /// Format version number (currently 1).
80    pub format_version: u16,
81    /// Feature flags describing optional sections and capabilities.
82    pub flags: Flags,
83    /// Byte offset where the compressed manifest begins.
84    pub manifest_offset: u64,
85    /// Size of the manifest after compression.
86    pub manifest_compressed: u64,
87    /// Size of the manifest before compression.
88    pub manifest_original: u64,
89    /// Byte offset where the payload section begins.
90    pub payload_offset: u64,
91    /// Total size of the payload section in bytes.
92    pub payload_size: u64,
93    /// Byte offset of the zstd dictionary, or 0 if absent.
94    pub dict_offset: u64,
95    /// Size of the zstd dictionary in bytes, or 0 if absent.
96    pub dict_size: u32,
97    /// xxHash32 checksum of the *uncompressed* manifest bytes, verified
98    /// before the manifest is trusted.
99    pub manifest_checksum: [u8; 4],
100}
101
102impl Footer {
103    /// Whether the payload is stored raw (no zstd). When true the runtime
104    /// must skip decompression and use payload bytes directly.
105    pub fn is_stored(&self) -> bool {
106        self.flags.contains(Flags::STORED)
107    }
108
109    pub fn write_to<W: Write>(&self, w: &mut W) -> io::Result<()> {
110        w.write_all(&MAGIC)?; // 8
111        w.write_all(&self.format_version.to_le_bytes())?; // 2
112        w.write_all(&self.flags.bits().to_le_bytes())?; // 2
113        w.write_all(&self.manifest_offset.to_le_bytes())?; // 8
114        w.write_all(&self.manifest_compressed.to_le_bytes())?; // 8
115        w.write_all(&self.manifest_original.to_le_bytes())?; // 8
116        w.write_all(&self.payload_offset.to_le_bytes())?; // 8
117        w.write_all(&self.payload_size.to_le_bytes())?; // 8
118        w.write_all(&self.dict_offset.to_le_bytes())?; // 8
119        w.write_all(&self.dict_size.to_le_bytes())?; // 4
120        w.write_all(&self.manifest_checksum)?; // 4
121        w.write_all(&END_MAGIC)?; // 8
122        Ok(()) // = 76
123    }
124
125    pub fn read_from<R: Read>(r: &mut R) -> io::Result<Self> {
126        let mut buf = [0u8; FOOTER_SIZE];
127        r.read_exact(&mut buf)?;
128        Self::from_bytes(&buf)
129    }
130
131    pub fn from_bytes(buf: &[u8; FOOTER_SIZE]) -> io::Result<Self> {
132        if buf[0..8] != MAGIC {
133            return Err(io::Error::new(
134                io::ErrorKind::InvalidData,
135                "invalid onelf magic",
136            ));
137        }
138        if buf[68..76] != END_MAGIC {
139            return Err(io::Error::new(
140                io::ErrorKind::InvalidData,
141                "invalid onelf end magic",
142            ));
143        }
144
145        let format_version = u16::from_le_bytes(buf[8..10].try_into().unwrap());
146        if format_version != 1 {
147            return Err(io::Error::new(
148                io::ErrorKind::InvalidData,
149                format!("unsupported format version: {}", format_version),
150            ));
151        }
152
153        let flags_raw = u16::from_le_bytes(buf[10..12].try_into().unwrap());
154        let flags = Flags::from_bits_retain(flags_raw);
155
156        Ok(Footer {
157            format_version,
158            flags,
159            manifest_offset: u64::from_le_bytes(buf[12..20].try_into().unwrap()),
160            manifest_compressed: u64::from_le_bytes(buf[20..28].try_into().unwrap()),
161            manifest_original: u64::from_le_bytes(buf[28..36].try_into().unwrap()),
162            payload_offset: u64::from_le_bytes(buf[36..44].try_into().unwrap()),
163            payload_size: u64::from_le_bytes(buf[44..52].try_into().unwrap()),
164            dict_offset: u64::from_le_bytes(buf[52..60].try_into().unwrap()),
165            dict_size: u32::from_le_bytes(buf[60..64].try_into().unwrap()),
166            manifest_checksum: buf[64..68].try_into().unwrap(),
167        })
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    fn sample() -> Footer {
176        Footer {
177            format_version: 1,
178            flags: Flags::HAS_DICT | Flags::STORED,
179            manifest_offset: 0x1122,
180            manifest_compressed: 0x33,
181            manifest_original: 0x44,
182            payload_offset: 0x55,
183            payload_size: 0x66,
184            dict_offset: 0x77,
185            dict_size: 0x88,
186            manifest_checksum: [1, 2, 3, 4],
187        }
188    }
189
190    #[test]
191    fn footer_roundtrips() {
192        let f = sample();
193        let mut buf = Vec::new();
194        f.write_to(&mut buf).unwrap();
195        assert_eq!(buf.len(), FOOTER_SIZE);
196        let back = Footer::from_bytes(&buf.try_into().unwrap()).unwrap();
197        assert_eq!(back.format_version, f.format_version);
198        assert_eq!(back.flags, f.flags);
199        assert_eq!(back.manifest_offset, f.manifest_offset);
200        assert_eq!(back.dict_size, f.dict_size);
201        assert_eq!(back.manifest_checksum, f.manifest_checksum);
202    }
203
204    #[test]
205    fn malformed_footers_error_without_panic() {
206        let mut buf = Vec::new();
207        sample().write_to(&mut buf).unwrap();
208
209        // Bad start magic.
210        let mut bad = buf.clone();
211        bad[0] ^= 0xff;
212        assert!(Footer::from_bytes(&bad.clone().try_into().unwrap()).is_err());
213
214        // Bad end magic.
215        let mut bad = buf.clone();
216        bad[68] ^= 0xff;
217        assert!(Footer::from_bytes(&bad.clone().try_into().unwrap()).is_err());
218
219        // Unsupported version.
220        let mut bad = buf.clone();
221        bad[8..10].copy_from_slice(&2u16.to_le_bytes());
222        assert!(Footer::from_bytes(&bad.try_into().unwrap()).is_err());
223    }
224}