// Copyright (C) 2024-2025, Benjamin Drung <bdrung@posteo.de>
// SPDX-License-Identifier: ISC

use std::collections::BTreeMap;
use std::fs::{
    create_dir, create_dir_all, hard_link, remove_file, set_permissions, symlink_metadata, File,
    OpenOptions,
};
use std::io::{prelude::*, Error, ErrorKind, Result};
use std::os::unix::fs::{chown, fchown, lchown, symlink};
use std::path::{Path, PathBuf};
use std::time::SystemTime;

use glob::Pattern;

use crate::compression::read_magic_header;
use crate::filetype::*;
use crate::header::Header;
use crate::libc::{mknod, set_modified};
use crate::ranges::Ranges;
use crate::seek_forward::SeekForward;
use crate::{filename_matches, seek_to_cpio_end, SeenFiles, LOG_LEVEL_DEBUG, LOG_LEVEL_INFO};

#[derive(Clone, Debug, PartialEq)]
pub struct ExtractOptions {
    make_directories: bool,
    parts: Option<Ranges>,
    patterns: Vec<Pattern>,
    preserve_permissions: bool,
    subdir: Option<String>,
}

impl ExtractOptions {
    pub fn new(
        make_directories: bool,
        parts: Option<Ranges>,
        patterns: Vec<Pattern>,
        preserve_permissions: bool,
        subdir: Option<String>,
    ) -> Self {
        Self {
            make_directories,
            parts,
            patterns,
            preserve_permissions,
            subdir,
        }
    }
}

impl Default for ExtractOptions {
    fn default() -> Self {
        Self::new(false, None, Vec::new(), false, None)
    }
}

struct Extractor {
    seen_files: SeenFiles,
    mtimes: BTreeMap<String, i64>,
}

impl Extractor {
    fn new() -> Extractor {
        Extractor {
            seen_files: SeenFiles::new(),
            mtimes: BTreeMap::new(),
        }
    }

    fn set_modified_times(&self, log_level: u32) -> Result<()> {
        for (path, mtime) in self.mtimes.iter().rev() {
            if log_level >= LOG_LEVEL_DEBUG {
                writeln!(std::io::stderr(), "set mtime {mtime} for '{path}'")?;
            };
            set_modified(path, *mtime)?;
        }
        Ok(())
    }
}

fn absolute_parent_directory<S: AsRef<str>>(path: S, base_dir: &Path) -> Result<PathBuf>
where
    PathBuf: From<S>,
{
    let abspath = if path.as_ref().starts_with("/") {
        PathBuf::from(path)
    } else {
        base_dir.join(path.as_ref())
    };
    match abspath.parent() {
        Some(d) => Ok(d.into()),
        // TODO: Use ErrorKind::InvalidFilename once stable.
        None => Err(Error::new(
            ErrorKind::InvalidData,
            format!("Path {abspath:#?} has no parent directory."),
        )),
    }
}

fn check_path_is_canonical_subdir<S: AsRef<str> + std::fmt::Display>(
    path: S,
    dir: &Path,
    base_dir: &PathBuf,
) -> Result<PathBuf> {
    let canonicalized_path = dir.canonicalize()?;
    if !canonicalized_path.starts_with(base_dir) {
        return Err(Error::new(
            ErrorKind::InvalidData,
            format!(
                "The parent directory of \"{path}\" (resolved to {canonicalized_path:#?}) \
                is not within the directory {base_dir:#?}.",
            ),
        ));
    }
    Ok(canonicalized_path)
}

fn create_dir_ignore_existing<P: AsRef<std::path::Path>>(path: P) -> Result<()> {
    if let Err(e) = create_dir(&path) {
        if e.kind() != ErrorKind::AlreadyExists {
            return Err(e);
        }
        let stat = symlink_metadata(&path)?;
        if !stat.is_dir() {
            remove_file(&path)?;
            create_dir(&path)?;
        }
    };
    Ok(())
}

