Skip to main content

onelf_format/
manifest.rs

1use std::collections::HashMap;
2use std::io::{self, Cursor, Read, Write};
3
4use crate::entry::{Entry, EntryPoint};
5
6pub const MANIFEST_HEADER_SIZE: usize = 2 + 4 + 4 + 2 + 2 + 2 + 2 + 32; // 50 bytes
7
8#[derive(Debug, Clone)]
9pub struct ManifestHeader {
10    /// Manifest format version.
11    pub version: u16,
12    /// Total number of filesystem entries in the manifest.
13    pub entry_count: u32,
14    /// Size of the string table in bytes.
15    pub string_table_size: u32,
16    /// Number of entrypoints defined in this package.
17    pub entrypoint_count: u16,
18    /// Index of the default entrypoint to use when none is specified.
19    pub default_entrypoint: u16,
20    /// Number of library directory paths in the manifest.
21    pub lib_dir_count: u16,
22    /// Offset into the string table for the package name.
23    pub name_offset: u16,
24    /// Unique package identifier (BLAKE3 hash).
25    pub package_id: [u8; 32],
26}
27
28impl ManifestHeader {
29    pub fn write_to<W: Write>(&self, w: &mut W) -> io::Result<()> {
30        w.write_all(&self.version.to_le_bytes())?;
31        w.write_all(&self.entry_count.to_le_bytes())?;
32        w.write_all(&self.string_table_size.to_le_bytes())?;
33        w.write_all(&self.entrypoint_count.to_le_bytes())?;
34        w.write_all(&self.default_entrypoint.to_le_bytes())?;
35        w.write_all(&self.lib_dir_count.to_le_bytes())?;
36        w.write_all(&self.name_offset.to_le_bytes())?;
37        w.write_all(&self.package_id)?;
38        Ok(())
39    }
40
41    pub fn read_from<R: Read>(r: &mut R) -> io::Result<Self> {
42        let mut buf = [0u8; MANIFEST_HEADER_SIZE];
43        r.read_exact(&mut buf)?;
44
45        let mut package_id = [0u8; 32];
46        package_id.copy_from_slice(&buf[18..50]);
47
48        Ok(ManifestHeader {
49            version: u16::from_le_bytes(buf[0..2].try_into().unwrap()),
50            entry_count: u32::from_le_bytes(buf[2..6].try_into().unwrap()),
51            string_table_size: u32::from_le_bytes(buf[6..10].try_into().unwrap()),
52            entrypoint_count: u16::from_le_bytes(buf[10..12].try_into().unwrap()),
53            default_entrypoint: u16::from_le_bytes(buf[12..14].try_into().unwrap()),
54            lib_dir_count: u16::from_le_bytes(buf[14..16].try_into().unwrap()),
55            name_offset: u16::from_le_bytes(buf[16..18].try_into().unwrap()),
56            package_id,
57        })
58    }
59}
60
61#[derive(Debug, Clone)]
62pub struct Manifest {
63    /// Fixed-size header containing counts, offsets, and the package ID.
64    pub header: ManifestHeader,
65    /// Named executable entrypoints into the package.
66    pub entrypoints: Vec<EntryPoint>,
67    /// All filesystem entries (files, directories, symlinks) in the package.
68    pub entries: Vec<Entry>,
69    /// Library directory string table offsets for `LD_LIBRARY_PATH` injection.
70    pub lib_dir_offsets: Vec<u32>,
71    /// Null-terminated string pool referenced by offset from entries and entrypoints.
72    pub string_table: Vec<u8>,
73}
74
75impl Manifest {
76    pub fn serialize(&self) -> io::Result<Vec<u8>> {
77        let mut buf = Vec::new();
78        self.header.write_to(&mut buf)?;
79        for ep in &self.entrypoints {
80            ep.write_to(&mut buf)?;
81        }
82        for entry in &self.entries {
83            entry.write_to(&mut buf)?;
84        }
85        for &offset in &self.lib_dir_offsets {
86            buf.write_all(&offset.to_le_bytes())?;
87        }
88        buf.write_all(&self.string_table)?;
89        Ok(buf)
90    }
91
92    pub fn deserialize(data: &[u8]) -> io::Result<Self> {
93        let mut cursor = Cursor::new(data);
94        let header = ManifestHeader::read_from(&mut cursor)?;
95
96        if header.version != 1 {
97            return Err(io::Error::new(
98                io::ErrorKind::InvalidData,
99                format!("unsupported manifest version: {}", header.version),
100            ));
101        }
102
103        let mut entrypoints = Vec::with_capacity(header.entrypoint_count as usize);
104        for _ in 0..header.entrypoint_count {
105            entrypoints.push(EntryPoint::read_from(&mut cursor)?);
106        }
107
108        let mut entries = Vec::with_capacity(header.entry_count as usize);
109        for _ in 0..header.entry_count {
110            entries.push(Entry::read_from(&mut cursor)?);
111        }
112
113        let mut lib_dir_offsets = Vec::with_capacity(header.lib_dir_count as usize);
114        for _ in 0..header.lib_dir_count {
115            let mut offset_buf = [0u8; 4];
116            cursor.read_exact(&mut offset_buf)?;
117            lib_dir_offsets.push(u32::from_le_bytes(offset_buf));
118        }
119
120        let mut string_table = vec![0u8; header.string_table_size as usize];
121        cursor.read_exact(&mut string_table)?;
122
123        Ok(Manifest {
124            header,
125            entrypoints,
126            entries,
127            lib_dir_offsets,
128            string_table,
129        })
130    }
131
132    /// Returns the package name, or empty string if unset.
133    pub fn name(&self) -> &str {
134        if self.header.name_offset > 0 {
135            self.get_string(self.header.name_offset as u32)
136        } else {
137            ""
138        }
139    }
140
141    /// Returns resolved library directory paths.
142    pub fn lib_dirs(&self) -> Vec<&str> {
143        self.lib_dir_offsets
144            .iter()
145            .map(|&offset| self.get_string(offset))
146            .collect()
147    }
148
149    pub fn get_string(&self, offset: u32) -> &str {
150        let start = offset as usize;
151        let end = self.string_table[start..]
152            .iter()
153            .position(|&b| b == 0)
154            .map(|p| start + p)
155            .unwrap_or(self.string_table.len());
156        std::str::from_utf8(&self.string_table[start..end]).unwrap_or("")
157    }
158
159    /// Check if a top-level directory with the given name exists
160    pub fn has_toplevel_dir(&self, name: &str) -> bool {
161        use crate::entry::EntryKind;
162        self.entries.iter().any(|e| {
163            e.kind == EntryKind::Dir && e.parent == u32::MAX && self.get_string(e.name) == name
164        })
165    }
166
167    /// Find the path to a lib directory if one exists
168    /// Returns the path (e.g., "lib" or "overlayed/lib") or empty string if not found
169    pub fn find_lib_dir(&self) -> String {
170        use crate::entry::EntryKind;
171        for (i, e) in self.entries.iter().enumerate() {
172            if e.kind == EntryKind::Dir && self.get_string(e.name) == "lib" {
173                return self.entry_path(i);
174            }
175        }
176        String::new()
177    }
178
179    /// Reconstruct the full path for an entry by walking parent chain
180    pub fn entry_path(&self, index: usize) -> String {
181        let mut parts = Vec::new();
182        let mut idx = index;
183        loop {
184            let entry = &self.entries[idx];
185            let name = self.get_string(entry.name);
186            if name.is_empty() {
187                break;
188            }
189            parts.push(name);
190            if entry.parent == u32::MAX {
191                break;
192            }
193            idx = entry.parent as usize;
194        }
195        parts.reverse();
196        parts.join("/")
197    }
198}
199
200/// Helper for building a string table during packing.
201#[derive(Debug, Default)]
202pub struct StringTableBuilder {
203    data: Vec<u8>,
204    index: HashMap<String, u32>,
205}
206
207impl StringTableBuilder {
208    pub fn new() -> Self {
209        Self {
210            data: Vec::new(),
211            index: HashMap::new(),
212        }
213    }
214
215    /// Add a string and return its offset in the table.
216    pub fn add(&mut self, s: &str) -> u32 {
217        if let Some(&offset) = self.index.get(s) {
218            return offset;
219        }
220        let offset = self.data.len() as u32;
221        self.data.extend_from_slice(s.as_bytes());
222        self.data.push(0);
223        self.index.insert(s.to_owned(), offset);
224        offset
225    }
226
227    pub fn finish(self) -> Vec<u8> {
228        self.data
229    }
230
231    pub fn len(&self) -> u32 {
232        self.data.len() as u32
233    }
234}