use std::collections::HashMap;
use std::fs::{symlink_metadata, Metadata};
use std::io::{BufRead, BufWriter, Error, ErrorKind, Result, Write};
use std::num::NonZeroU32;
use std::os::unix::fs::{MetadataExt, PermissionsExt};
use crate::compression::Compression;
use crate::extended_error::ExtendedError;
use crate::filetype::*;
use crate::header::{calculate_size, padding_needed_for, Header, TRAILER_SIZE};
use crate::libc::{major, minor};
use crate::logger::Logger;
use crate::CPIO_ALIGNMENT;
#[derive(Debug, PartialEq)]
struct Hardlink {
location: String,
filesize: u32,
references: u32,
}
fn get_hardlink_key(stat: &Metadata) -> u128 {
(u128::from(stat.ino()) << 64) | u128::from(stat.dev())
}
impl Hardlink {
fn new<S: Into<String>>(location: S, filesize: u32) -> Self {
Self {
location: location.into(),
filesize,
references: 1,
}
}
#[cfg(test)]
fn with_references<S: Into<String>>(location: S, filesize: u32, references: u32) -> Self {
Self {
location: location.into(),
filesize,
references,
}
}
}
#[derive(Debug, PartialEq)]
enum Filetype {
Hardlink { key: u128, index: u32 },
EmptyFile,
Directory,
BlockDevice { major: u32, minor: u32 },
CharacterDevice { major: u32, minor: u32 },
Fifo,
Socket,
Symlink { target: String },
}
#[derive(Debug, PartialEq)]
struct File {
filetype: Filetype,
name: String,
mode: u16,
uid: u32,
gid: u32,
mtime: u32,
}
#[derive(Debug, PartialEq)]
pub(crate) struct Archive {
compression: Compression,
files: Vec<File>,
hardlinks: HashMap<u128, Hardlink>,
}
#[derive(Debug, PartialEq)]
pub(crate) struct Manifest {
archives: Vec<Archive>,
umask: u32,
}
struct LazyMetadata<'a> {
location: Option<&'a str>,
metadata: Option<Metadata>,
}
impl<'a> LazyMetadata<'a> {
fn new(location: Option<&'a str>) -> Self {
LazyMetadata {
location,
metadata: None,
}
}
fn get_metadata(&mut self, name: &str) -> Result<&Metadata> {
if self.metadata.is_none() {
let stat = match self.location {
None => {
return Err(Error::new(
ErrorKind::InvalidInput,
format!("Neither {name} nor location specified."),
))
}
Some(path) => symlink_metadata(path).map_err(|e| e.add_prefix(path))?,
};
self.metadata = Some(stat);
}
Ok(self.metadata.as_ref().unwrap())
}
fn parse_u32(
&mut self,
entry: Option<&str>,
name: &str,
f: impl Fn(&Metadata) -> Result<u32>,
) -> Result<u32> {
match entry {
Some("-") | Some("") | None => Ok(f(self.get_metadata(name)?)?),
Some(x) => match x.parse() {
Ok(y) => Ok(y),
Err(e) => Err(Error::new(
ErrorKind::InvalidInput,
format!("invalid {name}: {e}"),
)),
},
}
}
fn parse_octal(
&mut self,
entry: Option<&str>,
name: &str,
f: impl Fn(&Metadata) -> u16,
) -> Result<u16> {
match entry {
Some("-") | Some("") | None => Ok(f(self.get_metadata(name)?)),
Some(x) => match u16::from_str_radix(x, 8) {
Ok(y) => Ok(y),
Err(e) => Err(Error::new(
ErrorKind::InvalidInput,
format!("invalid {name}: {e}"),
)),
},
}
}
fn parse_filetype(&mut self, entry: Option<&str>, name: &str) -> Result<u32> {
let filetype = match entry {
Some("file") => FILETYPE_REGULAR_FILE,
Some("dir") => FILETYPE_DIRECTORY,
Some("block") => FILETYPE_BLOCK_DEVICE,
Some("char") => FILETYPE_CHARACTER_DEVICE,
Some("link") => FILETYPE_SYMLINK,
Some("fifo") => FILETYPE_FIFO,
Some("sock") => FILETYPE_SOCKET,
Some("-") | Some("") | None => self.get_metadata(name)?.mode() & MODE_FILETYPE_MASK,
Some(x) => {
return Err(Error::new(
ErrorKind::InvalidInput,
format!("Unknown filetype '{x}'"),
))
}
};
Ok(filetype)
}
}
fn pathbuf_to_string(path: std::path::PathBuf) -> Result<String> {
path.into_os_string().into_string().map_err(|e| {
Error::new(
ErrorKind::InvalidInput,
format!("failed to convert path {e:#?} to string"),
)
})
}
fn parse_symlink(entry: Option<&str>, location: Option<&str>) -> Result<String> {
match entry {
Some("-") | Some("") | None => match location {
None => Err(Error::new(
ErrorKind::InvalidInput,
"Neither symlink nor location specified.",
)),
Some(path) => Ok(pathbuf_to_string(std::fs::read_link(path)?)?),
},
Some(x) => Ok(x.into()),
}
}
fn replace_empty(entry: Option<&str>) -> Option<&str> {
match entry {
Some("-") | Some("") | None => None,
Some(x) => Some(x),
}
}
fn sanitize_path(path: &str) -> &str {
match path.strip_prefix("./") {
Some(p) => {
if p.is_empty() {
"."
} else {
p
}
}
None => match path.strip_prefix("/") {
Some(p) => {
if p.is_empty() {
"."
} else {
p
}
}
None => path,
},
}
}
fn get_permission(mode: u32) -> u16 {
(mode & MODE_PERMISSION_MASK) as u16
}
fn get_rmajor(metadata: &Metadata) -> Result<u32> {
Ok(major(metadata.rdev()))
}
fn get_rminor(metadata: &Metadata) -> Result<u32> {
Ok(minor(metadata.rdev()))
}
fn get_mtime(metadata: &Metadata) -> Result<u32> {
metadata.mtime().try_into().map_err(|_| {
Error::new(
ErrorKind::InvalidData,
format!(
"mtime {} outside of supported range from 0 to 4,294,967,295.",
metadata.mtime()
),
)
})
}
fn determine_umask(mode: u32) -> u32 {
let other_umask = !mode & 0o7;
(other_umask << 3) | other_umask
}
impl File {
fn new<S: Into<String>>(
filetype: Filetype,
name: S,
mode: u16,
uid: u32,
gid: u32,
mtime: u32,
) -> Self {
Self {
filetype,
name: name.into(),
mode,
uid,
gid,
mtime,
}
}
fn from_line<S: AsRef<str>>(
line: S,
hardlinks: &mut HashMap<u128, Hardlink>,
) -> Result<(Self, u32)> {
let mut umask = 0;
let mut iter = line.as_ref().split('\t');
let location = replace_empty(iter.next());
let name = match replace_empty(iter.next()) {
Some(name) => name,
None => match location {
Some(path) => sanitize_path(path),
None => {
return Err(Error::new(
ErrorKind::InvalidInput,
"Neither location nor name were specified.",
))
}
},
};
let mut lazy_metadata = LazyMetadata::new(location);
let filetype_value = lazy_metadata.parse_filetype(iter.next(), "filetype")?;
let mode = lazy_metadata.parse_octal(iter.next(), "mode", |m| get_permission(m.mode()))?;
let uid = lazy_metadata.parse_u32(iter.next(), "uid", |m| Ok(m.uid()))?;
let gid = lazy_metadata.parse_u32(iter.next(), "gid", |m| Ok(m.gid()))?;
let mtime = lazy_metadata.parse_u32(iter.next(), "mtime", get_mtime)?;
let filetype = match filetype_value {
FILETYPE_REGULAR_FILE => {
let filesize = lazy_metadata.parse_u32(iter.next(), "filesize", |m| {
m.size().try_into().map_err(|_| {
Error::new(
ErrorKind::InvalidData,
format!(
"File '{}' exceeds file size limit of 4 GiB.",
location.unwrap()
),
)
})
})?;
if filesize == 0 {
Filetype::EmptyFile
} else {
let stat = lazy_metadata.get_metadata("filetype")?;
umask = determine_umask(stat.mode());
let key = get_hardlink_key(stat);
let index = match hardlinks.get_mut(&key) {
Some(hardlink) => {
hardlink.references += 1;
hardlink.references
}
None => {
hardlinks.insert(key, Hardlink::new(location.unwrap(), filesize));
1
}
};
Filetype::Hardlink { key, index }
}
}
FILETYPE_DIRECTORY => Filetype::Directory,
FILETYPE_BLOCK_DEVICE => Filetype::BlockDevice {
major: lazy_metadata.parse_u32(iter.next(), "major", get_rmajor)?,
minor: lazy_metadata.parse_u32(iter.next(), "minor", get_rminor)?,
},
FILETYPE_CHARACTER_DEVICE => Filetype::CharacterDevice {
major: lazy_metadata.parse_u32(iter.next(), "major", get_rmajor)?,
minor: lazy_metadata.parse_u32(iter.next(), "minor", get_rminor)?,
},
FILETYPE_SYMLINK => Filetype::Symlink {
target: parse_symlink(iter.next(), location)?,
},
FILETYPE_FIFO => Filetype::Fifo,
FILETYPE_SOCKET => Filetype::Socket,
unknown => {
return Err(Error::new(
ErrorKind::InvalidInput,
format!("Unknown filetype '{unknown}'"),
))
}
};
Ok((Self::new(filetype, name, mode, uid, gid, mtime), umask))
}
fn generate_header(
&self,
next_free_ino: u32,
hardlinks: &HashMap<u128, Hardlink>,
hardlinks2ino: &mut HashMap<u128, u32>,
) -> (Header, u32) {
let mut nlink = 1;
let mut filesize = 0;
let mut rmajor = 0;
let mut rminor = 0;
let mut ino = next_free_ino;
let mut next_ino = next_free_ino + 1;
let filetype;
match &self.filetype {
Filetype::EmptyFile => filetype = FILETYPE_REGULAR_FILE,
Filetype::Hardlink { key, index } => {
filetype = FILETYPE_REGULAR_FILE;
if let Some(existing_ino) = hardlinks2ino.get(key) {
ino = *existing_ino;
next_ino = next_free_ino;
} else {
hardlinks2ino.insert(*key, ino);
}
let hardlink = hardlinks.get(key).unwrap();
nlink = hardlink.references;
filesize = if *index == nlink {
hardlink.filesize
} else {
0
};
}
Filetype::Directory => {
filetype = FILETYPE_DIRECTORY;
nlink = 2;
}
Filetype::BlockDevice { major, minor } => {
filetype = FILETYPE_BLOCK_DEVICE;
rmajor = *major;
rminor = *minor;
}
Filetype::CharacterDevice { major, minor } => {
filetype = FILETYPE_CHARACTER_DEVICE;
rmajor = *major;
rminor = *minor;
}
Filetype::Symlink { target } => {
filetype = FILETYPE_SYMLINK;
filesize = target.len().try_into().unwrap();
}
Filetype::Fifo => filetype = FILETYPE_FIFO,
Filetype::Socket => filetype = FILETYPE_SOCKET,
}
(
Header::new(
ino,
filetype | u32::from(self.mode),
self.uid,
self.gid,
nlink,
self.mtime,
filesize,
rmajor,
rminor,
self.name.clone(),
),
next_ino,
)
}
}
impl Archive {
fn new() -> Self {
Self {
compression: Compression::Uncompressed,
files: Vec::new(),
hardlinks: HashMap::new(),
}
}
#[cfg(test)]
fn with_files(files: Vec<File>) -> Self {
Self {
compression: Compression::Uncompressed,
files,
hardlinks: HashMap::new(),
}
}
#[cfg(test)]
fn with_files_and_hardlinks(files: Vec<File>, hardlinks: HashMap<u128, Hardlink>) -> Self {
Self {
compression: Compression::Uncompressed,
files,
hardlinks,
}
}
#[cfg(test)]
fn with_files_compressed(files: Vec<File>, compression: Compression) -> Self {
Self {
compression,
files,
hardlinks: HashMap::new(),
}
}
fn add_line<S: AsRef<str>>(&mut self, line: S) -> Result<u32> {
let (file, umask) = File::from_line(line, &mut self.hardlinks)?;
self.files.push(file);
Ok(umask)
}
fn is_empty(&self) -> bool {
self.files.is_empty()
}
fn set_compression(&mut self, compression: Compression) {
self.compression = compression;
}
fn size(&self) -> u64 {
let mut size = 0;
for file in &self.files {
let filesize = match &file.filetype {
Filetype::Hardlink { key: _, index: _ } => 0,
Filetype::Symlink { target } => u32::try_from(target.len()).unwrap(),
Filetype::EmptyFile
| Filetype::Directory
| Filetype::BlockDevice { major: _, minor: _ }
| Filetype::CharacterDevice { major: _, minor: _ }
| Filetype::Fifo
| Filetype::Socket => 0,
};
size += calculate_size(&file.name, filesize.into());
}
for hardlink in self.hardlinks.values() {
debug_assert!(hardlink.references > 0);
let filesize = hardlink.filesize.into();
size += filesize + padding_needed_for(filesize, CPIO_ALIGNMENT);
}
size + TRAILER_SIZE
}
fn write<W: Write, LW: Write>(
&self,
output_file: &mut W,
alignment: Option<NonZeroU32>,
source_date_epoch: Option<u32>,
mut size: u64,
logger: &mut Logger<LW>,
) -> Result<u64> {
let mut next_ino = 0;
let mut hardlink_ino = HashMap::new();
let mut header;
for file in &self.files {
info!(logger, "{}", file.name)?;
(header, next_ino) = file.generate_header(next_ino, &self.hardlinks, &mut hardlink_ino);
if let Some(epoch) = source_date_epoch {
if header.mtime > epoch {
header.mtime = epoch;
}
}
debug!(logger, "{header:?}")?;
size += header.write_with_alignment(output_file, alignment, size)?;
match &file.filetype {
Filetype::Hardlink { key, index: _ } => {
if header.filesize > 0 {
let hardlink = self.hardlinks.get(key).unwrap();
size += copy_file(&hardlink.location, hardlink.filesize, output_file)?;
size += header.write_file_data_padding(output_file)?;
}
}
Filetype::Symlink { target } => {
output_file.write_all(target.as_bytes())?;
size += u64::try_from(target.len()).unwrap();
size += header.write_file_data_padding(output_file)?;
}
Filetype::EmptyFile
| Filetype::Directory
| Filetype::BlockDevice { major: _, minor: _ }
| Filetype::CharacterDevice { major: _, minor: _ }
| Filetype::Fifo
| Filetype::Socket => {}
}
}
size += Header::trailer().write(output_file)?;
Ok(size)
}
}
impl Manifest {
fn new(archives: Vec<Archive>, umask: u32) -> Self {
Self { archives, umask }
}
pub(crate) fn from_input<R: BufRead, W: Write>(
reader: R,
logger: &mut Logger<W>,
) -> Result<Self> {
let mut archives = vec![Archive::new()];
let mut current_archive = archives.last_mut().unwrap();
let mut umask = 0;
for (line_number, line) in reader.lines().enumerate() {
let line = line.map_err(|e| e.add_line(line_number + 1))?;
let line = line.trim();
if line.starts_with("#") || line.is_empty() {
if line.starts_with("#cpio") {
debug!(logger, "Parsing line {}: {line}", line_number + 1)?;
if !current_archive.is_empty() {
archives.push(Archive::new());
current_archive = archives.last_mut().unwrap();
};
match line.strip_prefix("#cpio:") {
Some(compression_str) => {
let compression = Compression::from_command_line(compression_str)
.map_err(|e| e.add_line(line_number + 1))?;
current_archive.set_compression(compression);
}
None => {
if line != "#cpio" {
return Err(Error::new(
ErrorKind::InvalidInput,
format!(
"line {}: Unknown cpio archive directive: {line}",
line_number + 1,
),
));
}
}
}
}
continue;
}
debug!(logger, "Parsing line {}: {line}", line_number + 1)?;
let file_mask = current_archive
.add_line(line)
.map_err(|e| e.add_line(line_number + 1))?;
umask |= file_mask;
}
Ok(Self::new(archives, umask))
}
fn apply_umask(&self, file: &std::fs::File) -> Result<()> {
let mode = file.metadata()?.mode();
let new_mode = mode & !self.umask;
if mode != new_mode {
file.set_permissions(PermissionsExt::from_mode(new_mode))?;
}
Ok(())
}
pub(crate) fn write_archive<W: Write>(
self,
mut file: Option<std::fs::File>,
alignment: Option<NonZeroU32>,
source_date_epoch: Option<u32>,
logger: &mut Logger<W>,
) -> Result<u64> {
let mut size = 0;
if let Some(file) = file.as_ref() {
self.apply_umask(file)?;
}
for archive in self.archives {
if archive.compression.is_uncompressed() {
if let Some(file) = file.as_mut() {
let mut writer = BufWriter::new(file);
size =
archive.write(&mut writer, alignment, source_date_epoch, size, logger)?;
writer.flush()?;
} else {
let mut stdout = std::io::stdout().lock();
size =
archive.write(&mut stdout, alignment, source_date_epoch, size, logger)?;
stdout.flush()?;
}
} else {
let mut compressor =
archive
.compression
.compress(file, source_date_epoch, || archive.size())?;
let mut writer = BufWriter::new(compressor.stdin.as_ref().unwrap());
size = archive.write(&mut writer, None, source_date_epoch, size, logger)?;
writer.flush()?;
drop(writer);
let exit_status = compressor.wait()?;
if !exit_status.success() {
return Err(Error::other(format!(
"{} failed: {exit_status}",
archive.compression.command()
)));
}
break;
}
}
Ok(size)
}
}
fn copy_file<W: Write>(path: &str, filesize: u32, writer: &mut W) -> Result<u64> {
let file = std::fs::File::open(path).map_err(|e| e.add_prefix(path))?;
let mut reader = std::io::BufReader::new(file);
let copied_bytes = std::io::copy(&mut reader, writer)?;
if copied_bytes != filesize.into() {
return Err(Error::new(
ErrorKind::UnexpectedEof,
format!("Copied {copied_bytes} bytes from {path} but expected {filesize} bytes."),
));
}
Ok(copied_bytes)
}
#[cfg(test)]
mod tests {
use std::fs::{canonicalize, hard_link};
use std::io::Read;
use std::path::Path;
use super::*;
use crate::logger::Level;
use crate::temp_dir::TempDir;
use crate::tests::TEST_LOCK;
fn create_text_file_in_tmpdir<P: AsRef<Path>>(
tempdir: &TempDir,
filename: P,
content: &[u8],
) -> String {
let path = tempdir.path.join(filename);
let mut text_file = std::fs::File::create(&path).unwrap();
text_file.write_all(content).unwrap();
path.into_os_string().into_string().unwrap()
}
pub(crate) fn make_temp_dir_with_hardlinks() -> Result<TempDir> {
let temp_dir = TempDir::new()?;
let path = temp_dir.path.join("a");
let mut file = std::fs::File::create(&path)?;
file.set_permissions(PermissionsExt::from_mode(0o755))?;
file.write_all(b"content")?;
hard_link(&path, temp_dir.path.join("b"))?;
hard_link(&path, temp_dir.path.join("c"))?;
Ok(temp_dir)
}
#[test]
fn test_determine_umask_all_read() {
assert_eq!(determine_umask(0o755), 0o022);
}
#[test]
fn test_determine_umask_only_root() {
assert_eq!(determine_umask(0o640), 0o077);
}
#[test]
fn test_sanitize_path_absolute_path() {
assert_eq!(sanitize_path("/path/to/file"), "path/to/file");
}
#[test]
fn test_sanitize_path_dot() {
assert_eq!(sanitize_path("."), ".");
}
#[test]
fn test_sanitize_path_dot_slash() {
assert_eq!(sanitize_path("./"), ".");
}
#[test]
fn test_sanitize_path_dot_slash_path() {
assert_eq!(sanitize_path("./path/to/file"), "path/to/file");
}
#[test]
fn test_sanitize_path_relative_path() {
assert_eq!(sanitize_path("path/to/file"), "path/to/file");
}
#[test]
fn test_sanitize_path_root() {
assert_eq!(sanitize_path("/"), ".");
}
#[test]
fn test_file_from_line_full_regular_file() {
let line = "/usr/bin/gzip\tusr/bin/gzip\tfile\t755\t0\t0\t1739259005\t35288";
let stat = symlink_metadata("/usr/bin/gzip").unwrap();
let key = get_hardlink_key(&stat);
let mut hardlinks = HashMap::new();
let (file, umask) = File::from_line(line, &mut hardlinks).unwrap();
assert_eq!(
file,
File::new(
Filetype::Hardlink { key, index: 1 },
"usr/bin/gzip",
0o755,
0,
0,
1739259005
)
);
assert_eq!(umask, 0o022);
assert_eq!(
hardlinks,
HashMap::from([(key, Hardlink::new("/usr/bin/gzip", 35288))])
);
}
#[test]
fn test_file_from_line_full_directory() {
let line = "/usr\tusr\tdir\t755\t0\t0\t1681992796";
let mut hardlinks = HashMap::new();
let (file, umask) = File::from_line(line, &mut hardlinks).unwrap();
assert_eq!(
file,
File::new(Filetype::Directory, "usr", 0o755, 0, 0, 1681992796)
);
assert_eq!(umask, 0);
assert!(hardlinks.is_empty());
}
#[test]
fn test_file_from_line_full_block_device() {
let line = "/dev/nvme0n1p2\tdev/nvme0n1p2\tblock\t660\t0\t0\t1745246683\t259\t2";
let mut hardlinks = HashMap::new();
let (file, umask) = File::from_line(line, &mut hardlinks).unwrap();
assert_eq!(
file,
File::new(
Filetype::BlockDevice {
major: 259,
minor: 2
},
"dev/nvme0n1p2",
0o660,
0,
0,
1745246683
)
);
assert_eq!(umask, 0);
assert!(hardlinks.is_empty());
}
#[test]
fn test_file_from_line_full_character_device() {
let line = "/dev/console\tdev/console\tchar\t600\t0\t5\t1745246724\t5\t1";
let mut hardlinks = HashMap::new();
let (file, umask) = File::from_line(line, &mut hardlinks).unwrap();
assert_eq!(
file,
File::new(
Filetype::CharacterDevice { major: 5, minor: 1 },
"dev/console",
0o600,
0,
5,
1745246724
)
);
assert_eq!(umask, 0);
assert!(hardlinks.is_empty());
}
#[test]
fn test_file_from_line_full_symlink() {
let line = "/bin\tbin\tlink\t777\t0\t0\t1647786132\tusr/bin";
let mut hardlinks = HashMap::new();
let (file, umask) = File::from_line(line, &mut hardlinks).unwrap();
assert_eq!(
file,
File::new(
Filetype::Symlink {
target: "usr/bin".into()
},
"bin",
0o777,
0,
0,
1647786132
)
);
assert_eq!(umask, 0);
assert!(hardlinks.is_empty());
}
#[test]
fn test_file_from_line_full_fifo() {
let line = "/run/initctl\trun/initctl\tfifo\t0600\t0\t0\t1746789067";
let mut hardlinks = HashMap::new();
let (file, umask) = File::from_line(line, &mut hardlinks).unwrap();
assert_eq!(
file,
File::new(Filetype::Fifo, "run/initctl", 0o600, 0, 0, 1746789067)
);
assert_eq!(umask, 0);
assert!(hardlinks.is_empty());
}
#[test]
fn test_file_from_line_full_socket() {
let line = "/run/systemd/notify\trun/systemd/notify\tsock\t777\t0\t0\t1746789058";
let mut hardlinks = HashMap::new();
let (file, umask) = File::from_line(line, &mut hardlinks).unwrap();
assert_eq!(
file,
File::new(
Filetype::Socket,
"run/systemd/notify",
0o777,
0,
0,
1746789058,
)
);
assert_eq!(umask, 0);
assert!(hardlinks.is_empty());
}
#[test]
fn test_file_from_line_empty_file() {
let line = "\tetc/fstab.empty\tfile\t644\t0\t0\t1744705149\t0";
let mut hardlinks = HashMap::new();
let (file, umask) = File::from_line(line, &mut hardlinks).unwrap();
assert_eq!(
file,
File::new(
Filetype::EmptyFile,
"etc/fstab.empty",
0o644,
0,
0,
1744705149
)
);
assert_eq!(umask, 0);
assert!(hardlinks.is_empty());
}
#[test]
fn test_file_from_line_location_regular_file() {
let line = "/usr/bin/gzip";
let stat = symlink_metadata("/usr/bin/gzip").unwrap();
let key = get_hardlink_key(&stat);
let mtime = stat.mtime().try_into().unwrap();
let size = stat.size().try_into().unwrap();
let mut hardlinks = HashMap::new();
let (file, umask) = File::from_line(line, &mut hardlinks).unwrap();
assert_eq!(
file,
File::new(
Filetype::Hardlink { key, index: 1 },
"usr/bin/gzip",
0o755,
0,
0,
mtime,
)
);
assert_eq!(umask, 0o022);
assert_eq!(
hardlinks,
HashMap::from([(key, Hardlink::new("/usr/bin/gzip", size))])
);
}
#[test]
fn test_file_from_line_location_duplicate_file() {
let line = "/usr/bin/gzip\tgzip\t\t\t\t\t1745485084";
let stat = symlink_metadata("/usr/bin/gzip").unwrap();
let key = get_hardlink_key(&stat);
let size = stat.size().try_into().unwrap();
let mut hardlinks = HashMap::new();
let (file, umask) = File::from_line(line, &mut hardlinks).unwrap();
assert_eq!(
file,
File::new(
Filetype::Hardlink { key, index: 1 },
"gzip",
0o755,
0,
0,
1745485084,
)
);
assert_eq!(umask, 0o022);
assert_eq!(
hardlinks,
HashMap::from([(key, Hardlink::new("/usr/bin/gzip", size))])
);
let (file, umask) = File::from_line(line, &mut hardlinks).unwrap();
assert_eq!(
file,
File::new(
Filetype::Hardlink { key, index: 2 },
"gzip",
0o755,
0,
0,
1745485084,
)
);
assert_eq!(umask, 0o022);
assert_eq!(
hardlinks,
HashMap::from([(key, Hardlink::with_references("/usr/bin/gzip", size, 2))])
);
}
#[test]
fn test_file_from_line_location_hardlink() {
let temp_dir = make_temp_dir_with_hardlinks().unwrap();
let path = temp_dir.path.join("a").to_str().unwrap().to_owned();
let line = format!("{path}\ta\t\t644\t1\t2");
let stat = symlink_metadata(&path).unwrap();
let mtime = stat.mtime().try_into().unwrap();
let key = get_hardlink_key(&stat);
let mut hardlinks = HashMap::new();
let (file, umask) = File::from_line(line, &mut hardlinks).unwrap();
assert_eq!(
file,
File::new(
Filetype::Hardlink { key, index: 1 },
"a",
0o644,
1,
2,
mtime,
)
);
assert_eq!(umask, 0o022);
assert_eq!(hardlinks, HashMap::from([(key, Hardlink::new(&path, 7))]));
let line = format!(
"{}/b\tb\t\t640\t3\t4\t1751413453",
temp_dir.path.to_str().unwrap()
);
let (file, umask) = File::from_line(line, &mut hardlinks).unwrap();
assert_eq!(
file,
File::new(
Filetype::Hardlink { key, index: 2 },
"b",
0o640,
3,
4,
1751413453,
)
);
assert_eq!(umask, 0o022);
assert_eq!(
hardlinks,
HashMap::from([(key, Hardlink::with_references(&path, 7, 2))])
);
}
#[test]
fn test_file_from_line_location_relative_directory() {
let _lock = TEST_LOCK.lock().unwrap();
let line = "./tests\t\t\t510\t7\t42";
let stat = symlink_metadata("tests").unwrap();
let mtime = stat.mtime().try_into().unwrap();
let mut hardlinks = HashMap::new();
let (file, umask) = File::from_line(line, &mut hardlinks).unwrap();
assert_eq!(
file,
File::new(Filetype::Directory, "tests", 0o510, 7, 42, mtime)
);
assert_eq!(umask, 0);
assert!(hardlinks.is_empty());
}
#[test]
fn test_file_from_line_location_directory() {
let line = "/usr";
let stat = symlink_metadata("/usr").unwrap();
let mtime = stat.mtime().try_into().unwrap();
let mut hardlinks = HashMap::new();
let (file, umask) = File::from_line(line, &mut hardlinks).unwrap();
assert_eq!(
file,
File::new(Filetype::Directory, "usr", 0o755, 0, 0, mtime)
);
assert_eq!(umask, 0);
assert!(hardlinks.is_empty());
}
#[test]
fn test_file_from_line_location_block_device() {
let line = "/dev/loop0";
let stat = match symlink_metadata("/dev/loop0") {
Ok(s) => s,
Err(_) => return,
};
let mode = (stat.mode() & MODE_PERMISSION_MASK).try_into().unwrap();
let mtime = stat.mtime().try_into().unwrap();
let mut hardlinks = HashMap::new();
let (file, umask) = File::from_line(line, &mut hardlinks).unwrap();
assert_eq!(
file,
File::new(
Filetype::BlockDevice {
major: major(stat.rdev()),
minor: minor(stat.rdev()),
},
"dev/loop0",
mode,
0,
stat.gid(),
mtime,
)
);
assert_eq!(umask, 0);
assert!(hardlinks.is_empty());
}
#[test]
fn test_file_from_line_location_character_device() {
let path = canonicalize("/dev/console").unwrap();
let line = path.clone().into_os_string().into_string().unwrap();
let stat = path.symlink_metadata().unwrap();
let rdev = stat.rdev();
let mode = (stat.mode() & MODE_PERMISSION_MASK).try_into().unwrap();
let mtime = stat.mtime().try_into().unwrap();
let mut hardlinks = HashMap::new();
let (file, umask) = File::from_line(&line, &mut hardlinks).unwrap();
assert_eq!(
file,
File::new(
Filetype::CharacterDevice {
major: major(rdev),
minor: minor(rdev)
},
line.strip_prefix("/").unwrap(),
mode,
stat.uid(),
stat.gid(),
mtime,
)
);
assert_eq!(umask, 0);
assert!(hardlinks.is_empty());
}
#[test]
fn test_file_from_line_location_symlink() {
let line = "/bin";
let stat = symlink_metadata("/bin").unwrap();
let mtime = stat.mtime().try_into().unwrap();
let mut hardlinks = HashMap::new();
let (file, umask) = File::from_line(line, &mut hardlinks).unwrap();
assert_eq!(
file,
File::new(
Filetype::Symlink {
target: "usr/bin".into()
},
"bin",
0o777,
0,
0,
mtime,
)
);
assert_eq!(umask, 0);
assert!(hardlinks.is_empty());
}
#[test]
fn test_file_from_line_location_fifo() {
let line = "/run/initctl";
let stat = match symlink_metadata("/run/initctl") {
Ok(s) => s,
Err(_) => return,
};
let mtime = stat.mtime().try_into().unwrap();
let mut hardlinks = HashMap::new();
let (file, umask) = File::from_line(line, &mut hardlinks).unwrap();
assert_eq!(
file,
File::new(Filetype::Fifo, "run/initctl", 0o600, 0, 0, mtime)
);
assert_eq!(umask, 0);
assert!(hardlinks.is_empty());
}
#[test]
fn test_file_from_line_location_socket() {
let line = "/run/systemd/notify";
let stat = match symlink_metadata("/run/systemd/notify") {
Ok(s) => s,
Err(_) => return,
};
let mtime = stat.mtime().try_into().unwrap();
let mut hardlinks = HashMap::new();
let (file, umask) = File::from_line(line, &mut hardlinks).unwrap();
assert_eq!(
file,
File::new(Filetype::Socket, "run/systemd/notify", 0o777, 0, 0, mtime)
);
assert_eq!(umask, 0);
assert!(hardlinks.is_empty());
}
#[test]
fn test_file_from_line_empty_fields() {
let line = "/run\t\t\t\t\t\t";
let stat = symlink_metadata("/run").unwrap();
let mtime = stat.mtime().try_into().unwrap();
let mut hardlinks = HashMap::new();
let (file, umask) = File::from_line(line, &mut hardlinks).unwrap();
assert_eq!(
file,
File::new(Filetype::Directory, "run", 0o755, 0, 0, mtime)
);
assert_eq!(umask, 0);
assert!(hardlinks.is_empty());
}
#[test]
fn test_file_from_line_fields_with_dash() {
let line = "/etc\t-\t-\t-\t-\t-\t";
let stat = symlink_metadata("/etc").unwrap();
let mtime = stat.mtime().try_into().unwrap();
let mut hardlinks = HashMap::new();
let (file, umask) = File::from_line(line, &mut hardlinks).unwrap();
assert_eq!(
file,
File::new(Filetype::Directory, "etc", 0o755, 0, 0, mtime)
);
assert_eq!(umask, 0);
assert!(hardlinks.is_empty());
}
#[test]
fn test_manifest_from_input() {
let input = b"\
# This is a comment\n\n\
/bin\tbin\tdir\t755\t0\t0\t1681992796\n\
/usr/bin/gzip\tbin/gzip\tfile\t755\t0\t0\t1739259005\t35288\n";
let mut logger = Logger::new_vec(Level::Debug);
let manifest = Manifest::from_input(input.as_ref(), &mut logger).unwrap();
let stat = symlink_metadata("/usr/bin/gzip").unwrap();
let key = get_hardlink_key(&stat);
let expected_archive = Archive::with_files_and_hardlinks(
vec![
File::new(Filetype::Directory, "bin", 0o755, 0, 0, 1681992796),
File::new(
Filetype::Hardlink { key, index: 1 },
"bin/gzip",
0o755,
0,
0,
1739259005,
),
],
HashMap::from([(key, Hardlink::new("/usr/bin/gzip", 35288))]),
);
assert_eq!(manifest, Manifest::new(vec![expected_archive], 0o022));
assert_eq!(
logger.get_logs(),
"Parsing line 3: /bin\tbin\tdir\t755\t0\t0\t1681992796\n\
Parsing line 4: /usr/bin/gzip\tbin/gzip\tfile\t755\t0\t0\t1739259005\t35288\n",
);
}
#[test]
fn test_manifest_from_input_compressed() {
let input = b"\
#cpio: zstd -1\n\
/bin\tbin\tdir\t755\t0\t0\t1681992796\n";
let mut logger = Logger::new_vec(Level::Info);
let manifest = Manifest::from_input(input.as_ref(), &mut logger).unwrap();
let expected_archive = Archive::with_files_compressed(
vec![File::new(
Filetype::Directory,
"bin",
0o755,
0,
0,
1681992796,
)],
Compression::Zstd { level: Some(1) },
);
assert_eq!(manifest, Manifest::new(vec![expected_archive], 0));
assert_eq!(logger.get_logs(), "");
}
#[test]
fn test_manifest_from_input_multiple_uncompressed() {
let input = b"\
# This is a comment\n\n\
#cpio\n\
/bin\tbin\tdir\t755\t0\t0\t1681992796\n\
#cpio\n\
/\t.\tdir\t755\t0\t0\t1732230747\n";
let mut logger = Logger::new_vec(Level::Debug);
let manifest = Manifest::from_input(input.as_ref(), &mut logger).unwrap();
let expected_manifest = Manifest::new(
vec![
Archive::with_files(vec![File::new(
Filetype::Directory,
"bin",
0o755,
0,
0,
1681992796,
)]),
Archive::with_files(vec![File::new(
Filetype::Directory,
".",
0o755,
0,
0,
1732230747,
)]),
],
0,
);
assert_eq!(manifest, expected_manifest);
assert_eq!(
logger.get_logs(),
"Parsing line 3: #cpio\n\
Parsing line 4: /bin\tbin\tdir\t755\t0\t0\t1681992796\n\
Parsing line 5: #cpio\n\
Parsing line 6: /\t.\tdir\t755\t0\t0\t1732230747\n",
);
}
#[test]
fn test_manifest_from_input_file_not_found() {
let input = b"/nonexistent\n";
let mut logger = Logger::new_vec(Level::Info);
let got = Manifest::from_input(input.as_ref(), &mut logger).unwrap_err();
assert_eq!(got.kind(), ErrorKind::NotFound);
assert_eq!(
got.to_string(),
"line 1: /nonexistent: No such file or directory (os error 2)"
);
assert_eq!(logger.get_logs(), "");
}
#[test]
fn test_manifest_from_input_invalid_cpio_directive() {
let input = b" #cpio \n #cpio: zstd \n #cpio something -42 ";
let mut logger = Logger::new_vec(Level::Warning);
let got = Manifest::from_input(input.as_ref(), &mut logger).unwrap_err();
assert_eq!(got.kind(), ErrorKind::InvalidInput);
assert_eq!(
got.to_string(),
"line 3: Unknown cpio archive directive: #cpio something -42"
);
assert_eq!(logger.get_logs(), "");
}
#[test]
fn test_manifest_from_input_unknown_compressor() {
let input = b"#cpio: brotli\n";
let mut logger = Logger::new_vec(Level::Info);
let got = Manifest::from_input(input.as_ref(), &mut logger).unwrap_err();
assert_eq!(got.kind(), ErrorKind::InvalidData);
assert_eq!(
got.to_string(),
"line 1: Unknown compression format: brotli"
);
assert_eq!(logger.get_logs(), "");
}
#[test]
fn test_manifest_write_archive_empty_bzip2() {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path.join("initrd.img");
let input = b"#cpio: bzip2 -3\n";
let mut logger = Logger::new_vec(Level::Info);
let manifest = Manifest::from_input(input.as_ref(), &mut logger).unwrap();
let file = std::fs::File::create(&path).unwrap();
let size = manifest
.write_archive(Some(file), None, Some(1754439117), &mut logger)
.unwrap();
assert_eq!(size, 124);
let mut written_file = std::fs::File::open(&path).unwrap();
let mut output = Vec::new();
let read = written_file.read_to_end(&mut output).unwrap();
assert_eq!(
output,
b"BZh31AY&SY\x12<\x9e\xb3\0\0\
\x0c^\0D\0(\0h\x802$\x14\0 \x001\
L\0\0\xd3(\x0d\x0fH\x88\x17A\xa8\x8eh!$\
\xe5l\xc6e\xf5\xba\xaf\x8b\xb9\"\x9c(H\x09\x1eOY\x80"
);
assert_eq!(read, 66);
assert_eq!(logger.get_logs(), "");
}
#[test]
fn test_manifest_write_archive_empty_gzip() {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path.join("initrd.img");
let input = b"#cpio: gzip -7\n";
let mut logger = Logger::new_vec(Level::Warning);
let manifest = Manifest::from_input(input.as_ref(), &mut logger).unwrap();
let file = std::fs::File::create(&path).unwrap();
let size = manifest
.write_archive(Some(file), None, Some(1754439117), &mut logger)
.unwrap();
assert_eq!(size, 124);
let mut written_file = std::fs::File::open(&path).unwrap();
let mut output = Vec::new();
let read = written_file.read_to_end(&mut output).unwrap();
assert_eq!(
output,
b"\x1f\x8b\x08\0\0\0\0\0\0\x03307070\
4 \x0e\x10\xab\x0e\x1d8\xc1\x18!A\x8e\x9e>\xae\
A\x8a\x8a\x8a\x0c@\0\0N\xe5\x097|\0\0\0"
);
assert_eq!(read, 48);
assert_eq!(logger.get_logs(), "");
}
#[test]
fn test_manifest_write_archive_empty_lz4() {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path.join("initrd.img");
let input = b"#cpio: lz4 -4\n";
let mut logger = Logger::new_vec(Level::Warning);
let manifest = Manifest::from_input(input.as_ref(), &mut logger).unwrap();
let file = std::fs::File::create(&path).unwrap();
let size = manifest
.write_archive(Some(file), None, Some(1754439117), &mut logger)
.unwrap();
assert_eq!(size, 124);
let mut written_file = std::fs::File::open(&path).unwrap();
let mut output = Vec::new();
let read = written_file.read_to_end(&mut output).unwrap();
assert_eq!(
output,
b"\x02!L\x18$\0\0\0\x7f0707010\
\x01\0\x13/10\x01\0#\x14B\x09\0\xe0TR\
AILER!!!\0\0\0\0"
);
assert_eq!(read, 44);
assert_eq!(logger.get_logs(), "");
}
#[test]
fn test_manifest_write_archive_empty_lzma() {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path.join("initrd.img");
let input = b"#cpio: lzma -1\n";
let mut logger = Logger::new_vec(Level::Warning);
let manifest = Manifest::from_input(input.as_ref(), &mut logger).unwrap();
let file = std::fs::File::create(&path).unwrap();
let size = manifest
.write_archive(Some(file), None, Some(1754439117), &mut logger)
.unwrap();
assert_eq!(size, 124);
let mut written_file = std::fs::File::open(&path).unwrap();
let mut output = Vec::new();
let read = written_file.read_to_end(&mut output).unwrap();
assert_eq!(
output,
b"]\0\0\x10\0\xff\xff\xff\xff\xff\xff\xff\xff\0\x18\x0d\
\xdd\x04b3\x02;A\xe5P\x06\xe8\xc4\xa0\xd8\x89Z\
pL\xa1]\xb0mv\xe7&\xc4o\xff\xfe$\x90\0"
);
assert_eq!(read, 48);
assert_eq!(logger.get_logs(), "");
}
#[test]
fn test_manifest_write_archive_empty_lzop() {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path.join("initrd.img");
let input = b"#cpio: lzop -9\n";
let mut logger = Logger::new_vec(Level::Warning);
let manifest = Manifest::from_input(input.as_ref(), &mut logger).unwrap();
let file = std::fs::File::create(&path).unwrap();
let got = manifest.write_archive(Some(file), None, Some(1754439117), &mut logger);
if got
.as_ref()
.is_err_and(|e| e.to_string() == "Program 'lzop' not found in PATH.")
{
return;
}
let size = got.unwrap();
assert_eq!(size, 124);
let mut written_file = std::fs::File::open(&path).unwrap();
let mut output = Vec::new();
let read = written_file.read_to_end(&mut output).unwrap();
output.splice(9..15, b"versio".to_owned());
output.splice(25..33, b"mtime-42".to_owned());
output.splice(34..38, b"CRCS".to_owned());
assert_eq!(
output,
b"\x89LZO\0\x0d\x0a\x1a\x0aversio\x03\
\x09\x03\0\0\x0d\0\0\0\0mtime-4\
2\0CRCS\0\0\0|\0\0\0%\xbc\x7f\
\x179\x1307F\x0010 \x05\x02\x0010 \
\x15\x01\0B\xe0\x01\x08TRAILER!!\
!\0@\0\x11\0\0\0\0\0\0"
);
assert_eq!(read, 91);
assert_eq!(logger.get_logs(), "");
}
#[test]
fn test_manifest_write_archive_empty_xz() {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path.join("initrd.img");
let input = b"#cpio: xz -6\n";
let mut logger = Logger::new_vec(Level::Warning);
let manifest = Manifest::from_input(input.as_ref(), &mut logger).unwrap();
let file = std::fs::File::create(&path).unwrap();
let size = manifest
.write_archive(Some(file), None, Some(1754439117), &mut logger)
.unwrap();
assert_eq!(size, 124);
let mut written_file = std::fs::File::open(&path).unwrap();
let mut output = Vec::new();
let read = written_file.read_to_end(&mut output).unwrap();
assert_eq!(
output,
b"\xfd7zXZ\0\0\x01i\"\xde6\x02\0!\x01\
\x16\0\0\0t/\xe5\xa3\xe0\0{\0\x1c]\0\x18\
\x0d\xdd\x04c\x9d\x8a@Z1\xe4\xcb{\x1c\xc7\xc9\xc0\
\xef\x917N\x01]\xbd\xd5q\xc8\0\0N\xe5\x097\
\0\x014|\xcb{\x1f\xc2\x90B\x99\x0d\x01\0\0\0\0\x01YZ"
);
assert_eq!(read, 84);
assert_eq!(logger.get_logs(), "");
}
#[test]
fn test_manifest_write_archive_empty_zstd() {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path.join("initrd.img");
let input = b"#cpio: zstd -2\n";
let mut logger = Logger::new_vec(Level::Warning);
let manifest = Manifest::from_input(input.as_ref(), &mut logger).unwrap();
let file = std::fs::File::create(&path).unwrap();
let size = manifest
.write_archive(Some(file), None, Some(1754439117), &mut logger)
.unwrap();
assert_eq!(size, 124);
let mut written_file = std::fs::File::open(&path).unwrap();
let mut output = Vec::new();
let read = written_file.read_to_end(&mut output).unwrap();
assert_eq!(
output,
b"(\xb5/\xfd$|\x15\x01\0\xc8070701\
010B0TRAILER!!!\0\
\0\0\0\x03\x10\0\x19\xde\x89?F\x95\xfb\x16m"
);
assert_eq!(read, 47);
assert_eq!(logger.get_logs(), "");
}
#[test]
fn test_manifest_write_fail_compression() {
let temp_dir = TempDir::new().unwrap();
let root_dir = File::new(Filetype::Directory, ".", 0o755, 0x333, 0x42, 0x6841897B);
let archive = Archive::with_files_compressed(vec![root_dir], Compression::Failing);
let manifest = Manifest::new(vec![archive], 0o022);
let file = std::fs::File::create(temp_dir.path.join("initrd.img")).unwrap();
let mut logger = Logger::new_vec(Level::Debug);
let got = manifest
.write_archive(Some(file), None, None, &mut logger)
.unwrap_err();
assert!(
matches!(got.kind(), ErrorKind::Other if got.to_string() == "false failed: exit status: 1")
|| matches!(got.kind(), ErrorKind::BrokenPipe)
);
assert_eq!(
logger.get_logs(),
".\n\
Header { ino: 0, mode: 16877, uid: 819, gid: 66, nlink: 2, mtime: 1749125499, \
filesize: 0, major: 0, minor: 0, rmajor: 0, rminor: 0, filename: \".\" }\n",
);
}
#[test]
fn test_archive_write() {
let archive = Archive::with_files(vec![
File::new(Filetype::Directory, ".", 0o755, 0x333, 0x42, 0x6841897B),
File::new(
Filetype::BlockDevice {
major: 0x6425,
minor: 0x1437,
},
"loop0",
0o660,
0x334,
0x43,
0x6862B88B,
),
File::new(
Filetype::CharacterDevice {
major: 0x2E0E,
minor: 0x8C75,
},
"console",
0o600,
0x335,
0x44,
0x6862B8B4,
),
File::new(
Filetype::Symlink {
target: "usr/sbin".into(),
},
"sbin",
0o777,
0x336,
0x45,
0x62373894,
),
File::new(Filetype::Fifo, "initctl", 0o600, 0x337, 0x46, 0x6862B88A),
File::new(Filetype::Socket, "notify", 0o777, 0x338, 0x47, 0x681DE2C2),
File::new(Filetype::EmptyFile, "fstab", 0o644, 0x339, 0x48, 0x6E44C280),
]);
let mut output = Vec::new();
let mut logger = Logger::new_vec(Level::Info);
let size = archive
.write(&mut output, None, Some(0x6B49D200), 0, &mut logger)
.unwrap();
assert_eq!(
std::str::from_utf8(&output).unwrap(),
"07070100000000000041ED0000033300000042000000026841897B\
00000000000000000000000000000000000000000000000200000000\
.\0\
07070100000001000061B00000033400000043000000016862B88B\
00000000000000000000000000006425000014370000000600000000\
loop0\0\
07070100000002000021800000033500000044000000016862B8B4\
00000000000000000000000000002E0E00008C750000000800000000\
console\0\0\0\
070701000000030000A1FF00000336000000450000000162373894\
00000008000000000000000000000000000000000000000500000000\
sbin\0\0usr/sbin\
07070100000004000011800000033700000046000000016862B88A\
00000000000000000000000000000000000000000000000800000000\
initctl\0\0\0\
070701000000050000C1FF000003380000004700000001681DE2C2\
00000000000000000000000000000000000000000000000700000000\
notify\0\0\0\0\
07070100000006000081A40000033900000048000000016B49D200\
00000000000000000000000000000000000000000000000600000000\
fstab\0\
070701000000000000000000000000000000000000000100000000\
00000000000000000000000000000000000000000000000B00000000\
TRAILER!!!\0\0\0\0",
);
assert_eq!(size, 952);
assert_eq!(archive.size(), 952);
assert_eq!(
logger.get_logs(),
".\nloop0\nconsole\nsbin\ninitctl\nnotify\nfstab\n"
);
}
#[test]
fn test_archive_write_aligned() {
let tempdir = TempDir::new().unwrap();
let example = create_text_file_in_tmpdir(
&tempdir,
"example.txt",
b"This is just an example text file!\n",
);
let small = create_text_file_in_tmpdir(&tempdir, "small.txt", b"shorter than alignment\n");
let mut hardlinks = HashMap::new();
hardlinks.insert(42, Hardlink::with_references(example, 35, 1));
hardlinks.insert(99, Hardlink::with_references(small, 23, 1));
let archive = Archive::with_files_and_hardlinks(
vec![
File::new(Filetype::Directory, ".", 0o755, 0x333, 0x42, 0x6841897B),
File::new(
Filetype::Hardlink { key: 42, index: 1 },
"example",
0o644,
0x339,
0x48,
0x6E44C280,
),
File::new(
Filetype::Hardlink { key: 99, index: 1 },
"small.txt",
0o644,
0x339,
0x48,
0x6E44C280,
),
],
hardlinks,
);
let mut output = Vec::new();
let mut logger = Logger::new_vec(Level::Info);
let size = archive
.write(
&mut output,
NonZeroU32::new(32),
Some(0x6B49D200),
0,
&mut logger,
)
.unwrap();
assert_eq!(
std::str::from_utf8(&output).unwrap(),
"07070100000000000041ED0000033300000042000000026841897B\
00000000000000000000000000000000000000000000000200000000\
.\0\
07070100000001000081A40000033900000048000000016B49D200\
00000023000000000000000000000000000000000000002200000000\
example\0\0\0\
\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\
This is just an example text file!\n\0\
07070100000002000081A40000033900000048000000016B49D200\
00000017000000000000000000000000000000000000000A00000000\
small.txt\0\
shorter than alignment\n\0\
070701000000000000000000000000000000000000000100000000\
00000000000000000000000000000000000000000000000B00000000\
TRAILER!!!\0\0\0\0",
);
assert_eq!(size, 560);
assert_eq!(archive.size(), 536);
assert_eq!(logger.get_logs(), ".\nexample\nsmall.txt\n");
}
#[test]
fn test_archive_write_hardlinks() {
let temp_dir = make_temp_dir_with_hardlinks().unwrap();
let path = temp_dir.path.join("a").to_str().unwrap().to_owned();
let archive = Archive::with_files_and_hardlinks(
vec![
File::new(
Filetype::Hardlink {
key: 8921120,
index: 1,
},
"a",
0o644,
1,
2,
0x6861C7C5,
),
File::new(
Filetype::Hardlink {
key: 8921120,
index: 2,
},
"b",
0o640,
3,
4,
0x686472CD,
),
],
HashMap::from([(8921120, Hardlink::with_references(&path, 7, 2))]),
);
let mut output = Vec::new();
let mut logger = Logger::new_vec(Level::Info);
let size = archive
.write(&mut output, None, None, 0, &mut logger)
.unwrap();
assert_eq!(
std::str::from_utf8(&output).unwrap(),
"07070100000000000081A40000000100000002000000026861C7C5\
00000000000000000000000000000000000000000000000200000000\
a\0\
07070100000000000081A0000000030000000400000002686472CD\
00000007000000000000000000000000000000000000000200000000\
b\0content\0\
070701000000000000000000000000000000000000000100000000\
00000000000000000000000000000000000000000000000B00000000\
TRAILER!!!\0\0\0\0",
);
assert_eq!(size, 356);
assert_eq!(archive.size(), 356);
assert_eq!(logger.get_logs(), "a\nb\n");
}
}