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, EntryKind, 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 = 3;
60
61pub const FOOTER_HASH_RANGE: std::ops::Range<usize> = 82..114;
66
67pub const MANIFEST_HEADER_SIZE: usize = 114;
70
71const V1_HEADER_SIZE: usize = 50;
73
74#[derive(Debug, Clone)]
75pub struct ManifestHeader {
76 pub version: u16,
78 pub entry_count: u32,
80 pub string_table_size: u32,
82 pub entrypoint_count: u16,
84 pub default_entrypoint: u16,
86 pub lib_dir_count: u16,
88 pub name_offset: u16,
90 pub package_id: [u8; 32],
92 pub prefix_hash: [u8; 32],
95 pub footer_hash: [u8; 32],
99}
100
101impl ManifestHeader {
102 pub fn write_to<W: Write>(&self, w: &mut W) -> io::Result<()> {
103 w.write_all(&self.version.to_le_bytes())?;
104 w.write_all(&self.entry_count.to_le_bytes())?;
105 w.write_all(&self.string_table_size.to_le_bytes())?;
106 w.write_all(&self.entrypoint_count.to_le_bytes())?;
107 w.write_all(&self.default_entrypoint.to_le_bytes())?;
108 w.write_all(&self.lib_dir_count.to_le_bytes())?;
109 w.write_all(&self.name_offset.to_le_bytes())?;
110 w.write_all(&self.package_id)?;
111 w.write_all(&self.prefix_hash)?;
112 w.write_all(&self.footer_hash)?;
113 Ok(())
114 }
115
116 pub fn read_from<R: Read>(r: &mut R) -> io::Result<Self> {
117 let mut buf = [0u8; V1_HEADER_SIZE];
118 r.read_exact(&mut buf)?;
119
120 let mut package_id = [0u8; 32];
121 package_id.copy_from_slice(&buf[18..50]);
122
123 let mut header = ManifestHeader {
124 version: u16::from_le_bytes(buf[0..2].try_into().unwrap()),
125 entry_count: u32::from_le_bytes(buf[2..6].try_into().unwrap()),
126 string_table_size: u32::from_le_bytes(buf[6..10].try_into().unwrap()),
127 entrypoint_count: u16::from_le_bytes(buf[10..12].try_into().unwrap()),
128 default_entrypoint: u16::from_le_bytes(buf[12..14].try_into().unwrap()),
129 lib_dir_count: u16::from_le_bytes(buf[14..16].try_into().unwrap()),
130 name_offset: u16::from_le_bytes(buf[16..18].try_into().unwrap()),
131 package_id,
132 prefix_hash: [0; 32],
133 footer_hash: [0; 32],
134 };
135
136 if header.version >= 3 {
139 let mut hashes = [0u8; 64];
140 r.read_exact(&mut hashes)?;
141 header.prefix_hash.copy_from_slice(&hashes[..32]);
142 header.footer_hash.copy_from_slice(&hashes[32..]);
143 }
144
145 Ok(header)
146 }
147}
148
149pub fn manifest_checksum(manifest: &[u8]) -> io::Result<[u8; 4]> {
159 let truncated = || io::Error::new(io::ErrorKind::InvalidData, "manifest header truncated");
160 let version = u16::from_le_bytes(
161 manifest
162 .get(..2)
163 .ok_or_else(truncated)?
164 .try_into()
165 .expect("u16 version"),
166 );
167 if version < 3 {
168 return Ok(xxhash_rust::xxh32::xxh32(manifest, 0).to_le_bytes());
169 }
170 if manifest.len() < MANIFEST_HEADER_SIZE {
171 return Err(truncated());
172 }
173 let mut zeroed = manifest.to_vec();
174 zeroed[FOOTER_HASH_RANGE].fill(0);
175 Ok(xxhash_rust::xxh32::xxh32(&zeroed, 0).to_le_bytes())
176}
177
178#[derive(Debug, Clone)]
179pub struct Manifest {
180 pub header: ManifestHeader,
182 pub entrypoints: Vec<EntryPoint>,
184 pub entries: Vec<Entry>,
186 pub lib_dir_offsets: Vec<u32>,
188 pub string_table: Vec<u8>,
190}
191
192impl Manifest {
193 pub fn serialize(&self) -> io::Result<Vec<u8>> {
201 let mut buf = Vec::new();
202 let header = ManifestHeader {
203 version: MANIFEST_VERSION,
204 ..self.header.clone()
205 };
206 header.write_to(&mut buf)?;
207 for ep in &self.entrypoints {
208 ep.write_to(&mut buf)?;
209 }
210 for entry in &self.entries {
211 entry.write_to(&mut buf)?;
212 }
213 for &offset in &self.lib_dir_offsets {
214 buf.write_all(&offset.to_le_bytes())?;
215 }
216 buf.write_all(&self.string_table)?;
217 Ok(buf)
218 }
219
220 pub fn deserialize(data: &[u8]) -> io::Result<Self> {
221 let total = data.len();
222 let mut cursor = Cursor::new(data);
223 let header = ManifestHeader::read_from(&mut cursor)?;
224
225 if header.version == 0 || header.version > MANIFEST_VERSION {
226 return Err(io::Error::new(
227 io::ErrorKind::InvalidData,
228 format!("unsupported manifest version: {}", header.version),
229 ));
230 }
231 let version = header.version;
232
233 let remaining = |c: &Cursor<&[u8]>| total.saturating_sub(c.position() as usize);
237
238 let ep_cap = (header.entrypoint_count as usize).min(remaining(&cursor) / ENTRYPOINT_SIZE);
239 let mut entrypoints = Vec::with_capacity(ep_cap);
240 for _ in 0..header.entrypoint_count {
241 entrypoints.push(EntryPoint::read_from(&mut cursor)?);
242 }
243
244 let entry_cap = (header.entry_count as usize).min(remaining(&cursor) / ENTRY_HEADER_SIZE);
245 let mut entries = Vec::with_capacity(entry_cap);
246 for _ in 0..header.entry_count {
247 entries.push(Entry::read_from(&mut cursor, version)?);
248 }
249
250 let ld_cap = (header.lib_dir_count as usize).min(remaining(&cursor) / 4);
251 let mut lib_dir_offsets = Vec::with_capacity(ld_cap);
252 for _ in 0..header.lib_dir_count {
253 let mut offset_buf = [0u8; 4];
254 cursor.read_exact(&mut offset_buf)?;
255 lib_dir_offsets.push(u32::from_le_bytes(offset_buf));
256 }
257
258 let st_size = header.string_table_size as usize;
259 if st_size > remaining(&cursor) {
260 return Err(io::Error::new(
261 io::ErrorKind::InvalidData,
262 "string table size exceeds remaining input",
263 ));
264 }
265 let mut string_table = vec![0u8; st_size];
266 cursor.read_exact(&mut string_table)?;
267
268 let manifest = Manifest {
269 header,
270 entrypoints,
271 entries,
272 lib_dir_offsets,
273 string_table,
274 };
275 manifest.validate()?;
276 Ok(manifest)
277 }
278
279 fn validate(&self) -> io::Result<()> {
283 let n = self.entries.len();
284 let st = self.string_table.len() as u32;
285 let bad = |msg: &'static str| io::Error::new(io::ErrorKind::InvalidData, msg);
286
287 for (i, e) in self.entries.iter().enumerate() {
288 if e.parent != u32::MAX {
293 let p = e.parent as usize;
294 if p >= n {
295 return Err(bad("entry parent index out of range"));
296 }
297 if p >= i {
298 return Err(bad("entry parent does not precede it"));
299 }
300 if self.entries[p].kind != EntryKind::Dir {
301 return Err(bad("entry parent is not a directory"));
302 }
303 }
304 if e.name > st {
305 return Err(bad("entry name offset out of range"));
306 }
307 if e.symlink_target > st {
308 return Err(bad("entry symlink_target offset out of range"));
309 }
310 }
311 for ep in &self.entrypoints {
312 if ep.target_entry as usize >= n {
313 return Err(bad("entrypoint target_entry out of range"));
314 }
315 if ep.name > st || ep.args > st {
316 return Err(bad("entrypoint string offset out of range"));
317 }
318 }
319 if !self.entrypoints.is_empty()
320 && self.header.default_entrypoint as usize >= self.entrypoints.len()
321 {
322 return Err(bad("default_entrypoint out of range"));
323 }
324 for &off in &self.lib_dir_offsets {
325 if off > st {
326 return Err(bad("lib_dir offset out of range"));
327 }
328 let dir = self.get_string(off);
332 let safe = !dir.is_empty()
333 && std::path::Path::new(dir)
334 .components()
335 .all(|c| matches!(c, std::path::Component::Normal(_)))
336 && !dir.contains('\0');
337 if !safe {
338 return Err(bad("lib_dir is not a relative path inside the package"));
339 }
340 }
341 if self.header.name_offset as u32 > st {
342 return Err(bad("name_offset out of range"));
343 }
344 Ok(())
345 }
346
347 pub fn name(&self) -> &str {
349 if self.header.name_offset > 0 {
350 self.get_string(self.header.name_offset as u32)
351 } else {
352 ""
353 }
354 }
355
356 pub fn lib_dirs(&self) -> Vec<&str> {
358 self.lib_dir_offsets
359 .iter()
360 .map(|&offset| self.get_string(offset))
361 .collect()
362 }
363
364 pub fn get_string(&self, offset: u32) -> &str {
365 let start = offset as usize;
366 let Some(tail) = self.string_table.get(start..) else {
367 return "";
368 };
369 let end = tail.iter().position(|&b| b == 0).unwrap_or(tail.len());
370 std::str::from_utf8(&tail[..end]).unwrap_or("")
371 }
372
373 pub fn has_toplevel_dir(&self, name: &str) -> bool {
375 use crate::entry::EntryKind;
376 self.entries.iter().any(|e| {
377 e.kind == EntryKind::Dir && e.parent == u32::MAX && self.get_string(e.name) == name
378 })
379 }
380
381 pub fn find_lib_dir(&self) -> String {
384 use crate::entry::EntryKind;
385 for (i, e) in self.entries.iter().enumerate() {
386 if e.kind == EntryKind::Dir && self.get_string(e.name) == "lib" {
387 return self.entry_path(i);
388 }
389 }
390 String::new()
391 }
392
393 pub fn entry_path(&self, index: usize) -> String {
399 let mut parts = Vec::new();
400 let mut idx = index;
401 for _ in 0..=self.entries.len() {
402 let Some(entry) = self.entries.get(idx) else {
403 break;
404 };
405 let name = self.get_string(entry.name);
406 if name.is_empty() {
407 break;
408 }
409 parts.push(name);
410 if entry.parent == u32::MAX {
411 break;
412 }
413 idx = entry.parent as usize;
414 }
415 parts.reverse();
416 parts.join("/")
417 }
418
419 pub fn validated_entry_path(&self, index: usize) -> io::Result<PathBuf> {
425 let mut parts: Vec<&str> = Vec::new();
426 let mut idx = index;
427 for _ in 0..=self.entries.len() {
428 let entry = self.entries.get(idx).ok_or_else(|| {
429 io::Error::new(
430 io::ErrorKind::InvalidData,
431 "entry parent index out of range",
432 )
433 })?;
434 let name = self.get_string(entry.name);
435 if name.is_empty() {
436 break;
437 }
438 if !is_safe_component(name) {
439 return Err(io::Error::new(
440 io::ErrorKind::InvalidData,
441 format!("unsafe path component: {name:?}"),
442 ));
443 }
444 parts.push(name);
445 if entry.parent == u32::MAX {
446 break;
447 }
448 if entry.parent as usize >= idx {
449 return Err(io::Error::new(
450 io::ErrorKind::InvalidData,
451 "entry parent does not precede it",
452 ));
453 }
454 idx = entry.parent as usize;
455 }
456 parts.reverse();
457 let mut path = PathBuf::new();
458 for part in parts {
459 path.push(part);
460 }
461 Ok(path)
462 }
463}
464
465#[cfg(test)]
466mod tests {
467 use super::*;
468 use crate::entry::{Block, Entry, EntryKind};
469
470 fn entry(name: u32, parent: u32) -> Entry {
471 Entry {
472 kind: EntryKind::Dir,
473 parent,
474 name,
475 mode: 0o755,
476 mtime_secs: 0,
477 mtime_nsec: 0,
478 content_hash: [0u8; 32],
479 blocks: Vec::new(),
480 symlink_target: 0,
481 }
482 }
483
484 fn manifest(entries: Vec<Entry>, string_table: Vec<u8>) -> Manifest {
485 Manifest {
486 header: ManifestHeader {
487 version: MANIFEST_VERSION,
488 entry_count: entries.len() as u32,
489 string_table_size: string_table.len() as u32,
490 entrypoint_count: 0,
491 default_entrypoint: 0,
492 lib_dir_count: 0,
493 name_offset: 0,
494 package_id: [0u8; 32],
495 prefix_hash: [0u8; 32],
496 footer_hash: [0u8; 32],
497 },
498 entrypoints: Vec::new(),
499 entries,
500 lib_dir_offsets: Vec::new(),
501 string_table,
502 }
503 }
504
505 #[test]
506 fn roundtrip_valid_manifest() {
507 let st = b"\0a\0b\0".to_vec();
509 let m = manifest(vec![entry(1, u32::MAX), entry(3, 0)], st);
510 let bytes = m.serialize().unwrap();
511 let back = Manifest::deserialize(&bytes).unwrap();
512 assert_eq!(back.entry_path(1), "a/b");
513 assert_eq!(back.get_string(3), "b");
514 }
515
516 #[test]
517 fn file_entry_with_blocks_roundtrips_byte_identical() {
518 let mut file = entry(1, u32::MAX);
522 file.kind = EntryKind::File;
523 file.blocks = vec![
524 Block {
525 payload_offset: 0,
526 compressed_size: 10,
527 original_size: 20,
528 content_hash: [0u8; 32],
529 },
530 Block {
531 payload_offset: 10,
532 compressed_size: 5,
533 original_size: 8,
534 content_hash: [0u8; 32],
535 },
536 ];
537 let m = manifest(vec![file], b"\0a\0".to_vec());
538
539 let bytes = m.serialize().unwrap();
540 let back = Manifest::deserialize(&bytes).unwrap();
541 assert_eq!(back.entries[0].blocks.len(), 2);
542 assert_eq!(back.entries[0].blocks[1].original_size, 8);
543 assert_eq!(back.serialize().unwrap(), bytes);
545 }
546
547 #[test]
551 fn version_one_blocks_still_decode() {
552 let mut file = entry(1, u32::MAX);
553 file.kind = EntryKind::File;
554 file.blocks = vec![Block {
555 payload_offset: 7,
556 compressed_size: 11,
557 original_size: 13,
558 content_hash: [0u8; 32],
559 }];
560 let m = manifest(vec![file], b"\0a\0".to_vec());
561
562 let mut bytes = Vec::new();
565 let header = &m.header;
566 bytes.extend_from_slice(&1u16.to_le_bytes());
567 bytes.extend_from_slice(&header.entry_count.to_le_bytes());
568 bytes.extend_from_slice(&header.string_table_size.to_le_bytes());
569 bytes.extend_from_slice(&header.entrypoint_count.to_le_bytes());
570 bytes.extend_from_slice(&header.default_entrypoint.to_le_bytes());
571 bytes.extend_from_slice(&header.lib_dir_count.to_le_bytes());
572 bytes.extend_from_slice(&header.name_offset.to_le_bytes());
573 bytes.extend_from_slice(&header.package_id);
574 let e = &m.entries[0];
575 bytes.push(e.kind as u8);
576 bytes.extend_from_slice(&e.parent.to_le_bytes());
577 bytes.extend_from_slice(&e.name.to_le_bytes());
578 bytes.extend_from_slice(&e.mode.to_le_bytes());
579 bytes.extend_from_slice(&e.mtime_secs.to_le_bytes());
580 bytes.extend_from_slice(&e.mtime_nsec.to_le_bytes());
581 bytes.extend_from_slice(&e.content_hash);
582 bytes.extend_from_slice(&1u32.to_le_bytes());
583 bytes.extend_from_slice(&e.symlink_target.to_le_bytes());
584 let b = &e.blocks[0];
585 bytes.extend_from_slice(&b.payload_offset.to_le_bytes());
586 bytes.extend_from_slice(&b.compressed_size.to_le_bytes());
587 bytes.extend_from_slice(&b.original_size.to_le_bytes());
588 bytes.extend_from_slice(&m.string_table);
589
590 let back = Manifest::deserialize(&bytes).expect("version 1 must decode");
591 assert_eq!(back.header.version, 1);
592 let block = &back.entries[0].blocks[0];
593 assert_eq!(block.payload_offset, 7);
594 assert_eq!(block.compressed_size, 11);
595 assert_eq!(block.original_size, 13);
596 assert!(!block.has_content_hash(), "v1 carries no per-block hash");
597 assert_eq!(back.header.prefix_hash, [0u8; 32]);
599 assert_eq!(back.header.footer_hash, [0u8; 32]);
600 }
601
602 #[test]
603 fn structure_hashes_roundtrip_through_serialization() {
604 let mut m = manifest(vec![entry(1, u32::MAX)], b"\0a\0".to_vec());
605 m.header.prefix_hash = [1u8; 32];
606 m.header.footer_hash = [2u8; 32];
607 let bytes = m.serialize().unwrap();
608 let back = Manifest::deserialize(&bytes).unwrap();
609 assert_eq!(back.header.prefix_hash, [1u8; 32]);
610 assert_eq!(back.header.footer_hash, [2u8; 32]);
611 assert_eq!(back.serialize().unwrap(), bytes);
613 }
614
615 #[test]
616 fn manifest_checksum_zeroes_the_footer_hash_field_from_v3() {
617 let mut m = manifest(vec![entry(1, u32::MAX)], b"\0a\0".to_vec());
618 m.header.prefix_hash = [3u8; 32];
619 m.header.footer_hash = [4u8; 32];
620 let bytes = m.serialize().unwrap();
621
622 let mut zeroed = bytes.clone();
623 zeroed[FOOTER_HASH_RANGE].fill(0);
624 assert_eq!(
625 manifest_checksum(&bytes).unwrap(),
626 xxhash_rust::xxh32::xxh32(&zeroed, 0).to_le_bytes()
627 );
628 assert_ne!(
631 manifest_checksum(&bytes).unwrap(),
632 xxhash_rust::xxh32::xxh32(&bytes, 0).to_le_bytes()
633 );
634
635 let v1 = [0u8, 0, 1, 2, 3];
637 assert_eq!(
638 manifest_checksum(&v1).unwrap(),
639 xxhash_rust::xxh32::xxh32(&v1, 0).to_le_bytes()
640 );
641 }
642
643 #[test]
646 fn manifest_checksum_refuses_truncated_headers() {
647 assert!(manifest_checksum(&[]).is_err());
648 assert!(manifest_checksum(&[3]).is_err());
649 let mut short_v3 = vec![0u8; MANIFEST_HEADER_SIZE - 1];
650 short_v3[0] = 3;
651 assert!(manifest_checksum(&short_v3).is_err());
652 short_v3.push(0);
653 assert!(manifest_checksum(&short_v3).is_ok());
654 }
655
656 #[test]
657 fn future_versions_are_refused() {
658 let m = manifest(vec![entry(1, u32::MAX)], b"\0a\0".to_vec());
659 let mut bytes = m.serialize().unwrap();
660 bytes[0..2].copy_from_slice(&(MANIFEST_VERSION + 1).to_le_bytes());
661 assert!(Manifest::deserialize(&bytes).is_err());
662 bytes[0..2].copy_from_slice(&0u16.to_le_bytes());
663 assert!(Manifest::deserialize(&bytes).is_err());
664 }
665
666 #[test]
667 fn serialize_always_writes_the_current_version() {
668 let mut m = manifest(vec![entry(1, u32::MAX)], b"\0a\0".to_vec());
669 m.header.version = 1;
670 let bytes = m.serialize().unwrap();
671 assert_eq!(
672 u16::from_le_bytes(bytes[0..2].try_into().unwrap()),
673 MANIFEST_VERSION,
674 "the body is written in the current format, so the header must say so"
675 );
676 }
677
678 #[test]
679 fn get_string_out_of_range_returns_empty() {
680 let m = manifest(vec![entry(0, u32::MAX)], b"\0".to_vec());
681 assert_eq!(m.get_string(9999), "");
682 }
683
684 #[test]
685 fn oversized_entry_count_errors_without_panic() {
686 let m = manifest(vec![entry(1, u32::MAX)], b"\0a\0".to_vec());
687 let mut bytes = m.serialize().unwrap();
688 bytes[2..6].copy_from_slice(&u32::MAX.to_le_bytes());
690 assert!(Manifest::deserialize(&bytes).is_err());
691 }
692
693 #[test]
694 fn oversized_string_table_errors() {
695 let m = manifest(vec![entry(1, u32::MAX)], b"\0a\0".to_vec());
696 let mut bytes = m.serialize().unwrap();
697 bytes[6..10].copy_from_slice(&u32::MAX.to_le_bytes());
699 assert!(Manifest::deserialize(&bytes).is_err());
700 }
701
702 #[test]
703 fn out_of_range_parent_rejected_at_deserialize() {
704 let m = manifest(vec![entry(1, 5)], b"\0a\0".to_vec());
705 let bytes = m.serialize().unwrap();
706 assert!(Manifest::deserialize(&bytes).is_err());
707 }
708
709 #[test]
710 fn parent_cycle_is_rejected() {
711 let st = b"\0a\0b\0".to_vec();
715 let m = manifest(vec![entry(1, 1), entry(3, 0)], st);
716 assert!(Manifest::deserialize(&m.serialize().unwrap()).is_err());
717 assert!(m.validated_entry_path(0).is_err());
718 assert!(
719 !m.entry_path(0).is_empty(),
720 "the unvalidated walk still ends"
721 );
722 }
723
724 #[test]
725 fn a_lib_dir_leaving_the_package_is_rejected() {
726 for (st, ok) in [
727 (&b"\0lib\0"[..], true),
728 (b"\0opt/app/lib\0", true),
729 (b"\0/usr/lib\0", false),
730 (b"\0../lib\0", false),
731 (b"\0lib/../../x\0", false),
732 (b"\0\0", false),
733 ] {
734 let mut m = manifest(vec![entry(0, u32::MAX)], st.to_vec());
735 m.lib_dir_offsets = vec![1];
736 m.header.lib_dir_count = 1;
737 let back = Manifest::deserialize(&m.serialize().unwrap());
738 assert_eq!(back.is_ok(), ok, "{:?}", String::from_utf8_lossy(st));
739 }
740 }
741
742 #[test]
743 fn a_file_cannot_be_a_parent() {
744 let st = b"\0a\0b\0".to_vec();
745 let mut file = entry(1, u32::MAX);
746 file.kind = EntryKind::File;
747 let m = manifest(vec![file, entry(3, 0)], st);
748 assert!(Manifest::deserialize(&m.serialize().unwrap()).is_err());
749 }
750
751 #[test]
752 fn unsafe_component_rejected() {
753 let m = manifest(vec![entry(1, u32::MAX)], b"\0..\0".to_vec());
755 assert!(m.validated_entry_path(0).is_err());
756 }
757
758 #[test]
759 fn safe_component_accepted() {
760 let m = manifest(vec![entry(1, u32::MAX)], b"\0a\0".to_vec());
761 assert_eq!(m.validated_entry_path(0).unwrap().to_str(), Some("a"));
762 }
763
764 #[test]
765 fn is_safe_component_rules() {
766 assert!(is_safe_component("lib"));
767 assert!(!is_safe_component(""));
768 assert!(!is_safe_component("."));
769 assert!(!is_safe_component(".."));
770 assert!(!is_safe_component("a/b"));
771 assert!(!is_safe_component("a\0b"));
772 }
773
774 #[test]
775 fn symlink_within_root_rules() {
776 use std::path::Path;
777 assert!(symlink_target_within_root(
779 Path::new("bin/app"),
780 "../lib/x.so"
781 ));
782 assert!(symlink_target_within_root(Path::new("a/b/c"), "d"));
783 assert!(!symlink_target_within_root(
785 Path::new("bin/app"),
786 "/etc/passwd"
787 ));
788 assert!(!symlink_target_within_root(
790 Path::new("bin/app"),
791 "../../etc/passwd"
792 ));
793 assert!(!symlink_target_within_root(Path::new("top"), "../escape"));
794 assert!(!symlink_target_within_root(Path::new("x"), ""));
796 }
797}
798
799#[derive(Debug, Default)]
801pub struct StringTableBuilder {
802 data: Vec<u8>,
803 index: HashMap<String, u32>,
804}
805
806impl StringTableBuilder {
807 pub fn new() -> Self {
808 Self {
809 data: Vec::new(),
810 index: HashMap::new(),
811 }
812 }
813
814 pub fn add(&mut self, s: &str) -> u32 {
816 if let Some(&offset) = self.index.get(s) {
817 return offset;
818 }
819 let offset = self.data.len() as u32;
820 self.data.extend_from_slice(s.as_bytes());
821 self.data.push(0);
822 self.index.insert(s.to_owned(), offset);
823 offset
824 }
825
826 pub fn finish(self) -> Vec<u8> {
827 self.data
828 }
829
830 pub fn len(&self) -> u32 {
833 self.data.len() as u32
834 }
835
836 pub fn is_empty(&self) -> bool {
840 self.data.is_empty()
841 }
842}