pub fn extract_cpio_archive<W: Write>(
    mut archive: File,
    mut out: Option<&mut W>,
    options: &ExtractOptions,
    log_level: u32,
) -> Result<()> {
    let mut count = 0;
    let base_dir = std::env::current_dir()?;
    loop {
        count += 1;
        let compression = match read_magic_header(&mut archive) {
            None => return Ok(()),
            Some(x) => x?,
        };
        if options.parts.as_ref().is_some_and(|f| !f.contains(&count)) {
            if compression.is_uncompressed() && options.parts.as_ref().unwrap().has_more(&count) {
                seek_to_cpio_end(&mut archive)?;
                continue;
            }
            break;
        }
        let mut dir = base_dir.clone();
        if let Some(ref s) = options.subdir {
            dir.push(format!("{s}{count}"));
            create_dir_ignore_existing(&dir)?;
            std::env::set_current_dir(&dir)?;
        }
        if compression.is_uncompressed() {
            read_cpio_and_extract(&mut archive, &dir, &mut out, options, log_level)?;
        } else {
            let mut decompressed = compression.decompress(archive)?;
            read_cpio_and_extract(&mut decompressed, &dir, &mut out, options, log_level)?;
            break;
        }
    }
    Ok(())
}

fn from_mtime(mtime: u32) -> SystemTime {
    std::time::UNIX_EPOCH + std::time::Duration::from_secs(mtime.into())
}

fn read_cpio_and_extract<R: Read + SeekForward, W: Write>(
    archive: &mut R,
    base_dir: &PathBuf,
    out: &mut Option<W>,
    options: &ExtractOptions,
    log_level: u32,
) -> Result<()> {
    let mut extractor = Extractor::new();
    let mut previous_checked_dir = PathBuf::new();
    loop {
        let header = match Header::read(archive) {
            Ok(header) => {
                if header.filename == "TRAILER!!!" {
                    break;
                } else {
                    header
                }
            }
            Err(e) => return Err(e),
        };

        if log_level >= LOG_LEVEL_DEBUG {
            writeln!(std::io::stderr(), "{header:?}")?;
        }

        if !options.patterns.is_empty() && !filename_matches(&header.filename, &options.patterns) {
            header.skip_file_content(archive)?;
            continue;
        }

        if log_level >= LOG_LEVEL_INFO {
            writeln!(std::io::stderr(), "{}", header.filename)?;
        }

        if out.is_none() && !header.is_root_directory() {
            // TODO: use dirfd once stable: https://github.com/rust-lang/rust/issues/120426
            let absdir = absolute_parent_directory(&header.filename, base_dir)?;
            // canonicalize() is an expensive call. So cache the previously resolved
            // parent directory. Skip the path traversal check in case the absolute
            // parent directory has no symlinks and matches the previouly checked directory.
            if absdir != previous_checked_dir {
                if options.make_directories {
                    create_dir_all(&absdir)?;
                }
                previous_checked_dir =
                    check_path_is_canonical_subdir(&header.filename, &absdir, base_dir)?;
            }
        }

        if let Some(out) = out {
            if header.filesize == 0 {
                continue;
            }
            match header.mode & MODE_FILETYPE_MASK {
                FILETYPE_DIRECTORY | FILETYPE_SYMLINK => {
                    header.skip_file_content(archive)?;
                }
                FILETYPE_REGULAR_FILE => write_file_content(archive, out, &header)?,
                FILETYPE_CHARACTER_DEVICE
                | FILETYPE_FIFO
                | FILETYPE_BLOCK_DEVICE
                | FILETYPE_SOCKET => {
                    unimplemented!(
                        "Mode {:o} (file {}) not implemented. Please open a bug report requesting support for this type.",
                        header.mode, header.filename
                    )
                }
                _ => {
                    return Err(Error::new(
                        ErrorKind::InvalidData,
                        format!(
                            "Invalid/unknown filetype {:o}: {}",
                            header.mode, header.filename
                        ),
                    ))
                }
            }
        } else {
            match header.mode & MODE_FILETYPE_MASK {
                FILETYPE_CHARACTER_DEVICE => {
                    write_character_device(&header, options.preserve_permissions, log_level)?
                }
                FILETYPE_DIRECTORY => write_directory(
                    &header,
                    options.preserve_permissions,
                    log_level,
                    &mut extractor.mtimes,
                )?,
                FILETYPE_REGULAR_FILE => write_file(
                    archive,
                    &header,
                    options.preserve_permissions,
                    &mut extractor.seen_files,
                    log_level,
                )?,
                FILETYPE_SYMLINK => {
                    write_symbolic_link(archive, &header, options.preserve_permissions, log_level)?
                }
                FILETYPE_FIFO | FILETYPE_BLOCK_DEVICE | FILETYPE_SOCKET => {
                    unimplemented!(
                        "Mode {:o} (file {}) not implemented. Please open a bug report requesting support for this type.",
                        header.mode, header.filename
                    )
                }
                _ => {
                    return Err(Error::new(
                        ErrorKind::InvalidData,
                        format!(
                            "Invalid/unknown filetype {:o}: {}",
                            header.mode, header.filename
                        ),
                    ))
                }
            }
        };
    }
    extractor.set_modified_times(log_level)?;
    Ok(())
}

