1use std::collections::{BTreeMap, HashMap};
5use std::fs::{
6 create_dir, hard_link, remove_file, set_permissions, symlink_metadata, File, OpenOptions,
7};
8use std::io::prelude::*;
9use std::io::Error;
10use std::io::ErrorKind;
11use std::io::Result;
12use std::io::SeekFrom;
13use std::os::unix::fs::{chown, fchown, lchown, symlink};
14use std::process::ChildStdout;
15use std::process::Command;
16use std::process::Stdio;
17use std::time::SystemTime;
18
19use crate::header::*;
20use crate::libc::{mknod, set_modified, strftime_local};
21use crate::seek_forward::SeekForward;
22
23mod header;
24mod libc;
25mod seek_forward;
26
27pub const LOG_LEVEL_WARNING: u32 = 5;
28pub const LOG_LEVEL_INFO: u32 = 7;
29pub const LOG_LEVEL_DEBUG: u32 = 8;
30
31struct CpioFilenameReader<'a, R: Read + SeekForward> {
32 file: &'a mut R,
33}
34
35impl<R: Read + SeekForward> Iterator for CpioFilenameReader<'_, R> {
36 type Item = Result<String>;
37
38 fn next(&mut self) -> Option<Self::Item> {
39 match read_filename_from_next_cpio_object(self.file) {
40 Ok(filename) => {
41 if filename == "TRAILER!!!" {
42 None
43 } else {
44 Some(Ok(filename))
45 }
46 }
47 x => Some(x),
48 }
49 }
50}
51
52struct UserGroupCache {
53 user_cache: HashMap<u32, Option<String>>,
54 group_cache: HashMap<u32, Option<String>>,
55}
56
57impl UserGroupCache {
58 fn new() -> Self {
59 Self {
60 user_cache: HashMap::new(),
61 group_cache: HashMap::new(),
62 }
63 }
64
65 fn get_user(&mut self, uid: u32) -> Result<Option<String>> {
67 match self.user_cache.get(&uid) {
68 Some(name) => Ok(name.clone()),
69 None => {
70 let name = libc::getpwuid_name(uid)?;
71 self.user_cache.insert(uid, name.clone());
72 Ok(name)
73 }
74 }
75 }
76
77 fn get_group(&mut self, gid: u32) -> Result<Option<String>> {
79 match self.group_cache.get(&gid) {
80 Some(name) => Ok(name.clone()),
81 None => {
82 let name = libc::getgrgid_name(gid)?;
83 self.group_cache.insert(gid, name.clone());
84 Ok(name)
85 }
86 }
87 }
88}
89
90fn format_time(timestamp: u32, now: i64) -> Result<String> {
92 let recent = now - i64::from(timestamp) <= 15778476;
97 if recent {
98 strftime_local(b"%b %e %H:%M\0", timestamp)
99 } else {
100 strftime_local(b"%b %e %Y\0", timestamp)
101 }
102}
103
104type SeenFiles = HashMap<u128, String>;
106
107struct Extractor {
108 seen_files: SeenFiles,
109 mtimes: BTreeMap<String, i64>,
110}
111
112impl Extractor {
113 fn new() -> Extractor {
114 Extractor {
115 seen_files: SeenFiles::new(),
116 mtimes: BTreeMap::new(),
117 }
118 }
119
120 fn set_modified_times(&self, log_level: u32) -> Result<()> {
121 for (path, mtime) in self.mtimes.iter().rev() {
122 if log_level >= LOG_LEVEL_DEBUG {
123 writeln!(std::io::stderr(), "set mtime {} for '{}'", mtime, path)?;
124 };
125 set_modified(path, *mtime)?;
126 }
127 Ok(())
128 }
129}
130
131fn align_to_4_bytes(length: u32) -> u32 {
132 let unaligned = length % 4;
133 if unaligned == 0 {
134 0
135 } else {
136 4 - unaligned
137 }
138}
139
140fn read_filename_from_next_cpio_object<R: Read + SeekForward>(file: &mut R) -> Result<String> {
145 let (filesize, filename) = Header::read_only_filesize_and_filename(file)?;
146 let skip = filesize + align_to_4_bytes(filesize);
147 file.seek_forward(skip.into())?;
148 Ok(filename)
149}
150
151fn read_magic_header<R: Read + Seek>(file: &mut R) -> Option<Result<Command>> {
152 let mut buffer = [0; 4];
153 while buffer == [0, 0, 0, 0] {
154 match file.read_exact(&mut buffer) {
155 Ok(()) => {}
156 Err(e) => match e.kind() {
157 ErrorKind::UnexpectedEof => return None,
158 _ => return Some(Err(e)),
159 },
160 };
161 }
162 let command = match buffer {
163 [0x42, 0x5A, 0x68, _] => {
164 let mut cmd = Command::new("bzip2");
165 cmd.arg("-cd");
166 cmd
167 }
168 [0x30, 0x37, 0x30, 0x37] => Command::new("cpio"),
169 [0x1F, 0x8B, _, _] => {
170 let mut cmd = Command::new("gzip");
171 cmd.arg("-cd");
172 cmd
173 }
174 [0x02, 0x21, 0x4C, 0x18] | [0x03, 0x21, 0x4C, 0x18] | [0x04, 0x22, 0x4D, 0x18] => {
179 let mut cmd = Command::new("lz4");
180 cmd.arg("-cd");
181 cmd
182 }
183 [0x5D, _, _, _] => {
184 let mut cmd = Command::new("lzma");
185 cmd.arg("-cd");
186 cmd
187 }
188 [0x89, 0x4C, 0x5A, 0x4F] => {
190 let mut cmd = Command::new("lzop");
191 cmd.arg("-cd");
192 cmd
193 }
194 [0xFD, 0x37, 0x7A, 0x58] => {
196 let mut cmd = Command::new("xz");
197 cmd.arg("-cd");
198 cmd
199 }
200 [0x28, 0xB5, 0x2F, 0xFD] => {
201 let mut cmd = Command::new("zstd");
202 cmd.arg("-cdq");
203 cmd
204 }
205 _ => {
206 return Some(Err(Error::new(
207 ErrorKind::InvalidData,
208 format!(
209 "Failed to determine CPIO or compression magic number: 0x{:02x}{:02x}{:02x}{:02x} (big endian)",
210 buffer[0], buffer[1], buffer[2], buffer[3]
211 ),
212 )));
213 }
214 };
215 match file.seek(SeekFrom::Current(-4)) {
216 Ok(_) => {}
217 Err(e) => {
218 return Some(Err(e));
219 }
220 };
221 Some(Ok(command))
222}
223
224fn decompress(command: &mut Command, file: File) -> Result<ChildStdout> {
225 let cmd = command
227 .stdin(file)
228 .stdout(Stdio::piped())
229 .spawn()
230 .map_err(|e| match e.kind() {
231 ErrorKind::NotFound => Error::other(format!(
232 "Program '{}' not found in PATH.",
233 command.get_program().to_str().unwrap()
234 )),
235 _ => e,
236 })?;
237 Ok(cmd.stdout.unwrap())
239}
240
241fn read_cpio_and_print_filenames<R: Read + SeekForward, W: Write>(
242 file: &mut R,
243 out: &mut W,
244) -> Result<()> {
245 let cpio = CpioFilenameReader { file };
246 for f in cpio {
247 let filename = f?;
248 writeln!(out, "{}", filename)?;
249 }
250 Ok(())
251}
252
253fn read_cpio_and_print_long_format<R: Read + SeekForward, W: Write>(
254 file: &mut R,
255 out: &mut W,
256 now: i64,
257 user_group_cache: &mut UserGroupCache,
258) -> Result<()> {
259 let mut last_mtime = 0;
262 let mut time_string: String = "".into();
263 loop {
264 let header = match Header::read(file) {
265 Ok(header) => {
266 if header.filename == "TRAILER!!!" {
267 break;
268 } else {
269 header
270 }
271 }
272 Err(e) => return Err(e),
273 };
274
275 let user = match user_group_cache.get_user(header.uid)? {
276 Some(name) => name,
277 None => header.uid.to_string(),
278 };
279 let group = match user_group_cache.get_group(header.gid)? {
280 Some(name) => name,
281 None => header.gid.to_string(),
282 };
283 let mode_string = header.mode_string();
284 if header.mtime != last_mtime || time_string.is_empty() {
285 last_mtime = header.mtime;
286 time_string = format_time(header.mtime, now)?;
287 };
288
289 match header.mode & MODE_FILETYPE_MASK {
290 FILETYPE_SYMLINK => {
291 let target = header.read_symlink_target(file)?;
292 writeln!(
293 out,
294 "{} {:>3} {:<8} {:<8} {:>8} {} {} -> {}",
295 std::str::from_utf8(&mode_string).unwrap(),
296 header.nlink,
297 user,
298 group,
299 header.filesize,
300 time_string,
301 header.filename,
302 target
303 )?;
304 }
305 FILETYPE_BLOCK_DEVICE | FILETYPE_CHARACTER_DEVICE => {
306 header.skip_file_content(file)?;
307 writeln!(
308 out,
309 "{} {:>3} {:<8} {:<8} {:>3}, {:>3} {} {}",
310 std::str::from_utf8(&mode_string).unwrap(),
311 header.nlink,
312 user,
313 group,
314 header.rmajor,
315 header.rminor,
316 time_string,
317 header.filename
318 )?;
319 }
320 _ => {
321 header.skip_file_content(file)?;
322 writeln!(
323 out,
324 "{} {:>3} {:<8} {:<8} {:>8} {} {}",
325 std::str::from_utf8(&mode_string).unwrap(),
326 header.nlink,
327 user,
328 group,
329 header.filesize,
330 time_string,
331 header.filename
332 )?;
333 }
334 };
335 }
336 Ok(())
337}
338
339fn create_dir_ignore_existing<P: AsRef<std::path::Path>>(path: P) -> Result<()> {
340 if let Err(e) = create_dir(&path) {
341 if e.kind() != ErrorKind::AlreadyExists {
342 return Err(e);
343 }
344 let stat = symlink_metadata(&path)?;
345 if !stat.is_dir() {
346 remove_file(&path)?;
347 create_dir(&path)?;
348 }
349 };
350 Ok(())
351}
352
353fn write_character_device(
354 header: &Header,
355 preserve_permissions: bool,
356 log_level: u32,
357) -> Result<()> {
358 if header.filesize != 0 {
359 return Err(Error::new(
360 ErrorKind::InvalidData,
361 format!(
362 "Invalid size for character device '{}': {} bytes instead of 0.",
363 header.filename, header.filesize
364 ),
365 ));
366 };
367 if log_level >= LOG_LEVEL_DEBUG {
368 writeln!(
369 std::io::stderr(),
370 "Creating character device '{}' with mode {:o}",
371 header.filename,
372 header.mode_perm(),
373 )?;
374 };
375 if let Err(e) = mknod(&header.filename, header.mode, header.rmajor, header.rminor) {
376 match e.kind() {
377 ErrorKind::AlreadyExists => {
378 remove_file(&header.filename)?;
379 mknod(&header.filename, header.mode, header.rmajor, header.rminor)?;
380 }
381 _ => {
382 return Err(e);
383 }
384 }
385 };
386 if preserve_permissions {
387 lchown(&header.filename, Some(header.uid), Some(header.gid))?;
388 };
389 set_permissions(&header.filename, header.permission())?;
390 set_modified(&header.filename, header.mtime.into())?;
391 Ok(())
392}
393
394fn write_directory(
395 header: &Header,
396 preserve_permissions: bool,
397 log_level: u32,
398 mtimes: &mut BTreeMap<String, i64>,
399) -> Result<()> {
400 if header.filesize != 0 {
401 return Err(Error::new(
402 ErrorKind::InvalidData,
403 format!(
404 "Invalid size for directory '{}': {} bytes instead of 0.",
405 header.filename, header.filesize
406 ),
407 ));
408 };
409 if log_level >= LOG_LEVEL_DEBUG {
410 writeln!(
411 std::io::stderr(),
412 "Creating directory '{}' with mode {:o}{}",
413 header.filename,
414 header.mode_perm(),
415 if preserve_permissions {
416 format!(" and owner {}:{}", header.uid, header.gid)
417 } else {
418 String::new()
419 },
420 )?;
421 };
422 create_dir_ignore_existing(&header.filename)?;
423 if preserve_permissions {
424 chown(&header.filename, Some(header.uid), Some(header.gid))?;
425 }
426 set_permissions(&header.filename, header.permission())?;
427 mtimes.insert(header.filename.to_string(), header.mtime.into());
428 Ok(())
429}
430
431fn from_mtime(mtime: u32) -> SystemTime {
432 std::time::UNIX_EPOCH + std::time::Duration::from_secs(mtime.into())
433}
434
435fn write_file<R: Read + SeekForward>(
436 cpio_file: &mut R,
437 header: &Header,
438 preserve_permissions: bool,
439 seen_files: &mut SeenFiles,
440 log_level: u32,
441) -> Result<()> {
442 let mut file;
443 if let Some(target) = header.try_get_hard_link_target(seen_files) {
444 if log_level >= LOG_LEVEL_DEBUG {
445 writeln!(
446 std::io::stderr(),
447 "Creating hard-link '{}' -> '{}' with permission {:o}{} and {} bytes",
448 header.filename,
449 target,
450 header.mode_perm(),
451 if preserve_permissions {
452 format!(" and owner {}:{}", header.uid, header.gid)
453 } else {
454 String::new()
455 },
456 header.filesize,
457 )?;
458 };
459 if let Err(e) = hard_link(target, &header.filename) {
460 match e.kind() {
461 ErrorKind::AlreadyExists => {
462 remove_file(&header.filename)?;
463 hard_link(target, &header.filename)?;
464 }
465 _ => {
466 return Err(e);
467 }
468 }
469 }
470 file = OpenOptions::new().write(true).open(&header.filename)?
471 } else {
472 if log_level >= LOG_LEVEL_DEBUG {
473 writeln!(
474 std::io::stderr(),
475 "Creating file '{}' with permission {:o}{} and {} bytes",
476 header.filename,
477 header.mode_perm(),
478 if preserve_permissions {
479 format!(" and owner {}:{}", header.uid, header.gid)
480 } else {
481 String::new()
482 },
483 header.filesize,
484 )?;
485 };
486 file = File::create(&header.filename)?
487 };
488 header.mark_seen(seen_files);
489 let mut reader = cpio_file.take(header.filesize.into());
490 let written = std::io::copy(&mut reader, &mut file)?;
493 if written != header.filesize.into() {
494 return Err(Error::other(format!(
495 "Wrong amound of bytes written to '{}': {} != {}.",
496 header.filename, written, header.filesize
497 )));
498 }
499 let skip = align_to_4_bytes(header.filesize);
500 cpio_file.seek_forward(skip.into())?;
501 if preserve_permissions {
502 fchown(&file, Some(header.uid), Some(header.gid))?;
503 }
504 file.set_permissions(header.permission())?;
505 file.set_modified(from_mtime(header.mtime))?;
506 Ok(())
507}
508
509fn write_symbolic_link<R: Read + SeekForward>(
510 cpio_file: &mut R,
511 header: &Header,
512 preserve_permissions: bool,
513 log_level: u32,
514) -> Result<()> {
515 let target = header.read_symlink_target(cpio_file)?;
516 if log_level >= LOG_LEVEL_DEBUG {
517 writeln!(
518 std::io::stderr(),
519 "Creating symlink '{}' -> '{}' with mode {:o}",
520 header.filename,
521 &target,
522 header.mode_perm(),
523 )?;
524 };
525 if let Err(e) = symlink(&target, &header.filename) {
526 match e.kind() {
527 ErrorKind::AlreadyExists => {
528 remove_file(&header.filename)?;
529 symlink(&target, &header.filename)?;
530 }
531 _ => {
532 return Err(e);
533 }
534 }
535 }
536 if preserve_permissions {
537 lchown(&header.filename, Some(header.uid), Some(header.gid))?;
538 }
539 if header.mode_perm() != 0o777 {
540 return Err(Error::new(
541 ErrorKind::Unsupported,
542 format!(
543 "Symlink '{}' has mode {:o}, but only mode 777 is supported.",
544 header.filename,
545 header.mode_perm()
546 ),
547 ));
548 };
549 set_modified(&header.filename, header.mtime.into())?;
550 Ok(())
551}
552
553fn read_cpio_and_extract<R: Read + SeekForward>(
554 file: &mut R,
555 preserve_permissions: bool,
556 log_level: u32,
557) -> Result<()> {
558 let mut extractor = Extractor::new();
559 loop {
560 let header = match Header::read(file) {
561 Ok(header) => {
562 if header.filename == "TRAILER!!!" {
563 break;
564 } else {
565 header
566 }
567 }
568 Err(e) => return Err(e),
569 };
570
571 if log_level >= LOG_LEVEL_DEBUG {
572 writeln!(std::io::stderr(), "{:?}", header)?;
573 } else if log_level >= LOG_LEVEL_INFO {
574 writeln!(std::io::stderr(), "{}", header.filename)?;
575 }
576
577 match header.mode & MODE_FILETYPE_MASK {
578 FILETYPE_CHARACTER_DEVICE => {
579 write_character_device(&header, preserve_permissions, log_level)?
580 }
581 FILETYPE_DIRECTORY => write_directory(
582 &header,
583 preserve_permissions,
584 log_level,
585 &mut extractor.mtimes,
586 )?,
587 FILETYPE_REGULAR_FILE => write_file(
588 file,
589 &header,
590 preserve_permissions,
591 &mut extractor.seen_files,
592 log_level,
593 )?,
594 FILETYPE_SYMLINK => {
595 write_symbolic_link(file, &header, preserve_permissions, log_level)?
596 }
597 FILETYPE_FIFO | FILETYPE_BLOCK_DEVICE | FILETYPE_SOCKET => {
598 unimplemented!(
599 "Mode {:o} (file {}) not implemented. Please open a bug report requesting support for this type.",
600 header.mode, header.filename
601 )
602 }
603 _ => {
604 return Err(Error::new(
605 ErrorKind::InvalidData,
606 format!(
607 "Invalid/unknown filetype {:o}: {}",
608 header.mode, header.filename
609 ),
610 ))
611 }
612 };
613 }
614 extractor.set_modified_times(log_level)?;
615 Ok(())
616}
617
618fn seek_to_cpio_end(file: &mut File) -> Result<()> {
619 let cpio = CpioFilenameReader { file };
620 for f in cpio {
621 f?;
622 }
623 Ok(())
624}
625
626pub fn get_cpio_archive_count(file: &mut File) -> Result<u32> {
627 let mut count = 0;
628 loop {
629 let command = match read_magic_header(file) {
630 None => return Ok(count),
631 Some(x) => x?,
632 };
633 count += 1;
634 if command.get_program() == "cpio" {
635 seek_to_cpio_end(file)?;
636 } else {
637 break;
638 }
639 }
640 Ok(count)
641}
642
643pub fn print_cpio_archive_count<W: Write>(mut file: File, out: &mut W) -> Result<()> {
644 let count = get_cpio_archive_count(&mut file)?;
645 writeln!(out, "{}", count)?;
646 Ok(())
647}
648
649pub fn examine_cpio_content<W: Write>(mut file: File, out: &mut W) -> Result<()> {
650 loop {
651 let command = match read_magic_header(&mut file) {
652 None => return Ok(()),
653 Some(x) => x?,
654 };
655 writeln!(
656 out,
657 "{}\t{}",
658 file.stream_position()?,
659 command.get_program().to_str().unwrap()
660 )?;
661 if command.get_program() == "cpio" {
662 seek_to_cpio_end(&mut file)?;
663 } else {
664 break;
665 }
666 }
667 Ok(())
668}
669
670pub fn extract_cpio_archive(
671 mut file: File,
672 preserve_permissions: bool,
673 subdir: Option<String>,
674 log_level: u32,
675) -> Result<()> {
676 let mut count = 1;
677 let base_dir = std::env::current_dir()?;
678 loop {
679 if let Some(ref s) = subdir {
680 let mut dir = base_dir.clone();
681 dir.push(format!("{s}{count}"));
682 create_dir_ignore_existing(&dir)?;
683 std::env::set_current_dir(&dir)?;
684 }
685 let mut command = match read_magic_header(&mut file) {
686 None => return Ok(()),
687 Some(x) => x?,
688 };
689 if command.get_program() == "cpio" {
690 read_cpio_and_extract(&mut file, preserve_permissions, log_level)?;
691 } else {
692 let mut decompressed = decompress(&mut command, file)?;
693 read_cpio_and_extract(&mut decompressed, preserve_permissions, log_level)?;
694 break;
695 }
696 count += 1;
697 }
698 Ok(())
699}
700
701pub fn list_cpio_content<W: Write>(mut file: File, out: &mut W, log_level: u32) -> Result<()> {
702 let mut user_group_cache = UserGroupCache::new();
703 let now = SystemTime::now()
704 .duration_since(SystemTime::UNIX_EPOCH)
705 .unwrap()
706 .as_secs()
707 .try_into()
708 .unwrap();
709 loop {
710 let mut command = match read_magic_header(&mut file) {
711 None => return Ok(()),
712 Some(x) => x?,
713 };
714 if command.get_program() == "cpio" {
715 if log_level >= LOG_LEVEL_INFO {
716 read_cpio_and_print_long_format(&mut file, out, now, &mut user_group_cache)?;
717 } else {
718 read_cpio_and_print_filenames(&mut file, out)?;
719 }
720 } else {
721 let mut decompressed = decompress(&mut command, file)?;
722 if log_level >= LOG_LEVEL_INFO {
723 read_cpio_and_print_long_format(
724 &mut decompressed,
725 out,
726 now,
727 &mut user_group_cache,
728 )?;
729 } else {
730 read_cpio_and_print_filenames(&mut decompressed, out)?;
731 }
732 break;
733 }
734 }
735 Ok(())
736}
737
738#[cfg(test)]
739mod tests {
740 use std::env;
741 use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt};
742
743 use super::*;
744 use crate::libc::{major, minor};
745
746 fn getgid() -> u32 {
747 unsafe { ::libc::getgid() }
748 }
749
750 fn getuid() -> u32 {
751 unsafe { ::libc::getuid() }
752 }
753
754 extern "C" {
755 fn tzset();
756 }
757
758 impl UserGroupCache {
759 fn insert_test_data(&mut self) {
760 self.user_cache.insert(1000, Some("user".into()));
761 self.group_cache.insert(123, Some("whoopsie".into()));
762 self.group_cache.insert(2000, None);
763 }
764 }
765
766 #[test]
767 fn test_align_to_4_bytes() {
768 assert_eq!(align_to_4_bytes(110), 2);
769 }
770
771 #[test]
772 fn test_align_to_4_bytes_is_aligned() {
773 assert_eq!(align_to_4_bytes(32), 0);
774 }
775
776 #[test]
777 fn test_decompress_program_not_found() {
778 let file = File::open("tests/single.cpio").expect("test cpio should be present");
779 let mut cmd = Command::new("non-existing-program");
780 let got = decompress(&mut cmd, file).unwrap_err();
781 assert_eq!(got.kind(), ErrorKind::Other);
782 assert_eq!(
783 got.to_string(),
784 "Program 'non-existing-program' not found in PATH."
785 );
786 }
787
788 #[test]
789 fn test_get_cpio_archive_count_single() {
790 let mut file = File::open("tests/single.cpio").expect("test cpio should be present");
791 let count = get_cpio_archive_count(&mut file).unwrap();
792 assert_eq!(count, 1);
793 }
794
795 #[test]
796 fn test_print_cpio_archive_count() {
797 let mut file = File::open("tests/zstd.cpio").expect("test cpio should be present");
798 let mut output = Vec::new();
799
800 let count = get_cpio_archive_count(&mut file).unwrap();
801 assert_eq!(count, 2);
802
803 file.seek(SeekFrom::Start(0)).unwrap();
804 print_cpio_archive_count(file, &mut output).unwrap();
805 assert_eq!(String::from_utf8(output).unwrap(), "2\n");
806 }
807
808 #[test]
809 fn test_read_cpio_and_print_long_format_character_device() {
810 let cpio_data = b"07070100000003000021A4000000000000\
812 00000000000167055BC800000000000000000000000000000005000000010000\
813 000C00000000dev/console\0\0\0\
814 0707010000000000000000000000000000000000000001\
815 0000000000000000000000000000000000000000000000000000000B00000000\
816 TRAILER!!!\0\0\0\0";
817 let mut output = Vec::new();
818 let mut user_group_cache = UserGroupCache::new();
819 env::set_var("TZ", "UTC");
820 unsafe { tzset() };
821 read_cpio_and_print_long_format(
822 &mut cpio_data.as_ref(),
823 &mut output,
824 1728486311,
825 &mut user_group_cache,
826 )
827 .unwrap();
828 assert_eq!(
829 String::from_utf8(output).unwrap(),
830 "crw-r--r-- 1 root root 5, 1 Oct 8 16:20 dev/console\n"
831 );
832 }
833
834 #[test]
835 fn test_read_cpio_and_print_long_format_directory() {
836 let cpio_data = b"07070100000001000047FF000000000000007B00000002\
838 66A6E40400000000000000000000000000000000000000000000000B00000000\
839 /var/crash\0\0\0\0\
840 0707010000000000000000000000000000000000000001\
841 0000000000000000000000000000000000000000000000000000000B00000000\
842 TRAILER!!!\0\0\0\0";
843 let mut output = Vec::new();
844 let mut user_group_cache = UserGroupCache::new();
845 user_group_cache.insert_test_data();
846 env::set_var("TZ", "UTC");
847 unsafe { tzset() };
848 read_cpio_and_print_long_format(
849 &mut cpio_data.as_ref(),
850 &mut output,
851 1722389471,
852 &mut user_group_cache,
853 )
854 .unwrap();
855 assert_eq!(
856 String::from_utf8(output).unwrap(),
857 "drwxrwsrwt 2 root whoopsie 0 Jul 29 00:36 /var/crash\n"
858 );
859 }
860
861 #[test]
862 fn test_read_cpio_and_print_long_format_file() {
863 let cpio_data = b"070701000036E4000081A4000003E8000007D000000001\
865 66A3285300000041000000000000002400000000000000000000000D00000000\
866 conf/modules\0\0\
867 linear\nmultipath\nraid0\nraid1\nraid456\nraid5\nraid6\nraid10\nefivarfs\0\0\0\0\
868 0707010000000000000000000000000000000000000001\
869 0000000000000000000000000000000000000000000000000000000B00000000\
870 TRAILER!!!\0\0\0\0";
871 let mut output = Vec::new();
872 let mut user_group_cache = UserGroupCache::new();
873 user_group_cache.insert_test_data();
874 env::set_var("TZ", "UTC");
875 unsafe { tzset() };
876 read_cpio_and_print_long_format(
877 &mut cpio_data.as_ref(),
878 &mut output,
879 1722645915,
880 &mut user_group_cache,
881 )
882 .unwrap();
883 assert_eq!(
884 String::from_utf8(output).unwrap(),
885 "-rw-r--r-- 1 user 2000 65 Jul 26 04:38 conf/modules\n"
886 );
887 }
888
889 #[test]
890 fn test_read_cpio_and_print_long_format_symlink() {
891 let cpio_data = b"0707010000000D0000A1FF000000000000000000000001\
893 6237389400000007000000000000000000000000000000000000000400000000\
894 bin\0\0\0usr/bin\0\
895 0707010000000000000000000000000000000000000001\
896 0000000000000000000000000000000000000000000000000000000B00000000\
897 TRAILER!!!\0\0\0\0";
898 let mut output = Vec::new();
899 let mut user_group_cache = UserGroupCache::new();
900 user_group_cache.insert_test_data();
901 read_cpio_and_print_long_format(
902 &mut cpio_data.as_ref(),
903 &mut output,
904 1722645915,
905 &mut user_group_cache,
906 )
907 .unwrap();
908 assert_eq!(
909 String::from_utf8(output).unwrap(),
910 "lrwxrwxrwx 1 root root 7 Mar 20 2022 bin -> usr/bin\n"
911 );
912 }
913
914 #[test]
915 fn test_write_character_device() {
916 if getuid() != 0 {
917 return;
919 }
920 let mut header = Header::new(1, 0o20_644, 0, 0, 0, 1740402179, 0, "./null".into());
921 header.rmajor = 1;
922 header.rminor = 3;
923 write_character_device(&header, true, LOG_LEVEL_WARNING).unwrap();
924
925 let attr = std::fs::metadata("null").unwrap();
926 assert_eq!(attr.len(), header.filesize.into());
927 assert!(attr.file_type().is_char_device());
928 assert_eq!(attr.modified().unwrap(), from_mtime(header.mtime));
929 assert_eq!(attr.permissions(), PermissionsExt::from_mode(header.mode));
930 assert_eq!(attr.uid(), header.uid);
931 assert_eq!(attr.gid(), header.gid);
932 assert_eq!(major(attr.rdev()), header.rmajor);
933 assert_eq!(minor(attr.rdev()), header.rminor);
934 std::fs::remove_file("null").unwrap();
935 }
936
937 #[test]
938 fn test_write_directory_with_setuid() {
939 let mut mtimes = BTreeMap::new();
940 let header = Header::new(
941 1,
942 0o43_777,
943 getuid(),
944 getgid(),
945 0,
946 1720081471,
947 0,
948 "./directory_with_setuid".into(),
949 );
950 write_directory(&header, true, LOG_LEVEL_WARNING, &mut mtimes).unwrap();
951
952 let attr = std::fs::metadata("directory_with_setuid").unwrap();
953 assert!(attr.is_dir());
954 assert_eq!(attr.permissions(), PermissionsExt::from_mode(header.mode));
955 assert_eq!(attr.uid(), header.uid);
956 assert_eq!(attr.gid(), header.gid);
957 std::fs::remove_dir("directory_with_setuid").unwrap();
958
959 let mut expected_mtimes: BTreeMap<String, i64> = BTreeMap::new();
960 expected_mtimes.insert("./directory_with_setuid".into(), header.mtime.into());
961 assert_eq!(mtimes, expected_mtimes);
962 }
963
964 #[test]
965 fn test_write_file_with_setuid() {
966 let mut seen_files = SeenFiles::new();
967 let header = Header::new(
968 1,
969 0o104_755,
970 getuid(),
971 getgid(),
972 0,
973 1720081471,
974 9,
975 "./file_with_setuid".into(),
976 );
977 let cpio = b"!/bin/sh\n\0\0\0";
978 write_file(
979 &mut cpio.as_ref(),
980 &header,
981 true,
982 &mut seen_files,
983 LOG_LEVEL_WARNING,
984 )
985 .unwrap();
986
987 let attr = std::fs::metadata("file_with_setuid").unwrap();
988 assert_eq!(attr.len(), header.filesize.into());
989 assert!(attr.is_file());
990 assert_eq!(attr.modified().unwrap(), from_mtime(header.mtime));
991 assert_eq!(attr.permissions(), PermissionsExt::from_mode(header.mode));
992 assert_eq!(attr.uid(), header.uid);
993 assert_eq!(attr.gid(), header.gid);
994 std::fs::remove_file("file_with_setuid").unwrap();
995 }
996
997 #[test]
998 fn test_write_symbolic_link() {
999 let header = Header::new(
1000 1,
1001 0o120_777,
1002 getuid(),
1003 getgid(),
1004 0,
1005 1721427072,
1006 12,
1007 "./dead_symlink".into(),
1008 );
1009 let cpio = b"/nonexistent";
1010 write_symbolic_link(&mut cpio.as_ref(), &header, true, LOG_LEVEL_WARNING).unwrap();
1011
1012 let attr = std::fs::symlink_metadata("dead_symlink").unwrap();
1013 assert_eq!(attr.len(), header.filesize.into());
1014 assert!(attr.is_symlink());
1015 assert_eq!(attr.modified().unwrap(), from_mtime(header.mtime));
1016 assert_eq!(attr.permissions(), PermissionsExt::from_mode(header.mode));
1017 assert_eq!(attr.uid(), header.uid);
1018 assert_eq!(attr.gid(), header.gid);
1019 std::fs::remove_file("dead_symlink").unwrap();
1020 }
1021}