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}
67
68pub const BLOCK_SIZE: usize = 24;
69
70impl Block {
71    pub fn write_to<W: Write>(&self, w: &mut W) -> io::Result<()> {
72        w.write_all(&self.payload_offset.to_le_bytes())?;
73        w.write_all(&self.compressed_size.to_le_bytes())?;
74        w.write_all(&self.original_size.to_le_bytes())?;
75        Ok(())
76    }
77
78    pub fn read_from<R: Read>(r: &mut R) -> io::Result<Self> {
79        let mut buf = [0u8; BLOCK_SIZE];
80        r.read_exact(&mut buf)?;
81
82        Ok(Block {
83            payload_offset: u64::from_le_bytes(buf[0..8].try_into().unwrap()),
84            compressed_size: u64::from_le_bytes(buf[8..16].try_into().unwrap()),
85            original_size: u64::from_le_bytes(buf[16..24].try_into().unwrap()),
86        })
87    }
88}
89
90#[derive(Debug, Clone)]
91pub struct EntryPoint {
92    /// Offset into the string table for this entrypoint's name.
93    pub name: u32,
94    /// Index of the filesystem entry this entrypoint executes.
95    pub target_entry: u32,
96    /// Offset into the string table for the argument string.
97    pub args: u32,
98    /// Working directory strategy when launching this entrypoint.
99    pub working_dir: WorkingDir,
100    /// Behavioral flags for this entrypoint.
101    pub flags: EntryPointFlags,
102}
103
104/// Size of serialized EntryPoint: 4+4+4+1+1 = 14 bytes
105pub const ENTRYPOINT_SIZE: usize = 14;
106
107impl EntryPoint {
108    pub fn write_to<W: Write>(&self, w: &mut W) -> io::Result<()> {
109        w.write_all(&self.name.to_le_bytes())?;
110        w.write_all(&self.target_entry.to_le_bytes())?;
111        w.write_all(&self.args.to_le_bytes())?;
112        w.write_all(&[self.working_dir as u8])?;
113        w.write_all(&[self.flags.bits()])?;
114        Ok(())
115    }
116
117    pub fn read_from<R: Read>(r: &mut R) -> io::Result<Self> {
118        let mut buf = [0u8; ENTRYPOINT_SIZE];
119        r.read_exact(&mut buf)?;
120
121        Ok(EntryPoint {
122            name: u32::from_le_bytes(buf[0..4].try_into().unwrap()),
123            target_entry: u32::from_le_bytes(buf[4..8].try_into().unwrap()),
124            args: u32::from_le_bytes(buf[8..12].try_into().unwrap()),
125            working_dir: WorkingDir::try_from(buf[12])?,
126            flags: EntryPointFlags::from_bits_truncate(buf[13]),
127        })
128    }
129
130    pub fn is_memfd_eligible(&self) -> bool {
131        self.flags.contains(EntryPointFlags::MEMFD_ELIGIBLE)
132    }
133}
134
135#[derive(Debug, Clone)]
136pub struct Entry {
137    /// Type of this entry (file, directory, or symlink).
138    pub kind: EntryKind,
139    /// Index of the parent directory entry, or `u32::MAX` for top-level entries.
140    pub parent: u32,
141    /// Offset into the string table for this entry's name.
142    pub name: u32,
143    /// Unix file mode (permissions and type bits).
144    pub mode: u32,
145    /// Modification time: seconds since Unix epoch.
146    pub mtime_secs: u64,
147    /// Modification time: nanosecond component.
148    pub mtime_nsec: u32,
149    /// BLAKE3 hash of the file content (files only).
150    pub content_hash: [u8; 32],
151    /// Number of compressed payload blocks (files only).
152    pub num_blocks: u32,
153    /// Compressed payload blocks containing this file's data (files only).
154    pub blocks: Vec<Block>,
155    /// Offset into the string table for the symlink target path (symlinks only).
156    pub symlink_target: u32,
157}
158
159/// Size of serialized Entry (without blocks):
160/// 1 + 4 + 4 + 4 + 8 + 4 + 32 + 4 + 4 = 65 bytes
161/// Plus num_blocks * BLOCK_SIZE bytes of block data
162pub const ENTRY_HEADER_SIZE: usize = 65;
163
164impl Entry {
165    pub fn write_to<W: Write>(&self, w: &mut W) -> io::Result<()> {
166        w.write_all(&[self.kind as u8])?;
167        w.write_all(&self.parent.to_le_bytes())?;
168        w.write_all(&self.name.to_le_bytes())?;
169        w.write_all(&self.mode.to_le_bytes())?;
170        w.write_all(&self.mtime_secs.to_le_bytes())?;
171        w.write_all(&self.mtime_nsec.to_le_bytes())?;
172        w.write_all(&self.content_hash)?;
173        w.write_all(&self.num_blocks.to_le_bytes())?;
174        w.write_all(&self.symlink_target.to_le_bytes())?;
175        for block in &self.blocks {
176            block.write_to(w)?;
177        }
178        Ok(())
179    }
180
181    pub fn read_from<R: Read>(r: &mut R) -> io::Result<Self> {
182        let mut buf = [0u8; ENTRY_HEADER_SIZE];
183        r.read_exact(&mut buf)?;
184
185        let kind = EntryKind::try_from(buf[0])?;
186        let parent = u32::from_le_bytes(buf[1..5].try_into().unwrap());
187        let name = u32::from_le_bytes(buf[5..9].try_into().unwrap());
188        let mode = u32::from_le_bytes(buf[9..13].try_into().unwrap());
189        let mtime_secs = u64::from_le_bytes(buf[13..21].try_into().unwrap());
190        let mtime_nsec = u32::from_le_bytes(buf[21..25].try_into().unwrap());
191        let mut content_hash = [0u8; 32];
192        content_hash.copy_from_slice(&buf[25..57]);
193        let num_blocks = u32::from_le_bytes(buf[57..61].try_into().unwrap());
194        let symlink_target = u32::from_le_bytes(buf[61..65].try_into().unwrap());
195
196        let mut blocks = Vec::with_capacity(num_blocks as usize);
197        for _ in 0..num_blocks {
198            blocks.push(Block::read_from(r)?);
199        }
200
201        Ok(Entry {
202            kind,
203            parent,
204            name,
205            mode,
206            mtime_secs,
207            mtime_nsec,
208            content_hash,
209            num_blocks,
210            blocks,
211            symlink_target,
212        })
213    }
214}