fn write_character_device(
    header: &Header,
    preserve_permissions: bool,
    log_level: u32,
) -> Result<()> {
    if header.filesize != 0 {
        return Err(Error::new(
            ErrorKind::InvalidData,
            format!(
                "Invalid size for character device '{}': {} bytes instead of 0.",
                header.filename, header.filesize
            ),
        ));
    };
    if log_level >= LOG_LEVEL_DEBUG {
        writeln!(
            std::io::stderr(),
            "Creating character device '{}' with mode {:o}",
            header.filename,
            header.mode_perm(),
        )?;
    };
    if let Err(e) = mknod(&header.filename, header.mode, header.rmajor, header.rminor) {
        match e.kind() {
            ErrorKind::AlreadyExists => {
                remove_file(&header.filename)?;
                mknod(&header.filename, header.mode, header.rmajor, header.rminor)?;
            }
            _ => {
                return Err(e);
            }
        }
    };
    if preserve_permissions {
        lchown(&header.filename, Some(header.uid), Some(header.gid))?;
    };
    set_permissions(&header.filename, header.permission())?;
    set_modified(&header.filename, header.mtime.into())?;
    Ok(())
}

fn write_directory(
    header: &Header,
    preserve_permissions: bool,
    log_level: u32,
    mtimes: &mut BTreeMap<String, i64>,
) -> Result<()> {
    if header.filesize != 0 {
        return Err(Error::new(
            ErrorKind::InvalidData,
            format!(
                "Invalid size for directory '{}': {} bytes instead of 0.",
                header.filename, header.filesize
            ),
        ));
    };
    if log_level >= LOG_LEVEL_DEBUG {
        writeln!(
            std::io::stderr(),
            "Creating directory '{}' with mode {:o}{}",
            header.filename,
            header.mode_perm(),
            if preserve_permissions {
                format!(" and owner {}:{}", header.uid, header.gid)
            } else {
                String::new()
            },
        )?;
    };
    create_dir_ignore_existing(&header.filename)?;
    if preserve_permissions {
        chown(&header.filename, Some(header.uid), Some(header.gid))?;
    }
    set_permissions(&header.filename, header.permission())?;
    mtimes.insert(header.filename.to_string(), header.mtime.into());
    Ok(())
}

fn write_file<R: Read + SeekForward>(
    archive: &mut R,
    header: &Header,
    preserve_permissions: bool,
    seen_files: &mut SeenFiles,
    log_level: u32,
) -> Result<()> {
    let mut file;
    if let Some(target) = header.try_get_hard_link_target(seen_files) {
        if log_level >= LOG_LEVEL_DEBUG {
            writeln!(
                std::io::stderr(),
                "Creating hard-link '{}' -> '{}' with permission {:o}{} and {} bytes",
                header.filename,
                target,
                header.mode_perm(),
                if preserve_permissions {
                    format!(" and owner {}:{}", header.uid, header.gid)
                } else {
                    String::new()
                },
                header.filesize,
            )?;
        };
        if let Err(e) = hard_link(target, &header.filename) {
            match e.kind() {
                ErrorKind::AlreadyExists => {
                    remove_file(&header.filename)?;
                    hard_link(target, &header.filename)?;
                }
                _ => {
                    return Err(e);
                }
            }
        }
        file = OpenOptions::new().write(true).open(&header.filename)?
    } else {
        if log_level >= LOG_LEVEL_DEBUG {
            writeln!(
                std::io::stderr(),
                "Creating file '{}' with permission {:o}{} and {} bytes",
                header.filename,
                header.mode_perm(),
                if preserve_permissions {
                    format!(" and owner {}:{}", header.uid, header.gid)
                } else {
                    String::new()
                },
                header.filesize,
            )?;
        };
        file = File::create(&header.filename)?
    };
    header.mark_seen(seen_files);
    // TODO: check writing hard-link with length == 0
    // TODO: check overwriting existing files/hardlinks
    write_file_content(archive, &mut file, header)?;
    if preserve_permissions {
        fchown(&file, Some(header.uid), Some(header.gid))?;
    }
    file.set_permissions(header.permission())?;
    file.set_modified(from_mtime(header.mtime))?;
    Ok(())
}

