threecpio/
lib.rs

1// Copyright (C) 2024, Benjamin Drung <bdrung@posteo.de>
2// SPDX-License-Identifier: ISC
3
4use 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    /// Translate user ID (UID) to user name and cache result.
66    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    /// Translate group ID (GID) to group name and cache result.
78    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
90/// Format the time in a similar way to coreutils' ls command.
91fn format_time(timestamp: u32, now: i64) -> Result<String> {
92    // Logic from coreutils ls command:
93    // Consider a time to be recent if it is within the past six months.
94    // A Gregorian year has 365.2425 * 24 * 60 * 60 == 31556952 seconds
95    // on the average.
96    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
104// TODO: Document hardlink structure
105type 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
140/// Read only the file name from the next cpio object.
141///
142/// Read the next cpio object header, check the magic, skip the file data.
143/// Return the file name.
144fn 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        // Different magic numbers (little endian) for lz4:
175        // v0.1-v0.9: 0x184C2102
176        // v1.0-v1.3: 0x184C2103
177        // v1.4+: 0x184D2204
178        [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        // Full magic number for lzop: [0x89, 0x4C, 0x5A, 0x4F, 0x00, 0x0D, 0x0A, 0x1A, 0x0A]
189        [0x89, 0x4C, 0x5A, 0x4F] => {
190            let mut cmd = Command::new("lzop");
191            cmd.arg("-cd");
192            cmd
193        }
194        // Full magic number for xz: [0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00]
195        [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    // TODO: Propper error message if spawn fails
226    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    // TODO: Should unwrap be replaced by returning Result?
238    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    // Files can have the same mtime (especially when using SOURCE_DATE_EPOCH).
260    // Cache the time string of the last mtime.
261    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    // TODO: check writing hard-link with length == 0
491    // TODO: check overwriting existing files/hardlinks
492    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        // Wrapped before mtime and filename
811        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        // Wrapped before mtime and filename
837        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        // Wrapped before mtime and filename
864        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        // Wrapped before mtime and filename
892        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            // This test needs to run as root.
918            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}