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_VERSION: u16 = 2;
58
59pub const MANIFEST_HEADER_SIZE: usize = 2 + 4 + 4 + 2 + 2 + 2 + 2 + 32; #[derive(Debug, Clone)]
62pub struct ManifestHeader {
63 pub version: u16,
65 pub entry_count: u32,
67 pub string_table_size: u32,
69 pub entrypoint_count: u16,
71 pub default_entrypoint: u16,
73 pub lib_dir_count: u16,
75 pub name_offset: u16,
77 pub package_id: [u8; 32],
79}
80
81impl ManifestHeader {
82 pub fn write_to<W: Write>(&self, w: &mut W) -> io::Result<()> {
83 w.write_all(&self.version.to_le_bytes())?;
84 w.write_all(&self.entry_count.to_le_bytes())?;
85 w.write_all(&self.string_table_size.to_le_bytes())?;
86 w.write_all(&self.entrypoint_count.to_le_bytes())?;
87 w.write_all(&self.default_entrypoint.to_le_bytes())?;
88 w.write_all(&self.lib_dir_count.to_le_bytes())?;
89 w.write_all(&self.name_offset.to_le_bytes())?;
90 w.write_all(&self.package_id)?;
91 Ok(())
92 }
93
94 pub fn read_from<R: Read>(r: &mut R) -> io::Result<Self> {
95 let mut buf = [0u8; MANIFEST_HEADER_SIZE];
96 r.read_exact(&mut buf)?;
97
98 let mut package_id = [0u8; 32];
99 package_id.copy_from_slice(&buf[18..50]);
100
101 Ok(ManifestHeader {
102 version: u16::from_le_bytes(buf[0..2].try_into().unwrap()),
103 entry_count: u32::from_le_bytes(buf[2..6].try_into().unwrap()),
104 string_table_size: u32::from_le_bytes(buf[6..10].try_into().unwrap()),
105 entrypoint_count: u16::from_le_bytes(buf[10..12].try_into().unwrap()),
106 default_entrypoint: u16::from_le_bytes(buf[12..14].try_into().unwrap()),
107 lib_dir_count: u16::from_le_bytes(buf[14..16].try_into().unwrap()),
108 name_offset: u16::from_le_bytes(buf[16..18].try_into().unwrap()),
109 package_id,
110 })
111 }
112}
113
114#[derive(Debug, Clone)]
115pub struct Manifest {
116 pub header: ManifestHeader,
118 pub entrypoints: Vec<EntryPoint>,
120 pub entries: Vec<Entry>,
122 pub lib_dir_offsets: Vec<u32>,
124 pub string_table: Vec<u8>,
126}
127
128impl Manifest {
129 pub fn serialize(&self) -> io::Result<Vec<u8>> {
137 let mut buf = Vec::new();
138 let header = ManifestHeader {
139 version: MANIFEST_VERSION,
140 ..self.header.clone()
141 };
142 header.write_to(&mut buf)?;
143 for ep in &self.entrypoints {
144 ep.write_to(&mut buf)?;
145 }
146 for entry in &self.entries {
147 entry.write_to(&mut buf)?;
148 }
149 for &offset in &self.lib_dir_offsets {
150 buf.write_all(&offset.to_le_bytes())?;
151 }
152 buf.write_all(&self.string_table)?;
153 Ok(buf)
154 }
155
156 pub fn deserialize(data: &[u8]) -> io::Result<Self> {
157 let total = data.len();
158 let mut cursor = Cursor::new(data);
159 let header = ManifestHeader::read_from(&mut cursor)?;
160
161 if header.version == 0 || header.version > MANIFEST_VERSION {
162 return Err(io::Error::new(
163 io::ErrorKind::InvalidData,
164 format!("unsupported manifest version: {}", header.version),
165 ));
166 }
167 let version = header.version;
168
169 let remaining = |c: &Cursor<&[u8]>| total.saturating_sub(c.position() as usize);
173
174 let ep_cap = (header.entrypoint_count as usize).min(remaining(&cursor) / ENTRYPOINT_SIZE);
175 let mut entrypoints = Vec::with_capacity(ep_cap);
176 for _ in 0..header.entrypoint_count {
177 entrypoints.push(EntryPoint::read_from(&mut cursor)?);
178 }
179
180 let entry_cap = (header.entry_count as usize).min(remaining(&cursor) / ENTRY_HEADER_SIZE);
181 let mut entries = Vec::with_capacity(entry_cap);
182 for _ in 0..header.entry_count {
183 entries.push(Entry::read_from(&mut cursor, version)?);
184 }
185
186 let ld_cap = (header.lib_dir_count as usize).min(remaining(&cursor) / 4);
187 let mut lib_dir_offsets = Vec::with_capacity(ld_cap);
188 for _ in 0..header.lib_dir_count {
189 let mut offset_buf = [0u8; 4];
190 cursor.read_exact(&mut offset_buf)?;
191 lib_dir_offsets.push(u32::from_le_bytes(offset_buf));
192 }
193
194 let st_size = header.string_table_size as usize;
195 if st_size > remaining(&cursor) {
196 return Err(io::Error::new(
197 io::ErrorKind::InvalidData,
198 "string table size exceeds remaining input",
199 ));
200 }
201 let mut string_table = vec![0u8; st_size];
202 cursor.read_exact(&mut string_table)?;
203
204 let manifest = Manifest {
205 header,
206 entrypoints,
207 entries,
208 lib_dir_offsets,
209 string_table,
210 };
211 manifest.validate()?;
212 Ok(manifest)
213 }
214
215 fn validate(&self) -> io::Result<()> {
219 let n = self.entries.len();
220 let st = self.string_table.len() as u32;
221 let bad = |msg: &'static str| io::Error::new(io::ErrorKind::InvalidData, msg);
222
223 for e in &self.entries {
224 if e.parent != u32::MAX && e.parent as usize >= n {
225 return Err(bad("entry parent index out of range"));
226 }
227 if e.name > st {
228 return Err(bad("entry name offset out of range"));
229 }
230 if e.symlink_target > st {
231 return Err(bad("entry symlink_target offset out of range"));
232 }
233 }
234 for ep in &self.entrypoints {
235 if ep.target_entry as usize >= n {
236 return Err(bad("entrypoint target_entry out of range"));
237 }
238 if ep.name > st || ep.args > st {
239 return Err(bad("entrypoint string offset out of range"));
240 }
241 }
242 if !self.entrypoints.is_empty()
243 && self.header.default_entrypoint as usize >= self.entrypoints.len()
244 {
245 return Err(bad("default_entrypoint out of range"));
246 }
247 for &off in &self.lib_dir_offsets {
248 if off > st {
249 return Err(bad("lib_dir offset out of range"));
250 }
251 }
252 if self.header.name_offset as u32 > st {
253 return Err(bad("name_offset out of range"));
254 }
255 Ok(())
256 }
257
258 pub fn name(&self) -> &str {
260 if self.header.name_offset > 0 {
261 self.get_string(self.header.name_offset as u32)
262 } else {
263 ""
264 }
265 }
266
267 pub fn lib_dirs(&self) -> Vec<&str> {
269 self.lib_dir_offsets
270 .iter()
271 .map(|&offset| self.get_string(offset))
272 .collect()
273 }
274
275 pub fn get_string(&self, offset: u32) -> &str {
276 let start = offset as usize;
277 let Some(tail) = self.string_table.get(start..) else {
278 return "";
279 };
280 let end = tail.iter().position(|&b| b == 0).unwrap_or(tail.len());
281 std::str::from_utf8(&tail[..end]).unwrap_or("")
282 }
283
284 pub fn has_toplevel_dir(&self, name: &str) -> bool {
286 use crate::entry::EntryKind;
287 self.entries.iter().any(|e| {
288 e.kind == EntryKind::Dir && e.parent == u32::MAX && self.get_string(e.name) == name
289 })
290 }
291
292 pub fn find_lib_dir(&self) -> String {
295 use crate::entry::EntryKind;
296 for (i, e) in self.entries.iter().enumerate() {
297 if e.kind == EntryKind::Dir && self.get_string(e.name) == "lib" {
298 return self.entry_path(i);
299 }
300 }
301 String::new()
302 }
303
304 pub fn entry_path(&self, index: usize) -> String {
310 let mut parts = Vec::new();
311 let mut idx = index;
312 for _ in 0..=self.entries.len() {
313 let Some(entry) = self.entries.get(idx) else {
314 break;
315 };
316 let name = self.get_string(entry.name);
317 if name.is_empty() {
318 break;
319 }
320 parts.push(name);
321 if entry.parent == u32::MAX {
322 break;
323 }
324 idx = entry.parent as usize;
325 }
326 parts.reverse();
327 parts.join("/")
328 }
329
330 pub fn validated_entry_path(&self, index: usize) -> io::Result<PathBuf> {
336 let mut parts: Vec<&str> = Vec::new();
337 let mut idx = index;
338 for _ in 0..=self.entries.len() {
339 let entry = self.entries.get(idx).ok_or_else(|| {
340 io::Error::new(
341 io::ErrorKind::InvalidData,
342 "entry parent index out of range",
343 )
344 })?;
345 let name = self.get_string(entry.name);
346 if name.is_empty() {
347 break;
348 }
349 if !is_safe_component(name) {
350 return Err(io::Error::new(
351 io::ErrorKind::InvalidData,
352 format!("unsafe path component: {name:?}"),
353 ));
354 }
355 parts.push(name);
356 if entry.parent == u32::MAX {
357 break;
358 }
359 idx = entry.parent as usize;
360 }
361 parts.reverse();
362 let mut path = PathBuf::new();
363 for part in parts {
364 path.push(part);
365 }
366 Ok(path)
367 }
368}
369
370#[cfg(test)]
371mod tests {
372 use super::*;
373 use crate::entry::{Block, Entry, EntryKind};
374
375 fn entry(name: u32, parent: u32) -> Entry {
376 Entry {
377 kind: EntryKind::Dir,
378 parent,
379 name,
380 mode: 0o755,
381 mtime_secs: 0,
382 mtime_nsec: 0,
383 content_hash: [0u8; 32],
384 blocks: Vec::new(),
385 symlink_target: 0,
386 }
387 }
388
389 fn manifest(entries: Vec<Entry>, string_table: Vec<u8>) -> Manifest {
390 Manifest {
391 header: ManifestHeader {
392 version: 1,
393 entry_count: entries.len() as u32,
394 string_table_size: string_table.len() as u32,
395 entrypoint_count: 0,
396 default_entrypoint: 0,
397 lib_dir_count: 0,
398 name_offset: 0,
399 package_id: [0u8; 32],
400 },
401 entrypoints: Vec::new(),
402 entries,
403 lib_dir_offsets: Vec::new(),
404 string_table,
405 }
406 }
407
408 #[test]
409 fn roundtrip_valid_manifest() {
410 let st = b"\0a\0b\0".to_vec();
412 let m = manifest(vec![entry(1, u32::MAX), entry(3, 0)], st);
413 let bytes = m.serialize().unwrap();
414 let back = Manifest::deserialize(&bytes).unwrap();
415 assert_eq!(back.entry_path(1), "a/b");
416 assert_eq!(back.get_string(3), "b");
417 }
418
419 #[test]
420 fn file_entry_with_blocks_roundtrips_byte_identical() {
421 let mut file = entry(1, u32::MAX);
425 file.kind = EntryKind::File;
426 file.blocks = vec![
427 Block {
428 payload_offset: 0,
429 compressed_size: 10,
430 original_size: 20,
431 content_hash: [0u8; 32],
432 },
433 Block {
434 payload_offset: 10,
435 compressed_size: 5,
436 original_size: 8,
437 content_hash: [0u8; 32],
438 },
439 ];
440 let m = manifest(vec![file], b"\0a\0".to_vec());
441
442 let bytes = m.serialize().unwrap();
443 let back = Manifest::deserialize(&bytes).unwrap();
444 assert_eq!(back.entries[0].blocks.len(), 2);
445 assert_eq!(back.entries[0].blocks[1].original_size, 8);
446 assert_eq!(back.serialize().unwrap(), bytes);
448 }
449
450 #[test]
454 fn version_one_blocks_still_decode() {
455 let mut file = entry(1, u32::MAX);
456 file.kind = EntryKind::File;
457 file.blocks = vec![Block {
458 payload_offset: 7,
459 compressed_size: 11,
460 original_size: 13,
461 content_hash: [0u8; 32],
462 }];
463 let m = manifest(vec![file], b"\0a\0".to_vec());
464
465 let mut bytes = Vec::new();
467 let mut header = m.header.clone();
468 header.version = 1;
469 header.write_to(&mut bytes).unwrap();
470 let e = &m.entries[0];
471 bytes.push(e.kind as u8);
472 bytes.extend_from_slice(&e.parent.to_le_bytes());
473 bytes.extend_from_slice(&e.name.to_le_bytes());
474 bytes.extend_from_slice(&e.mode.to_le_bytes());
475 bytes.extend_from_slice(&e.mtime_secs.to_le_bytes());
476 bytes.extend_from_slice(&e.mtime_nsec.to_le_bytes());
477 bytes.extend_from_slice(&e.content_hash);
478 bytes.extend_from_slice(&1u32.to_le_bytes());
479 bytes.extend_from_slice(&e.symlink_target.to_le_bytes());
480 let b = &e.blocks[0];
481 bytes.extend_from_slice(&b.payload_offset.to_le_bytes());
482 bytes.extend_from_slice(&b.compressed_size.to_le_bytes());
483 bytes.extend_from_slice(&b.original_size.to_le_bytes());
484 bytes.extend_from_slice(&m.string_table);
485
486 let back = Manifest::deserialize(&bytes).expect("version 1 must decode");
487 assert_eq!(back.header.version, 1);
488 let block = &back.entries[0].blocks[0];
489 assert_eq!(block.payload_offset, 7);
490 assert_eq!(block.compressed_size, 11);
491 assert_eq!(block.original_size, 13);
492 assert!(!block.has_content_hash(), "v1 carries no per-block hash");
493 }
494
495 #[test]
496 fn future_versions_are_refused() {
497 let m = manifest(vec![entry(1, u32::MAX)], b"\0a\0".to_vec());
498 let mut bytes = m.serialize().unwrap();
499 bytes[0..2].copy_from_slice(&(MANIFEST_VERSION + 1).to_le_bytes());
500 assert!(Manifest::deserialize(&bytes).is_err());
501 bytes[0..2].copy_from_slice(&0u16.to_le_bytes());
502 assert!(Manifest::deserialize(&bytes).is_err());
503 }
504
505 #[test]
506 fn serialize_always_writes_the_current_version() {
507 let mut m = manifest(vec![entry(1, u32::MAX)], b"\0a\0".to_vec());
508 m.header.version = 1;
509 let bytes = m.serialize().unwrap();
510 assert_eq!(
511 u16::from_le_bytes(bytes[0..2].try_into().unwrap()),
512 MANIFEST_VERSION,
513 "the body is written in the current format, so the header must say so"
514 );
515 }
516
517 #[test]
518 fn get_string_out_of_range_returns_empty() {
519 let m = manifest(vec![entry(0, u32::MAX)], b"\0".to_vec());
520 assert_eq!(m.get_string(9999), "");
521 }
522
523 #[test]
524 fn oversized_entry_count_errors_without_panic() {
525 let m = manifest(vec![entry(1, u32::MAX)], b"\0a\0".to_vec());
526 let mut bytes = m.serialize().unwrap();
527 bytes[2..6].copy_from_slice(&u32::MAX.to_le_bytes());
529 assert!(Manifest::deserialize(&bytes).is_err());
530 }
531
532 #[test]
533 fn oversized_string_table_errors() {
534 let m = manifest(vec![entry(1, u32::MAX)], b"\0a\0".to_vec());
535 let mut bytes = m.serialize().unwrap();
536 bytes[6..10].copy_from_slice(&u32::MAX.to_le_bytes());
538 assert!(Manifest::deserialize(&bytes).is_err());
539 }
540
541 #[test]
542 fn out_of_range_parent_rejected_at_deserialize() {
543 let m = manifest(vec![entry(1, 5)], b"\0a\0".to_vec());
544 let bytes = m.serialize().unwrap();
545 assert!(Manifest::deserialize(&bytes).is_err());
546 }
547
548 #[test]
549 fn parent_cycle_terminates() {
550 let st = b"\0a\0b\0".to_vec();
552 let m = manifest(vec![entry(1, 1), entry(3, 0)], st);
553 let path = m.entry_path(0);
554 assert!(!path.is_empty());
555 }
556
557 #[test]
558 fn unsafe_component_rejected() {
559 let m = manifest(vec![entry(1, u32::MAX)], b"\0..\0".to_vec());
561 assert!(m.validated_entry_path(0).is_err());
562 }
563
564 #[test]
565 fn safe_component_accepted() {
566 let m = manifest(vec![entry(1, u32::MAX)], b"\0a\0".to_vec());
567 assert_eq!(m.validated_entry_path(0).unwrap().to_str(), Some("a"));
568 }
569
570 #[test]
571 fn is_safe_component_rules() {
572 assert!(is_safe_component("lib"));
573 assert!(!is_safe_component(""));
574 assert!(!is_safe_component("."));
575 assert!(!is_safe_component(".."));
576 assert!(!is_safe_component("a/b"));
577 assert!(!is_safe_component("a\0b"));
578 }
579
580 #[test]
581 fn symlink_within_root_rules() {
582 use std::path::Path;
583 assert!(symlink_target_within_root(
585 Path::new("bin/app"),
586 "../lib/x.so"
587 ));
588 assert!(symlink_target_within_root(Path::new("a/b/c"), "d"));
589 assert!(!symlink_target_within_root(
591 Path::new("bin/app"),
592 "/etc/passwd"
593 ));
594 assert!(!symlink_target_within_root(
596 Path::new("bin/app"),
597 "../../etc/passwd"
598 ));
599 assert!(!symlink_target_within_root(Path::new("top"), "../escape"));
600 assert!(!symlink_target_within_root(Path::new("x"), ""));
602 }
603}
604
605#[derive(Debug, Default)]
607pub struct StringTableBuilder {
608 data: Vec<u8>,
609 index: HashMap<String, u32>,
610}
611
612impl StringTableBuilder {
613 pub fn new() -> Self {
614 Self {
615 data: Vec::new(),
616 index: HashMap::new(),
617 }
618 }
619
620 pub fn add(&mut self, s: &str) -> u32 {
622 if let Some(&offset) = self.index.get(s) {
623 return offset;
624 }
625 let offset = self.data.len() as u32;
626 self.data.extend_from_slice(s.as_bytes());
627 self.data.push(0);
628 self.index.insert(s.to_owned(), offset);
629 offset
630 }
631
632 pub fn finish(self) -> Vec<u8> {
633 self.data
634 }
635
636 pub fn len(&self) -> u32 {
639 self.data.len() as u32
640 }
641
642 pub fn is_empty(&self) -> bool {
646 self.data.is_empty()
647 }
648}