fn write_file_content<R: Read + SeekForward, W: Write>(
    archive: &mut R,
    output_file: &mut W,
    header: &Header,
) -> Result<()> {
    let mut reader = archive.take(header.filesize.into());
    let written = std::io::copy(&mut reader, output_file)?;
    if written != header.filesize.into() {
        return Err(Error::other(format!(
            "Wrong amound of bytes written to '{}': {} != {}.",
            header.filename, written, header.filesize
        )));
    }
    header.skip_file_content_padding(archive)
}

fn write_symbolic_link<R: Read + SeekForward>(
    archive: &mut R,
    header: &Header,
    preserve_permissions: bool,
    log_level: u32,
) -> Result<()> {
    let target = header.read_symlink_target(archive)?;
    if log_level >= LOG_LEVEL_DEBUG {
        writeln!(
            std::io::stderr(),
            "Creating symlink '{}' -> '{}' with mode {:o}",
            header.filename,
            &target,
            header.mode_perm(),
        )?;
    };
    if let Err(e) = symlink(&target, &header.filename) {
        match e.kind() {
            ErrorKind::AlreadyExists => {
                remove_file(&header.filename)?;
                symlink(&target, &header.filename)?;
            }
            _ => {
                return Err(e);
            }
        }
    }
    if preserve_permissions {
        lchown(&header.filename, Some(header.uid), Some(header.gid))?;
    }
    if header.mode_perm() != 0o777 {
        return Err(Error::new(
            ErrorKind::Unsupported,
            format!(
                "Symlink '{}' has mode {:o}, but only mode 777 is supported.",
                header.filename,
                header.mode_perm()
            ),
        ));
    };
    set_modified(&header.filename, header.mtime.into())?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use std::io::Stdout;
    use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt};

    use super::*;
    use crate::libc::{major, minor};
    use crate::temp_dir::TempDir;
    use crate::tests::{tests_path, TEST_LOCK};
    use crate::LOG_LEVEL_WARNING;

    fn getgid() -> u32 {
        unsafe { ::libc::getgid() }
    }

    fn getuid() -> u32 {
        unsafe { ::libc::getuid() }
    }

    #[test]
    fn test_absolute_parent_directory() {
        let base_dir = Path::new("/nonexistent/arthur");
        assert_eq!(
            absolute_parent_directory("usr/bin/true", base_dir).unwrap(),
            PathBuf::from("/nonexistent/arthur/usr/bin")
        );
        assert_eq!(
            absolute_parent_directory("/usr/bin/true", base_dir).unwrap(),
            PathBuf::from("/usr/bin")
        );
        assert_eq!(
            absolute_parent_directory(".", base_dir).unwrap(),
            PathBuf::from("/nonexistent")
        );
    }

    #[test]
    fn test_absolute_parent_directory_error() {
        let got = absolute_parent_directory(".", Path::new("/")).unwrap_err();
        assert_eq!(got.kind(), ErrorKind::InvalidData);
        assert_eq!(got.to_string(), "Path \"/.\" has no parent directory.");
    }

    #[test]
    fn test_extract_cpio_archive_compressed_make_directories_with_pattern() {
        let _lock = TEST_LOCK.lock().unwrap();
        let archive = File::open(tests_path("lz4.cpio")).unwrap();
        let tempdir = TempDir::new_and_set_current_dir().unwrap();
        let patterns = vec![Pattern::new("p?th/f*").unwrap()];
        let options = ExtractOptions::new(true, None, patterns, false, None);

        extract_cpio_archive(archive, None::<&mut Stdout>, &options, LOG_LEVEL_WARNING).unwrap();
        assert!(tempdir.path.join("path").is_dir());
        assert!(tempdir.path.join("path/file").exists());
        assert!(!tempdir.path.join("usr").exists());
    }

    #[test]
    fn test_extract_cpio_archive_compressed_parts_to_stdout() {
        let archive = File::open(tests_path("lzma.cpio")).unwrap();
        let mut output = Vec::new();
        let options = ExtractOptions::new(
            false,
            Some("-1".parse::<Ranges>().unwrap()),
            Vec::new(),
            false,
            None,
        );
        extract_cpio_archive(archive, Some(&mut output), &options, LOG_LEVEL_WARNING).unwrap();
        assert_eq!(String::from_utf8(output).unwrap(), "content\n");
    }

    #[test]
    fn test_extract_cpio_archive_compressed_to_stdout() {
        let archive = File::open(tests_path("bzip2.cpio")).unwrap();
        let mut output = Vec::new();
        let options = ExtractOptions::default();
        extract_cpio_archive(archive, Some(&mut output), &options, LOG_LEVEL_WARNING).unwrap();
        assert_eq!(
            String::from_utf8(output).unwrap(),
            "content\nThis is a fake busybox binary to simulate a POSIX shell\n"
        );
    }

    #[test]
    fn test_extract_cpio_archive_compressed_with_pattern() {
        let _lock = TEST_LOCK.lock().unwrap();
        let archive = File::open(tests_path("zstd.cpio")).unwrap();
        let tempdir = TempDir::new_and_set_current_dir().unwrap();
        let patterns = vec![Pattern::new("p?th").unwrap()];
        let options = ExtractOptions::new(false, None, patterns, false, None);
        extract_cpio_archive(archive, None::<&mut Stdout>, &options, LOG_LEVEL_WARNING).unwrap();
        assert!(tempdir.path.join("path").is_dir());
        assert!(!tempdir.path.join("path/file").exists());
    }

    #[test]
    fn test_extract_cpio_archive_compressed_with_pattern_to_stdout() {
        let archive = File::open(tests_path("gzip.cpio")).unwrap();
        let patterns: Vec<Pattern> = vec![Pattern::new("*/b?n/sh").unwrap()];
        let mut output = Vec::new();
        let options = ExtractOptions::new(false, None, patterns, false, None);
        extract_cpio_archive(archive, Some(&mut output), &options, LOG_LEVEL_WARNING).unwrap();
        assert_eq!(
            String::from_utf8(output).unwrap(),
            "This is a fake busybox binary to simulate a POSIX shell\n"
        );
    }

    #[test]
    fn test_extract_cpio_archive_uncompressed_with_pattern() {
        let _lock = TEST_LOCK.lock().unwrap();
        let archive = File::open(tests_path("single.cpio")).unwrap();
        let tempdir = TempDir::new_and_set_current_dir().unwrap();
        let patterns = vec![Pattern::new("path").unwrap()];
        let options = ExtractOptions::new(false, None, patterns, false, None);
        extract_cpio_archive(archive, None::<&mut Stdout>, &options, LOG_LEVEL_WARNING).unwrap();
        assert!(tempdir.path.join("path").is_dir());
        assert!(!tempdir.path.join("path/file").exists());
    }

    #[test]
    fn test_extract_cpio_archive_with_subdir() {
        let _lock = TEST_LOCK.lock().unwrap();
        let archive = File::open(tests_path("single.cpio")).unwrap();
        let tempdir = TempDir::new_and_set_current_dir().unwrap();
        let options = ExtractOptions::new(false, None, Vec::new(), false, Some("cpio".into()));
        extract_cpio_archive(archive, None::<&mut Stdout>, &options, LOG_LEVEL_WARNING).unwrap();
        let path = tempdir.path.join("cpio1/path/file");
        assert!(path.exists());
    }

    // Test detecting path traversal attacks like CVE-2015-1197
    #[test]
    fn test_read_cpio_and_extract_path_traversal() {
        let _lock = TEST_LOCK.lock().unwrap();
        let mut archive = File::open(tests_path("path-traversal.cpio")).unwrap();
        let tempdir = TempDir::new_and_set_current_dir().unwrap();
        let got = read_cpio_and_extract(
            &mut archive,
            &tempdir.path,
            &mut None::<Stdout>,
            &ExtractOptions::default(),
            LOG_LEVEL_WARNING,
        )
        .unwrap_err();
        assert_eq!(got.kind(), ErrorKind::InvalidData);
        assert_eq!(got.to_string(), format!(
            "The parent directory of \"tmp/trav.txt\" (resolved to \"/tmp\") is not within the directory {:#?}.",
            &tempdir.path
        ));
    }

    #[test]
    fn test_read_cpio_and_extract_path_traversal_to_stdout() {
        let mut archive = File::open(tests_path("path-traversal.cpio")).unwrap();
        let base_dir = std::env::current_dir().unwrap();
        let mut output = Vec::new();
        read_cpio_and_extract(
            &mut archive,
            &base_dir,
            &mut Some(&mut output),
            &ExtractOptions::default(),
            LOG_LEVEL_WARNING,
        )
        .unwrap();
        assert_eq!(String::from_utf8(output).unwrap(), "TEST Traversal\n");
    }

    #[test]
    fn test_write_character_device() {
        let _lock = TEST_LOCK.lock().unwrap();
        if getuid() != 0 {
            // This test needs to run as root.
            return;
        }
        let _tempdir = TempDir::new_and_set_current_dir().unwrap();
        let mut header = Header::new(1, 0o20_644, 0, 0, 0, 1740402179, 0, 0, 0, "./null");
        header.rmajor = 1;
        header.rminor = 3;
        write_character_device(&header, true, LOG_LEVEL_WARNING).unwrap();

        let attr = std::fs::metadata("null").unwrap();
        assert_eq!(attr.len(), header.filesize.into());
        assert!(attr.file_type().is_char_device());
        assert_eq!(attr.modified().unwrap(), from_mtime(header.mtime));
        assert_eq!(attr.permissions(), PermissionsExt::from_mode(header.mode));
        assert_eq!(attr.uid(), header.uid);
        assert_eq!(attr.gid(), header.gid);
        assert_eq!(major(attr.rdev()), header.rmajor);
        assert_eq!(minor(attr.rdev()), header.rminor);
        std::fs::remove_file("null").unwrap();
    }

    #[test]
    fn test_write_directory_with_setuid() {
        let _lock = TEST_LOCK.lock().unwrap();
        let _tempdir = TempDir::new_and_set_current_dir().unwrap();
        let mut mtimes = BTreeMap::new();
        let header = Header::new(
            1,
            0o43_777,
            getuid(),
            getgid(),
            0,
            1720081471,
            0,
            0,
            0,
            "./directory_with_setuid",
        );
        write_directory(&header, true, LOG_LEVEL_WARNING, &mut mtimes).unwrap();

        let attr = std::fs::metadata("directory_with_setuid").unwrap();
        assert!(attr.is_dir());
        assert_eq!(attr.permissions(), PermissionsExt::from_mode(header.mode));
        assert_eq!(attr.uid(), header.uid);
        assert_eq!(attr.gid(), header.gid);
        std::fs::remove_dir("directory_with_setuid").unwrap();

        let mut expected_mtimes: BTreeMap<String, i64> = BTreeMap::new();
        expected_mtimes.insert("./directory_with_setuid".into(), header.mtime.into());
        assert_eq!(mtimes, expected_mtimes);
    }

    #[test]
    fn test_write_file_with_setuid() {
        let _lock = TEST_LOCK.lock().unwrap();
        let _tempdir = TempDir::new_and_set_current_dir().unwrap();
        let mut seen_files = SeenFiles::new();
        let header = Header::new(
            1,
            0o104_755,
            getuid(),
            getgid(),
            0,
            1720081471,
            9,
            0,
            0,
            "./file_with_setuid",
        );
        let cpio = b"!/bin/sh\n\0\0\0";
        write_file(
            &mut cpio.as_ref(),
            &header,
            true,
            &mut seen_files,
            LOG_LEVEL_WARNING,
        )
        .unwrap();

        let attr = std::fs::metadata("file_with_setuid").unwrap();
        assert_eq!(attr.len(), header.filesize.into());
        assert!(attr.is_file());
        assert_eq!(attr.modified().unwrap(), from_mtime(header.mtime));
        assert_eq!(attr.permissions(), PermissionsExt::from_mode(header.mode));
        assert_eq!(attr.uid(), header.uid);
        assert_eq!(attr.gid(), header.gid);
        std::fs::remove_file("file_with_setuid").unwrap();
    }

    #[test]
    fn test_write_symbolic_link() {
        let _lock = TEST_LOCK.lock().unwrap();
        let _tempdir = TempDir::new_and_set_current_dir().unwrap();
        let header = Header::new(
            1,
            0o120_777,
            getuid(),
            getgid(),
            0,
            1721427072,
            12,
            0,
            0,
            "./dead_symlink",
        );
        let cpio = b"/nonexistent";
        write_symbolic_link(&mut cpio.as_ref(), &header, true, LOG_LEVEL_WARNING).unwrap();

        let attr = std::fs::symlink_metadata("dead_symlink").unwrap();
        assert_eq!(attr.len(), header.filesize.into());
        assert!(attr.is_symlink());
        assert_eq!(attr.modified().unwrap(), from_mtime(header.mtime));
        assert_eq!(attr.permissions(), PermissionsExt::from_mode(header.mode));
        assert_eq!(attr.uid(), header.uid);
        assert_eq!(attr.gid(), header.gid);
        std::fs::remove_file("dead_symlink").unwrap();
    }
}
