Skip to main content

onelf_format/
entry.rs

1use std::io::{self, Read, Write};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4#[repr(u8)]
5pub enum EntryKind {
6    Dir = 0,
7    File = 1,
8    Symlink = 2,
9}
10
11impl TryFrom<u8> for EntryKind {
12    type Error = io::Error;
13
14    fn try_from(v: u8) -> Result<Self, Self::Error> {
15        match v {
16            0 => Ok(EntryKind::Dir),
17            1 => Ok(EntryKind::File),
18            2 => Ok(EntryKind::Symlink),
19            _ => Err(io::Error::new(
20                io::ErrorKind::InvalidData,
21                format!("invalid entry kind: {v}"),
22            )),
23        }
24    }
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28#[repr(u8)]
29pub enum WorkingDir {
30    Inherit = 0,
31    PackageRoot = 1,
32    EntrypointParent = 2,
33}
34
35impl TryFrom<u8> for WorkingDir {
36    type Error = io::Error;
37
38    fn try_from(v: u8) -> Result<Self, Self::Error> {
39        match v {
40            0 => Ok(WorkingDir::Inherit),
41            1 => Ok(WorkingDir::PackageRoot),
42            2 => Ok(WorkingDir::EntrypointParent),
43            _ => Err(io::Error::new(
44                io::ErrorKind::InvalidData,
45                format!("invalid working_dir: {v}"),
46            )),
47        }
48    }
49}
50
51bitflags! {
52    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
53    pub struct EntryPointFlags: u8 {
54        const MEMFD_ELIGIBLE = 1 << 0;
55    }
56}
57
58#[derive(Debug, Clone)]
59pub struct Block {
60    /// Byte offset into the payload section where this block's data begins.
61    pub payload_offset: u64,
62    /// Size of the block after compression.
63    pub compressed_size: u64,
64    /// Size of the block before compression.
65    pub original_size: u64,
66    /// BLAKE3 of this block's *decompressed* bytes.
67    ///
68    /// Hashing the decompressed form is what lets a random-access reader
69    /// verify one block without reassembling the whole entry, which is the
70    /// difference between serving a byte of a large file and buffering all
71    /// of it. Zero on manifests written before version 2, where the only
72    /// available check is the whole-entry hash on [`Entry`].
73    pub content_hash: [u8; 32],
74}
75
76/// Serialized width of a block, as written by the current format version.
77pub const BLOCK_SIZE: usize = 56;
78
79/// Serialized width in manifest version 1, which carried no per-block hash.
80pub const BLOCK_SIZE_V1: usize = 24;
81
82impl Block {
83    pub fn write_to<W: Write>(&self, w: &mut W) -> io::Result<()> {
84        w.write_all(&self.payload_offset.to_le_bytes())?;
85        w.write_all(&self.compressed_size.to_le_bytes())?;
86        w.write_all(&self.original_size.to_le_bytes())?;
87        w.write_all(&self.content_hash)?;
88        Ok(())
89    }
90
91    /// Read one block as written by manifest `version`.
92    pub fn read_from<R: Read>(r: &mut R, version: u16) -> io::Result<Self> {
93        let mut buf = [0u8; BLOCK_SIZE];
94        let width = if version >= 2 {
95            BLOCK_SIZE
96        } else {
97            BLOCK_SIZE_V1
98        };
99        r.read_exact(&mut buf[..width])?;
100
101        Ok(Block {
102            payload_offset: u64::from_le_bytes(buf[0..8].try_into().unwrap()),
103            compressed_size: u64::from_le_bytes(buf[8..16].try_into().unwrap()),
104            original_size: u64::from_le_bytes(buf[16..24].try_into().unwrap()),
105            // Left zero for version 1, where it means "no per-block check".
106            content_hash: buf[24..56].try_into().unwrap(),
107        })
108    }
109
110    /// Whether this block carries a usable per-block hash.
111    pub fn has_content_hash(&self) -> bool {
112        self.content_hash != [0u8; 32]
113    }
114}
115
116#[derive(Debug, Clone)]
117pub struct EntryPoint {
118    /// Offset into the string table for this entrypoint's name.
119    pub name: u32,
120    /// Index of the filesystem entry this entrypoint executes.
121    pub target_entry: u32,
122    /// Offset into the string table for the argument string.
123    pub args: u32,
124    /// Working directory strategy when launching this entrypoint.
125    pub working_dir: WorkingDir,
126    /// Behavioral flags for this entrypoint.
127    pub flags: EntryPointFlags,
128}
129
130/// Size of serialized EntryPoint: 4+4+4+1+1 = 14 bytes
131pub const ENTRYPOINT_SIZE: usize = 14;
132
133impl EntryPoint {
134    pub fn write_to<W: Write>(&self, w: &mut W) -> io::Result<()> {
135        w.write_all(&self.name.to_le_bytes())?;
136        w.write_all(&self.target_entry.to_le_bytes())?;
137        w.write_all(&self.args.to_le_bytes())?;
138        w.write_all(&[self.working_dir as u8])?;
139        w.write_all(&[self.flags.bits()])?;
140        Ok(())
141    }
142
143    pub fn read_from<R: Read>(r: &mut R) -> io::Result<Self> {
144        let mut buf = [0u8; ENTRYPOINT_SIZE];
145        r.read_exact(&mut buf)?;
146
147        Ok(EntryPoint {
148            name: u32::from_le_bytes(buf[0..4].try_into().unwrap()),
149            target_entry: u32::from_le_bytes(buf[4..8].try_into().unwrap()),
150            args: u32::from_le_bytes(buf[8..12].try_into().unwrap()),
151            working_dir: WorkingDir::try_from(buf[12])?,
152            flags: EntryPointFlags::from_bits_retain(buf[13]),
153        })
154    }
155
156    pub fn is_memfd_eligible(&self) -> bool {
157        self.flags.contains(EntryPointFlags::MEMFD_ELIGIBLE)
158    }
159}
160
161#[derive(Debug, Clone)]
162pub struct Entry {
163    /// Type of this entry (file, directory, or symlink).
164    pub kind: EntryKind,
165    /// Index of the parent directory entry, or `u32::MAX` for top-level entries.
166    pub parent: u32,
167    /// Offset into the string table for this entry's name.
168    pub name: u32,
169    /// Unix file mode (permissions and type bits).
170    pub mode: u32,
171    /// Modification time: seconds since Unix epoch.
172    pub mtime_secs: u64,
173    /// Modification time: nanosecond component.
174    pub mtime_nsec: u32,
175    /// BLAKE3 hash of the file content (files only).
176    pub content_hash: [u8; 32],
177    /// Compressed payload blocks containing this file's data (files only).
178    /// The serialized block count is derived from `blocks.len()`.
179    pub blocks: Vec<Block>,
180    /// Offset into the string table for the symlink target path (symlinks only).
181    pub symlink_target: u32,
182}
183
184/// Size of serialized Entry (without blocks):
185/// 1 + 4 + 4 + 4 + 8 + 4 + 32 + 4 + 4 = 65 bytes
186/// Plus num_blocks * BLOCK_SIZE bytes of block data
187pub const ENTRY_HEADER_SIZE: usize = 65;
188
189impl Entry {
190    pub fn write_to<W: Write>(&self, w: &mut W) -> io::Result<()> {
191        w.write_all(&[self.kind as u8])?;
192        w.write_all(&self.parent.to_le_bytes())?;
193        w.write_all(&self.name.to_le_bytes())?;
194        w.write_all(&self.mode.to_le_bytes())?;
195        w.write_all(&self.mtime_secs.to_le_bytes())?;
196        w.write_all(&self.mtime_nsec.to_le_bytes())?;
197        w.write_all(&self.content_hash)?;
198        // Block count is derived from the vec so it can never disagree with
199        // the blocks that follow.
200        w.write_all(&(self.blocks.len() as u32).to_le_bytes())?;
201        w.write_all(&self.symlink_target.to_le_bytes())?;
202        for block in &self.blocks {
203            block.write_to(w)?;
204        }
205        Ok(())
206    }
207
208    /// Read one entry as written by manifest `version`.
209    pub fn read_from<R: Read>(r: &mut R, version: u16) -> io::Result<Self> {
210        let mut buf = [0u8; ENTRY_HEADER_SIZE];
211        r.read_exact(&mut buf)?;
212
213        let kind = EntryKind::try_from(buf[0])?;
214        let parent = u32::from_le_bytes(buf[1..5].try_into().unwrap());
215        let name = u32::from_le_bytes(buf[5..9].try_into().unwrap());
216        let mode = u32::from_le_bytes(buf[9..13].try_into().unwrap());
217        let mtime_secs = u64::from_le_bytes(buf[13..21].try_into().unwrap());
218        let mtime_nsec = u32::from_le_bytes(buf[21..25].try_into().unwrap());
219        let mut content_hash = [0u8; 32];
220        content_hash.copy_from_slice(&buf[25..57]);
221        let num_blocks = u32::from_le_bytes(buf[57..61].try_into().unwrap());
222        let symlink_target = u32::from_le_bytes(buf[61..65].try_into().unwrap());
223
224        // Cap the speculative allocation so a crafted `num_blocks` cannot
225        // request gigabytes before the reader hits EOF. The loop still
226        // reads exactly `num_blocks` blocks (growing as needed) and fails
227        // cleanly via `read_exact` when the input is short.
228        let cap = (num_blocks as usize).min(4096);
229        let mut blocks = Vec::with_capacity(cap);
230        for _ in 0..num_blocks {
231            blocks.push(Block::read_from(r, version)?);
232        }
233
234        Ok(Entry {
235            kind,
236            parent,
237            name,
238            mode,
239            mtime_secs,
240            mtime_nsec,
241            content_hash,
242            blocks,
243            symlink_target,
244        })
245    }
246}