use std::collections::{BTreeMap, HashMap};
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::logger::Logger;
use crate::ranges::Ranges;
use crate::seek_forward::SeekForward;
use crate::{filename_matches, seek_to_cpio_end, TRAILER_FILENAME};
pub(crate) type SeenFiles = HashMap<u128, String>;
pub enum ExtractTarget<'a, W: Write> {
Directory(PathBuf),
WritableStream(&'a mut W),
}
#[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<W: Write>(&self, logger: &mut Logger<W>) -> Result<()> {
for (path, mtime) in self.mtimes.iter().rev() {
debug!(logger, "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()),
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, LW: Write>(
mut archive: File,
mut target: ExtractTarget<W>,
options: &ExtractOptions,
logger: &mut Logger<LW>,
) -> Result<()> {
let mut count = 0;
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 part_target = match target {
ExtractTarget::Directory(ref base_dir) => {
let mut dir = base_dir.clone();
if let Some(ref s) = options.subdir {
dir.push(format!("{s}{count}"));
create_dir_ignore_existing(&dir)?;
}
ExtractTarget::Directory(dir)
}
ExtractTarget::WritableStream(ref mut w) => ExtractTarget::WritableStream(w),
};
if compression.is_uncompressed() {
read_cpio_and_extract(&mut archive, part_target, options, logger)?;
} else {
let mut decompressed = compression.decompress(archive)?;
read_cpio_and_extract(&mut decompressed, part_target, options, logger)?;
break;
}
}
Ok(())
}
fn from_mtime(mtime: u32) -> SystemTime {
std::time::UNIX_EPOCH + std::time::Duration::from_secs(mtime.into())
}
fn extract_to_disk<R: Read + SeekForward, W: Write>(
archive: &mut R,
header: &Header,
extractor: &mut Extractor,
options: &ExtractOptions,
logger: &mut Logger<W>,
) -> Result<()> {
match header.mode & MODE_FILETYPE_MASK {
FILETYPE_BLOCK_DEVICE | FILETYPE_CHARACTER_DEVICE | FILETYPE_FIFO | FILETYPE_SOCKET => {
write_special_file(header, options.preserve_permissions, logger)?
}
FILETYPE_DIRECTORY => write_directory(
header,
options.preserve_permissions,
logger,
&mut extractor.mtimes,
)?,
FILETYPE_REGULAR_FILE => write_file(
archive,
header,
options.preserve_permissions,
&mut extractor.seen_files,
logger,
)?,
FILETYPE_SYMLINK => {
write_symbolic_link(archive, header, options.preserve_permissions, logger)?
}
_ => {
return Err(Error::new(
ErrorKind::InvalidData,
format!(
"Invalid/unknown file type 0o{:o} for '{}'",
header.mode, header.filename
),
))
}
}
Ok(())
}
fn extract_to_writable<R, W>(archive: &mut R, header: &Header, out: &mut W) -> Result<()>
where
R: Read + SeekForward,
W: Write,
{
if header.filesize == 0 {
return Ok(());
}
if matches!(header.mode & MODE_FILETYPE_MASK, FILETYPE_REGULAR_FILE) {
write_file_content(archive, out, header)?;
} else {
header.skip_file_content(archive)?;
}
Ok(())
}
fn read_cpio_and_extract<R: Read + SeekForward, W: Write, LW: Write>(
archive: &mut R,
mut target: ExtractTarget<W>,
options: &ExtractOptions,
logger: &mut Logger<LW>,
) -> Result<()> {
let mut extractor = Extractor::new();
let mut previous_checked_dir = PathBuf::new();
if let ExtractTarget::Directory(ref base_dir) = target {
std::env::set_current_dir(base_dir)?;
}
loop {
let header = match Header::read(archive) {
Ok(header) => {
if header.filename == TRAILER_FILENAME {
break;
} else {
header
}
}
Err(e) => return Err(e),
};
debug!(logger, "{header:?}")?;
if !options.patterns.is_empty() && !filename_matches(&header.filename, &options.patterns) {
header.skip_file_content(archive)?;
continue;
}
info!(logger, "{}", header.filename)?;
match target {
ExtractTarget::Directory(ref base_dir) => {
if !header.is_root_directory() {
let absdir = absolute_parent_directory(&header.filename, base_dir)?;
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)?;
}
}
extract_to_disk(archive, &header, &mut extractor, options, logger)?;
}
ExtractTarget::WritableStream(ref mut out) => {
extract_to_writable(archive, &header, out)?
}
}
}
extractor.set_modified_times(logger)?;
Ok(())
}
fn write_special_file<W: Write>(
header: &Header,
preserve_permissions: bool,
logger: &mut Logger<W>,
) -> Result<()> {
if header.filesize != 0 {
return Err(Error::new(
ErrorKind::InvalidData,
format!(
"Invalid size for {} '{}': {} bytes instead of 0.",
header.file_type_name(),
header.filename,
header.filesize
),
));
};
debug!(
logger,
"Creating {} '{}' with mode {:o}",
header.file_type_name(),
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<W: Write>(
header: &Header,
preserve_permissions: bool,
logger: &mut Logger<W>,
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
),
));
};
debug!(
logger,
"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, W: Write>(
archive: &mut R,
header: &Header,
preserve_permissions: bool,
seen_files: &mut SeenFiles,
logger: &mut Logger<W>,
) -> Result<()> {
let mut file;
if let Some(target) = header.try_get_hard_link_target(seen_files) {
debug!(
logger,
"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 {
debug!(
logger,
"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);
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, W: Write>(
archive: &mut R,
header: &Header,
preserve_permissions: bool,
logger: &mut Logger<W>,
) -> Result<()> {
let target = header.read_symlink_target(archive)?;
debug!(
logger,
"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::logger::Level;
use crate::temp_dir::TempDir;
use crate::tests::{tests_path, TEST_LOCK};
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);
let mut logger = Logger::new_vec(Level::Info);
extract_cpio_archive(
archive,
ExtractTarget::Directory::<Stdout>(std::env::current_dir().unwrap()),
&options,
&mut logger,
)
.unwrap();
assert!(tempdir.path.join("path").is_dir());
assert!(tempdir.path.join("path/file").exists());
assert!(!tempdir.path.join("usr").exists());
assert_eq!(logger.get_logs(), "path/file\n");
}
#[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,
);
let mut logger = Logger::new_vec(Level::Info);
extract_cpio_archive(
archive,
ExtractTarget::WritableStream(&mut output),
&options,
&mut logger,
)
.unwrap();
assert_eq!(String::from_utf8(output).unwrap(), "content\n");
assert_eq!(logger.get_logs(), ".\npath\npath/file\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();
let mut logger = Logger::new_vec(Level::Warning);
extract_cpio_archive(
archive,
ExtractTarget::WritableStream(&mut output),
&options,
&mut logger,
)
.unwrap();
assert_eq!(
String::from_utf8(output).unwrap(),
"content\nThis is a fake busybox binary to simulate a POSIX shell\n"
);
assert_eq!(logger.get_logs(), "");
}
#[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);
let mut logger = Logger::new_vec(Level::Debug);
extract_cpio_archive(
archive,
ExtractTarget::Directory::<Stdout>(std::env::current_dir().unwrap()),
&options,
&mut logger,
)
.unwrap();
assert!(tempdir.path.join("path").is_dir());
assert!(!tempdir.path.join("path/file").exists());
assert_eq!(
logger.get_logs(),
"Header { ino: 0, mode: 16893, uid: 0, gid: 0, nlink: 2, mtime: 1713104326, filesize: 0, \
major: 0, minor: 0, rmajor: 0, rminor: 0, filename: \".\" }\n\
Header { ino: 1, mode: 16893, uid: 0, gid: 0, nlink: 2, mtime: 1713104326, filesize: 0, \
major: 0, minor: 0, rmajor: 0, rminor: 0, filename: \"path\" }\n\
path\n\
Creating directory 'path' with mode 775\n\
Header { ino: 2, mode: 33204, uid: 0, gid: 0, nlink: 1, mtime: 1713104326, filesize: 8, \
major: 0, minor: 0, rmajor: 0, rminor: 0, filename: \"path/file\" }\n\
set mtime 1713104326 for 'path'\n\
Header { ino: 0, mode: 16893, uid: 0, gid: 0, nlink: 2, mtime: 1713104326, filesize: 0, \
major: 0, minor: 0, rmajor: 0, rminor: 0, filename: \".\" }\n\
Header { ino: 1, mode: 16893, uid: 0, gid: 0, nlink: 2, mtime: 1713104326, filesize: 0, \
major: 0, minor: 0, rmajor: 0, rminor: 0, filename: \"usr\" }\n\
Header { ino: 2, mode: 16893, uid: 0, gid: 0, nlink: 2, mtime: 1713104326, filesize: 0, \
major: 0, minor: 0, rmajor: 0, rminor: 0, filename: \"usr/bin\" }\n\
Header { ino: 3, mode: 41471, uid: 0, gid: 0, nlink: 1, mtime: 1713104326, filesize: 2, \
major: 0, minor: 0, rmajor: 0, rminor: 0, filename: \"usr/bin/ash\" }\n\
Header { ino: 4, mode: 33204, uid: 0, gid: 0, nlink: 1, mtime: 1713104326, filesize: 56, \
major: 0, minor: 0, rmajor: 0, rminor: 0, filename: \"usr/bin/sh\" }\n"
);
}
#[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);
let mut logger = Logger::new_vec(Level::Info);
extract_cpio_archive(
archive,
ExtractTarget::WritableStream(&mut output),
&options,
&mut logger,
)
.unwrap();
assert_eq!(
String::from_utf8(output).unwrap(),
"This is a fake busybox binary to simulate a POSIX shell\n"
);
assert_eq!(logger.get_logs(), "usr/bin/sh\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);
let mut logger = Logger::new_vec(Level::Info);
extract_cpio_archive(
archive,
ExtractTarget::Directory::<Stdout>(std::env::current_dir().unwrap()),
&options,
&mut logger,
)
.unwrap();
assert!(tempdir.path.join("path").is_dir());
assert!(!tempdir.path.join("path/file").exists());
assert_eq!(logger.get_logs(), "path\n");
}
#[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()));
let mut logger = Logger::new_vec(Level::Info);
extract_cpio_archive(
archive,
ExtractTarget::Directory::<Stdout>(std::env::current_dir().unwrap()),
&options,
&mut logger,
)
.unwrap();
let path = tempdir.path.join("cpio1/path/file");
assert!(path.exists());
assert_eq!(logger.get_logs(), ".\npath\npath/file\n");
}
#[test]
fn test_read_cpio_and_extract_fifo() {
let _lock = TEST_LOCK.lock().unwrap();
let tempdir = TempDir::new_and_set_current_dir().unwrap();
let path = tempdir.path.join("fifo.cpio");
let uid = getuid();
let gid = getgid();
let header = Header::new(1, 0o010_600, uid, gid, 1, 1746789067, 0, 0, 0, "initctl");
let mut archive = File::create(&path).unwrap();
header.write(&mut archive).unwrap();
Header::trailer().write(&mut archive).unwrap();
let mut archive = File::open(&path).unwrap();
let mut logger = Logger::new_vec(Level::Info);
read_cpio_and_extract(
&mut archive,
ExtractTarget::Directory::<Stdout>(tempdir.path.clone()),
&ExtractOptions::default(),
&mut logger,
)
.unwrap();
let attr = std::fs::metadata("initctl").unwrap();
assert_eq!(attr.len(), header.filesize.into());
assert!(attr.file_type().is_fifo());
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);
assert_eq!(logger.get_logs(), "initctl\n");
}
#[test]
fn test_read_cpio_and_extract_invalid_file_type() {
let _lock = TEST_LOCK.lock().unwrap();
let tempdir = TempDir::new().unwrap();
let cwd = std::env::current_dir().unwrap();
let path = tempdir.path.join("invalid.cpio");
let mut archive = File::create(&path).unwrap();
archive
.write_all(
b"070701000000010003FFA200000007000000070000000168AEBD2C\
00000000000000000000000000000000000000000000000800000000\
invalid\0\0\0\
070701000000000000000000000000000000000000000100000000\
00000000000000000000000000000000000000000000000B00000000\
TRAILER!!!\0\0\0\0",
)
.unwrap();
let mut archive = File::open(&path).unwrap();
let mut logger = Logger::new_vec(Level::Warning);
let got = read_cpio_and_extract(
&mut archive,
ExtractTarget::Directory::<Stdout>(tempdir.path.clone()),
&ExtractOptions::default(),
&mut logger,
)
.unwrap_err();
std::env::set_current_dir(&cwd).unwrap();
assert_eq!(got.kind(), ErrorKind::InvalidData);
assert_eq!(
got.to_string(),
"Invalid/unknown file type 0o777642 for 'invalid'",
);
assert_eq!(logger.get_logs(), "");
assert!(!Path::new("invalid").exists());
}
#[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 mut logger = Logger::new_vec(Level::Info);
let got = read_cpio_and_extract(
&mut archive,
ExtractTarget::Directory::<Stdout>(tempdir.path.clone()),
&ExtractOptions::default(),
&mut logger,
)
.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
));
assert_eq!(logger.get_logs(), ".\ntmp\ntmp/trav.txt\n");
}
#[test]
fn test_read_cpio_and_extract_path_traversal_to_stdout() {
let mut archive = File::open(tests_path("path-traversal.cpio")).unwrap();
let mut output = Vec::new();
let mut logger = Logger::new_vec(Level::Info);
read_cpio_and_extract(
&mut archive,
ExtractTarget::WritableStream(&mut output),
&ExtractOptions::default(),
&mut logger,
)
.unwrap();
assert_eq!(String::from_utf8(output).unwrap(), "TEST Traversal\n");
assert_eq!(logger.get_logs(), ".\ntmp\ntmp/trav.txt\n");
}
#[test]
fn test_write_special_file_block_device() {
if getuid() != 0 {
return;
}
let _lock = TEST_LOCK.lock().unwrap();
let _tempdir = TempDir::new_and_set_current_dir().unwrap();
let header = Header::new(1, 0o60_660, 0, 6, 1, 1751300235, 0, 7, 99, "loop99");
let mut logger = Logger::new_vec(Level::Debug);
write_special_file(&header, true, &mut logger).unwrap();
let attr = std::fs::metadata("loop99").unwrap();
assert_eq!(attr.len(), header.filesize.into());
assert!(attr.file_type().is_block_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);
assert_eq!(
logger.get_logs(),
"Creating block device 'loop99' with mode 660\n"
);
}
#[test]
fn test_write_special_file_character_device() {
if getuid() != 0 {
return;
}
let _lock = TEST_LOCK.lock().unwrap();
let _tempdir = TempDir::new_and_set_current_dir().unwrap();
let header = Header::new(1, 0o20_644, 0, 0, 0, 1740402179, 0, 1, 3, "./null");
let mut logger = Logger::new_vec(Level::Debug);
write_special_file(&header, true, &mut logger).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);
assert_eq!(
logger.get_logs(),
"Creating character device './null' with mode 644\n"
);
std::fs::remove_file("null").unwrap();
}
#[test]
fn test_write_special_file_fifo() {
let _lock = TEST_LOCK.lock().unwrap();
let _tempdir = TempDir::new_and_set_current_dir().unwrap();
let uid = getuid();
let gid = getgid();
let header = Header::new(1, 0o010_600, uid, gid, 1, 1746789067, 0, 0, 0, "initctl");
let mut logger = Logger::new_vec(Level::Debug);
write_special_file(&header, false, &mut logger).unwrap();
let attr = std::fs::metadata("initctl").unwrap();
assert_eq!(attr.len(), header.filesize.into());
assert!(attr.file_type().is_fifo());
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);
assert_eq!(logger.get_logs(), "Creating fifo 'initctl' with mode 600\n");
}
#[test]
fn test_write_special_file_socket() {
let _lock = TEST_LOCK.lock().unwrap();
let _tempdir = TempDir::new_and_set_current_dir().unwrap();
let uid = getuid();
let gid = getgid();
let header = Header::new(1, 0o140_777, uid, gid, 1, 1746789058, 0, 0, 0, "notify");
let mut logger = Logger::new_vec(Level::Debug);
write_special_file(&header, true, &mut logger).unwrap();
let attr = std::fs::metadata("notify").unwrap();
assert_eq!(attr.len(), header.filesize.into());
assert!(attr.file_type().is_socket());
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);
assert_eq!(
logger.get_logs(),
"Creating socket 'notify' with mode 777\n"
);
}
#[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",
);
let mut logger = Logger::new_vec(Level::Debug);
write_directory(&header, true, &mut logger, &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);
assert_eq!(
logger.get_logs(),
format!(
"Creating directory './directory_with_setuid' with mode 3777 and owner {}:{}\n",
getuid(),
getgid(),
),
);
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";
let mut logger = Logger::new_vec(Level::Debug);
write_file(
&mut cpio.as_ref(),
&header,
true,
&mut seen_files,
&mut logger,
)
.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);
assert_eq!(
logger.get_logs(),
format!(
"Creating file './file_with_setuid' with permission 4755 and owner {}:{} and 9 bytes\n",
getuid(),
getgid(),
),
);
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",
);
assert_eq!(header.file_type_name(), "symlink");
let cpio = b"/nonexistent";
let mut logger = Logger::new_vec(Level::Debug);
write_symbolic_link(&mut cpio.as_ref(), &header, true, &mut logger).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);
assert_eq!(
logger.get_logs(),
"Creating symlink './dead_symlink' -> '/nonexistent' with mode 777\n"
);
std::fs::remove_file("dead_symlink").unwrap();
}
}