1use std::collections::HashMap;
2use std::io::{self, Cursor, Read, Write};
3use std::path::PathBuf;
4
5use crate::entry::{ENTRY_HEADER_SIZE, ENTRYPOINT_SIZE, Entry, EntryPoint};
6
7pub fn is_safe_component(name: &str) -> bool {
11 !name.is_empty() && name != "." && name != ".." && !name.contains('/') && !name.contains('\0')
12}
13
14pub fn symlink_target_within_root(link_rel: &std::path::Path, target: &str) -> bool {
20 use std::ffi::OsString;
21 use std::path::Component;
22
23 if target.is_empty() {
24 return false;
25 }
26 let tpath = std::path::Path::new(target);
27 if tpath.is_absolute() {
28 return false;
29 }
30 let mut stack: Vec<OsString> = Vec::new();
31 let mut walk = |path: &std::path::Path| -> bool {
32 for c in path.components() {
33 match c {
34 Component::Normal(s) => stack.push(s.to_os_string()),
35 Component::ParentDir => {
36 if stack.pop().is_none() {
37 return false;
38 }
39 }
40 Component::CurDir => {}
41 _ => return false,
42 }
43 }
44 true
45 };
46 let parent_ok = link_rel.parent().map(&mut walk).unwrap_or(true);
48 parent_ok && walk(tpath)
49}
50
51pub const MANIFEST_HEADER_SIZE: usize = 2 + 4 + 4 + 2 + 2 + 2 + 2 + 32; #[derive(Debug, Clone)]
54pub struct ManifestHeader {
55 pub version: u16,
57 pub entry_count: u32,
59 pub string_table_size: u32,
61 pub entrypoint_count: u16,
63 pub default_entrypoint: u16,
65 pub lib_dir_count: u16,
67 pub name_offset: u16,
69 pub package_id: [u8; 32],
71}
72
73impl ManifestHeader {
74 pub fn write_to<W: Write>(&self, w: &mut W) -> io::Result<()> {
75 w.write_all(&self.version.to_le_bytes())?;
76 w.write_all(&self.entry_count.to_le_bytes())?;
77 w.write_all(&self.string_table_size.to_le_bytes())?;
78 w.write_all(&self.entrypoint_count.to_le_bytes())?;
79 w.write_all(&self.default_entrypoint.to_le_bytes())?;
80 w.write_all(&self.lib_dir_count.to_le_bytes())?;
81 w.write_all(&self.name_offset.to_le_bytes())?;
82 w.write_all(&self.package_id)?;
83 Ok(())
84 }
85
86 pub fn read_from<R: Read>(r: &mut R) -> io::Result<Self> {
87 let mut buf = [0u8; MANIFEST_HEADER_SIZE];
88 r.read_exact(&mut buf)?;
89
90 let mut package_id = [0u8; 32];
91 package_id.copy_from_slice(&buf[18..50]);
92
93 Ok(ManifestHeader {
94 version: u16::from_le_bytes(buf[0..2].try_into().unwrap()),
95 entry_count: u32::from_le_bytes(buf[2..6].try_into().unwrap()),
96 string_table_size: u32::from_le_bytes(buf[6..10].try_into().unwrap()),
97 entrypoint_count: u16::from_le_bytes(buf[10..12].try_into().unwrap()),
98 default_entrypoint: u16::from_le_bytes(buf[12..14].try_into().unwrap()),
99 lib_dir_count: u16::from_le_bytes(buf[14..16].try_into().unwrap()),
100 name_offset: u16::from_le_bytes(buf[16..18].try_into().unwrap()),
101 package_id,
102 })
103 }
104}
105
106#[derive(Debug, Clone)]
107pub struct Manifest {
108 pub header: ManifestHeader,
110 pub entrypoints: Vec<EntryPoint>,
112 pub entries: Vec<Entry>,
114 pub lib_dir_offsets: Vec<u32>,
116 pub string_table: Vec<u8>,
118}
119
120impl Manifest {
121 pub fn serialize(&self) -> io::Result<Vec<u8>> {
122 let mut buf = Vec::new();
123 self.header.write_to(&mut buf)?;
124 for ep in &self.entrypoints {
125 ep.write_to(&mut buf)?;
126 }
127 for entry in &self.entries {
128 entry.write_to(&mut buf)?;
129 }
130 for &offset in &self.lib_dir_offsets {
131 buf.write_all(&offset.to_le_bytes())?;
132 }
133 buf.write_all(&self.string_table)?;
134 Ok(buf)
135 }
136
137 pub fn deserialize(data: &[u8]) -> io::Result<Self> {
138 let total = data.len();
139 let mut cursor = Cursor::new(data);
140 let header = ManifestHeader::read_from(&mut cursor)?;
141
142 if header.version != 1 {
143 return Err(io::Error::new(
144 io::ErrorKind::InvalidData,
145 format!("unsupported manifest version: {}", header.version),
146 ));
147 }
148
149 let remaining = |c: &Cursor<&[u8]>| total.saturating_sub(c.position() as usize);
153
154 let ep_cap = (header.entrypoint_count as usize).min(remaining(&cursor) / ENTRYPOINT_SIZE);
155 let mut entrypoints = Vec::with_capacity(ep_cap);
156 for _ in 0..header.entrypoint_count {
157 entrypoints.push(EntryPoint::read_from(&mut cursor)?);
158 }
159
160 let entry_cap = (header.entry_count as usize).min(remaining(&cursor) / ENTRY_HEADER_SIZE);
161 let mut entries = Vec::with_capacity(entry_cap);
162 for _ in 0..header.entry_count {
163 entries.push(Entry::read_from(&mut cursor)?);
164 }
165
166 let ld_cap = (header.lib_dir_count as usize).min(remaining(&cursor) / 4);
167 let mut lib_dir_offsets = Vec::with_capacity(ld_cap);
168 for _ in 0..header.lib_dir_count {
169 let mut offset_buf = [0u8; 4];
170 cursor.read_exact(&mut offset_buf)?;
171 lib_dir_offsets.push(u32::from_le_bytes(offset_buf));
172 }
173
174 let st_size = header.string_table_size as usize;
175 if st_size > remaining(&cursor) {
176 return Err(io::Error::new(
177 io::ErrorKind::InvalidData,
178 "string table size exceeds remaining input",
179 ));
180 }
181 let mut string_table = vec![0u8; st_size];
182 cursor.read_exact(&mut string_table)?;
183
184 let manifest = Manifest {
185 header,
186 entrypoints,
187 entries,
188 lib_dir_offsets,
189 string_table,
190 };
191 manifest.validate()?;
192 Ok(manifest)
193 }
194
195 fn validate(&self) -> io::Result<()> {
199 let n = self.entries.len();
200 let st = self.string_table.len() as u32;
201 let bad = |msg: &'static str| io::Error::new(io::ErrorKind::InvalidData, msg);
202
203 for e in &self.entries {
204 if e.parent != u32::MAX && e.parent as usize >= n {
205 return Err(bad("entry parent index out of range"));
206 }
207 if e.name > st {
208 return Err(bad("entry name offset out of range"));
209 }
210 if e.symlink_target > st {
211 return Err(bad("entry symlink_target offset out of range"));
212 }
213 }
214 for ep in &self.entrypoints {
215 if ep.target_entry as usize >= n {
216 return Err(bad("entrypoint target_entry out of range"));
217 }
218 if ep.name > st || ep.args > st {
219 return Err(bad("entrypoint string offset out of range"));
220 }
221 }
222 if !self.entrypoints.is_empty()
223 && self.header.default_entrypoint as usize >= self.entrypoints.len()
224 {
225 return Err(bad("default_entrypoint out of range"));
226 }
227 for &off in &self.lib_dir_offsets {
228 if off > st {
229 return Err(bad("lib_dir offset out of range"));
230 }
231 }
232 if self.header.name_offset as u32 > st {
233 return Err(bad("name_offset out of range"));
234 }
235 Ok(())
236 }
237
238 pub fn name(&self) -> &str {
240 if self.header.name_offset > 0 {
241 self.get_string(self.header.name_offset as u32)
242 } else {
243 ""
244 }
245 }
246
247 pub fn lib_dirs(&self) -> Vec<&str> {
249 self.lib_dir_offsets
250 .iter()
251 .map(|&offset| self.get_string(offset))
252 .collect()
253 }
254
255 pub fn get_string(&self, offset: u32) -> &str {
256 let start = offset as usize;
257 let Some(tail) = self.string_table.get(start..) else {
258 return "";
259 };
260 let end = tail.iter().position(|&b| b == 0).unwrap_or(tail.len());
261 std::str::from_utf8(&tail[..end]).unwrap_or("")
262 }
263
264 pub fn has_toplevel_dir(&self, name: &str) -> bool {
266 use crate::entry::EntryKind;
267 self.entries.iter().any(|e| {
268 e.kind == EntryKind::Dir && e.parent == u32::MAX && self.get_string(e.name) == name
269 })
270 }
271
272 pub fn find_lib_dir(&self) -> String {
275 use crate::entry::EntryKind;
276 for (i, e) in self.entries.iter().enumerate() {
277 if e.kind == EntryKind::Dir && self.get_string(e.name) == "lib" {
278 return self.entry_path(i);
279 }
280 }
281 String::new()
282 }
283
284 pub fn entry_path(&self, index: usize) -> String {
290 let mut parts = Vec::new();
291 let mut idx = index;
292 for _ in 0..=self.entries.len() {
293 let Some(entry) = self.entries.get(idx) else {
294 break;
295 };
296 let name = self.get_string(entry.name);
297 if name.is_empty() {
298 break;
299 }
300 parts.push(name);
301 if entry.parent == u32::MAX {
302 break;
303 }
304 idx = entry.parent as usize;
305 }
306 parts.reverse();
307 parts.join("/")
308 }
309
310 pub fn validated_entry_path(&self, index: usize) -> io::Result<PathBuf> {
316 let mut parts: Vec<&str> = Vec::new();
317 let mut idx = index;
318 for _ in 0..=self.entries.len() {
319 let entry = self.entries.get(idx).ok_or_else(|| {
320 io::Error::new(
321 io::ErrorKind::InvalidData,
322 "entry parent index out of range",
323 )
324 })?;
325 let name = self.get_string(entry.name);
326 if name.is_empty() {
327 break;
328 }
329 if !is_safe_component(name) {
330 return Err(io::Error::new(
331 io::ErrorKind::InvalidData,
332 format!("unsafe path component: {name:?}"),
333 ));
334 }
335 parts.push(name);
336 if entry.parent == u32::MAX {
337 break;
338 }
339 idx = entry.parent as usize;
340 }
341 parts.reverse();
342 let mut path = PathBuf::new();
343 for part in parts {
344 path.push(part);
345 }
346 Ok(path)
347 }
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353 use crate::entry::{Block, Entry, EntryKind};
354
355 fn entry(name: u32, parent: u32) -> Entry {
356 Entry {
357 kind: EntryKind::Dir,
358 parent,
359 name,
360 mode: 0o755,
361 mtime_secs: 0,
362 mtime_nsec: 0,
363 content_hash: [0u8; 32],
364 blocks: Vec::new(),
365 symlink_target: 0,
366 }
367 }
368
369 fn manifest(entries: Vec<Entry>, string_table: Vec<u8>) -> Manifest {
370 Manifest {
371 header: ManifestHeader {
372 version: 1,
373 entry_count: entries.len() as u32,
374 string_table_size: string_table.len() as u32,
375 entrypoint_count: 0,
376 default_entrypoint: 0,
377 lib_dir_count: 0,
378 name_offset: 0,
379 package_id: [0u8; 32],
380 },
381 entrypoints: Vec::new(),
382 entries,
383 lib_dir_offsets: Vec::new(),
384 string_table,
385 }
386 }
387
388 #[test]
389 fn roundtrip_valid_manifest() {
390 let st = b"\0a\0b\0".to_vec();
392 let m = manifest(vec![entry(1, u32::MAX), entry(3, 0)], st);
393 let bytes = m.serialize().unwrap();
394 let back = Manifest::deserialize(&bytes).unwrap();
395 assert_eq!(back.entry_path(1), "a/b");
396 assert_eq!(back.get_string(3), "b");
397 }
398
399 #[test]
400 fn file_entry_with_blocks_roundtrips_byte_identical() {
401 let mut file = entry(1, u32::MAX);
405 file.kind = EntryKind::File;
406 file.blocks = vec![
407 Block {
408 payload_offset: 0,
409 compressed_size: 10,
410 original_size: 20,
411 },
412 Block {
413 payload_offset: 10,
414 compressed_size: 5,
415 original_size: 8,
416 },
417 ];
418 let m = manifest(vec![file], b"\0a\0".to_vec());
419
420 let bytes = m.serialize().unwrap();
421 let back = Manifest::deserialize(&bytes).unwrap();
422 assert_eq!(back.entries[0].blocks.len(), 2);
423 assert_eq!(back.entries[0].blocks[1].original_size, 8);
424 assert_eq!(back.serialize().unwrap(), bytes);
426 }
427
428 #[test]
429 fn get_string_out_of_range_returns_empty() {
430 let m = manifest(vec![entry(0, u32::MAX)], b"\0".to_vec());
431 assert_eq!(m.get_string(9999), "");
432 }
433
434 #[test]
435 fn oversized_entry_count_errors_without_panic() {
436 let m = manifest(vec![entry(1, u32::MAX)], b"\0a\0".to_vec());
437 let mut bytes = m.serialize().unwrap();
438 bytes[2..6].copy_from_slice(&u32::MAX.to_le_bytes());
440 assert!(Manifest::deserialize(&bytes).is_err());
441 }
442
443 #[test]
444 fn oversized_string_table_errors() {
445 let m = manifest(vec![entry(1, u32::MAX)], b"\0a\0".to_vec());
446 let mut bytes = m.serialize().unwrap();
447 bytes[6..10].copy_from_slice(&u32::MAX.to_le_bytes());
449 assert!(Manifest::deserialize(&bytes).is_err());
450 }
451
452 #[test]
453 fn out_of_range_parent_rejected_at_deserialize() {
454 let m = manifest(vec![entry(1, 5)], b"\0a\0".to_vec());
455 let bytes = m.serialize().unwrap();
456 assert!(Manifest::deserialize(&bytes).is_err());
457 }
458
459 #[test]
460 fn parent_cycle_terminates() {
461 let st = b"\0a\0b\0".to_vec();
463 let m = manifest(vec![entry(1, 1), entry(3, 0)], st);
464 let path = m.entry_path(0);
465 assert!(!path.is_empty());
466 }
467
468 #[test]
469 fn unsafe_component_rejected() {
470 let m = manifest(vec![entry(1, u32::MAX)], b"\0..\0".to_vec());
472 assert!(m.validated_entry_path(0).is_err());
473 }
474
475 #[test]
476 fn safe_component_accepted() {
477 let m = manifest(vec![entry(1, u32::MAX)], b"\0a\0".to_vec());
478 assert_eq!(m.validated_entry_path(0).unwrap().to_str(), Some("a"));
479 }
480
481 #[test]
482 fn is_safe_component_rules() {
483 assert!(is_safe_component("lib"));
484 assert!(!is_safe_component(""));
485 assert!(!is_safe_component("."));
486 assert!(!is_safe_component(".."));
487 assert!(!is_safe_component("a/b"));
488 assert!(!is_safe_component("a\0b"));
489 }
490
491 #[test]
492 fn symlink_within_root_rules() {
493 use std::path::Path;
494 assert!(symlink_target_within_root(
496 Path::new("bin/app"),
497 "../lib/x.so"
498 ));
499 assert!(symlink_target_within_root(Path::new("a/b/c"), "d"));
500 assert!(!symlink_target_within_root(
502 Path::new("bin/app"),
503 "/etc/passwd"
504 ));
505 assert!(!symlink_target_within_root(
507 Path::new("bin/app"),
508 "../../etc/passwd"
509 ));
510 assert!(!symlink_target_within_root(Path::new("top"), "../escape"));
511 assert!(!symlink_target_within_root(Path::new("x"), ""));
513 }
514}
515
516#[derive(Debug, Default)]
518pub struct StringTableBuilder {
519 data: Vec<u8>,
520 index: HashMap<String, u32>,
521}
522
523impl StringTableBuilder {
524 pub fn new() -> Self {
525 Self {
526 data: Vec::new(),
527 index: HashMap::new(),
528 }
529 }
530
531 pub fn add(&mut self, s: &str) -> u32 {
533 if let Some(&offset) = self.index.get(s) {
534 return offset;
535 }
536 let offset = self.data.len() as u32;
537 self.data.extend_from_slice(s.as_bytes());
538 self.data.push(0);
539 self.index.insert(s.to_owned(), offset);
540 offset
541 }
542
543 pub fn finish(self) -> Vec<u8> {
544 self.data
545 }
546
547 pub fn len(&self) -> u32 {
548 self.data.len() as u32
549 }
550}