#![allow(clippy::unwrap_used)]
use std::io::Cursor;
use std::io::Write;
#[cfg(target_os = "linux")]
pub const HOST_PATH_LIMIT: usize = 4095;
#[cfg(target_os = "windows")]
pub const HOST_PATH_LIMIT: usize = 32767;
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
pub const HOST_PATH_LIMIT: usize = 1023;
#[cfg(target_os = "windows")]
pub const HOST_SYMLINK_TARGET_BYTES: usize = 4091 * 3;
#[cfg(not(target_os = "windows"))]
pub const HOST_SYMLINK_TARGET_BYTES: usize = HOST_PATH_LIMIT;
pub fn flip_first(bytes: &mut [u8], marker: &[u8]) {
let at = bytes
.windows(marker.len())
.position(|w| w == marker)
.unwrap();
bytes[at] ^= 0x01;
}
#[must_use]
pub fn path_of_len(len: usize) -> String {
let mut parts = Vec::new();
let mut left = len;
while left > 0 {
let take = left.min(100);
parts.push("a".repeat(take));
left = left.saturating_sub(take + 1);
}
parts.join("/")
}
#[must_use]
pub fn create_test_tar(entries: Vec<(&str, &[u8])>) -> Vec<u8> {
let mut ar = tar::Builder::new(Vec::new());
for (path, data) in entries {
let mut header = tar::Header::new_gnu();
header.set_size(data.len() as u64);
header.set_mode(0o644);
header.set_cksum();
ar.append_data(&mut header, path, data).unwrap();
}
ar.into_inner().unwrap()
}
#[must_use]
pub fn create_test_zip(entries: Vec<(&str, &[u8])>) -> Vec<u8> {
use zip::write::SimpleFileOptions;
use zip::write::ZipWriter;
let buffer = Vec::new();
let mut zip = ZipWriter::new(Cursor::new(buffer));
let options = SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Stored)
.unix_permissions(0o644);
for (path, data) in entries {
zip.start_file(path, options).unwrap();
zip.write_all(data).unwrap();
}
zip.finish().unwrap().into_inner()
}
#[must_use]
pub fn create_raw_zip_entry(entry_name: &str, content: &[u8]) -> Vec<u8> {
use exarch_testkit::raw_zip::RawEntry;
use exarch_testkit::raw_zip::RawZip;
let entry = RawEntry {
crc: 0,
date: 0,
..RawEntry::stored(entry_name, content)
};
RawZip::new(vec![entry]).build().bytes
}
pub struct TarTestBuilder {
builder: tar::Builder<Vec<u8>>,
}
impl TarTestBuilder {
#[must_use]
pub fn new() -> Self {
Self {
builder: tar::Builder::new(Vec::new()),
}
}
#[must_use]
pub fn add_file(mut self, path: &str, data: &[u8]) -> Self {
let mut header = tar::Header::new_gnu();
header.set_size(data.len() as u64);
header.set_mode(0o644);
header.set_cksum();
self.builder.append_data(&mut header, path, data).unwrap();
self
}
#[must_use]
pub fn add_directory(mut self, path: &str) -> Self {
let mut header = tar::Header::new_gnu();
header.set_size(0);
header.set_mode(0o755);
header.set_entry_type(tar::EntryType::Directory);
header.set_cksum();
self.builder
.append_data(&mut header, path, std::io::empty())
.unwrap();
self
}
#[must_use]
pub fn build(self) -> Vec<u8> {
self.builder.into_inner().unwrap()
}
}
impl Default for TarTestBuilder {
fn default() -> Self {
Self::new()
}
}
#[must_use]
pub fn create_zip_with_symlink(link_path: &str, target: &str) -> Vec<u8> {
use exarch_testkit::raw_zip::RawEntry;
use exarch_testkit::raw_zip::RawZip;
use exarch_testkit::raw_zip::SYMLINK_MODE;
let entry = RawEntry {
date: 0,
external_attributes: SYMLINK_MODE << 16,
..RawEntry::stored(link_path, target.as_bytes())
};
RawZip::new(vec![entry]).build().bytes
}
pub struct ZipTestBuilder {
zip: zip::ZipWriter<Cursor<Vec<u8>>>,
}
impl ZipTestBuilder {
#[must_use]
pub fn new() -> Self {
Self {
zip: zip::ZipWriter::new(Cursor::new(Vec::new())),
}
}
#[must_use]
pub fn add_file(mut self, path: &str, data: &[u8]) -> Self {
use zip::write::SimpleFileOptions;
let options = SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Stored)
.unix_permissions(0o644);
self.zip.start_file(path, options).unwrap();
self.zip.write_all(data).unwrap();
self
}
#[must_use]
pub fn add_directory(mut self, path: &str) -> Self {
use zip::write::SimpleFileOptions;
let options = SimpleFileOptions::default().unix_permissions(0o755);
self.zip.add_directory(path, options).unwrap();
self
}
#[must_use]
pub fn build(self) -> Vec<u8> {
self.zip.finish().unwrap().into_inner()
}
}
impl Default for ZipTestBuilder {
fn default() -> Self {
Self::new()
}
}
fn raw_tar_header(name: &[u8], size: usize, typeflag: u8) -> [u8; 512] {
let mut h = [0u8; 512];
let name_len = name.len().min(100);
h[..name_len].copy_from_slice(&name[..name_len]);
h[100..108].copy_from_slice(b"0000644\0");
h[108..116].copy_from_slice(b"0000000\0");
h[116..124].copy_from_slice(b"0000000\0");
h[124..136].copy_from_slice(format!("{size:011o}\0").as_bytes());
h[136..148].copy_from_slice(b"00000000000\0");
h[156] = typeflag;
h[257..263].copy_from_slice(b"ustar\0");
h[263..265].copy_from_slice(b"00");
h[148..156].copy_from_slice(b" ");
let checksum: u32 = h.iter().map(|&b| u32::from(b)).sum();
h[148..156].copy_from_slice(format!("{checksum:06o}\0 ").as_bytes());
h
}
fn pad_tar_block(out: &mut Vec<u8>, data: &[u8]) {
out.extend_from_slice(data);
let rem = data.len() % 512;
if rem != 0 {
out.extend(std::iter::repeat_n(0u8, 512 - rem));
}
}
#[must_use]
pub fn tar_with_pax_record(
pax_key: &str,
pax_value: &[u8],
entry_name: &[u8],
typeflag: u8,
data: &[u8],
) -> Vec<u8> {
let base = pax_key.len() + pax_value.len() + 3;
let mut len = base + 1;
loop {
let candidate = len.to_string().len() + base;
if candidate == len {
break;
}
len = candidate;
}
let mut record = format!("{len} {pax_key}=").into_bytes();
record.extend_from_slice(pax_value);
record.push(b'\n');
let mut tar = Vec::new();
tar.extend_from_slice(&raw_tar_header(b"PaxHeaders/entry", record.len(), b'x'));
pad_tar_block(&mut tar, &record);
tar.extend_from_slice(&raw_tar_header(entry_name, data.len(), typeflag));
pad_tar_block(&mut tar, data);
tar.extend(std::iter::repeat_n(0u8, 1024));
tar
}
#[must_use]
pub fn tar_with_pax_nul_path(pax_path: &[u8], data: &[u8]) -> Vec<u8> {
tar_with_pax_record("path", pax_path, b"ustar_name.txt", b'0', data)
}
#[must_use]
pub fn tar_with_pax_linkpath(pax_linkpath: &[u8], typeflag: u8) -> Vec<u8> {
tar_with_pax_record("linkpath", pax_linkpath, b"link_entry", typeflag, b"")
}
#[must_use]
pub fn tar_with_nonutf8_name(name_bytes: &[u8], data: &[u8]) -> Vec<u8> {
let mut tar = Vec::new();
tar.extend_from_slice(&raw_tar_header(name_bytes, data.len(), b'0'));
pad_tar_block(&mut tar, data);
tar.extend(std::iter::repeat_n(0u8, 1024));
tar
}
#[cfg(test)]
mod tests {
use super::*;
use crate::raw_zip::RawEntry;
use crate::raw_zip::RawZip;
#[test]
fn test_create_test_tar() {
let tar_data = create_test_tar(vec![("file.txt", b"hello")]);
assert!(!tar_data.is_empty());
}
#[test]
fn test_create_test_zip() {
let zip_data = create_test_zip(vec![("file.txt", b"hello")]);
assert!(!zip_data.is_empty());
}
#[test]
fn test_tar_builder() {
let tar_data = TarTestBuilder::new()
.add_file("file.txt", b"content")
.add_directory("dir/")
.build();
assert!(!tar_data.is_empty());
}
#[test]
fn test_zip_builder() {
let zip_data = ZipTestBuilder::new()
.add_file("file.txt", b"content")
.add_directory("dir/")
.build();
assert!(!zip_data.is_empty());
}
fn structural_zip_seeds() -> Vec<(&'static str, RawZip)> {
let hidden = RawZip::new(vec![RawEntry::stored("evil.sh", b"echo pwned\n")]).build();
let gap = hidden.bytes[..hidden.directory_start].to_vec();
let mut hidden_local = RawZip::new(vec![RawEntry::stored("benign.txt", b"hello")]);
hidden_local.before_directory.clone_from(&gap);
let mut uncounted = RawZip::new(vec![
RawEntry::stored("benign.txt", b"hello"),
RawEntry::stored("hidden.sh", b"echo pwned\n"),
]);
uncounted.end_count = Some(1);
let mut descriptor_gap = RawZip::new(vec![
RawEntry::stored("a.txt", b"hello").with_descriptor(true, true),
]);
descriptor_gap.before_directory = gap;
vec![
(
"header-name-mismatch.bin",
RawZip::new(vec![
RawEntry::stored("cron_worker.sh", b"payload")
.with_central_name("doc_report.pdf"),
]),
),
(
"duplicate-names.bin",
RawZip::new(vec![
RawEntry::stored("a.txt", b"first"),
RawEntry::stored("a.txt", b"EVIL"),
]),
),
("hidden-local-header.bin", hidden_local),
("uncounted-central-record.bin", uncounted),
("descriptor-gap.bin", descriptor_gap),
]
}
#[test]
#[ignore = "generates fuzz/seeds/ on demand; not part of the default test run"]
fn dump_fuzz_seeds() {
let repo_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(std::path::Path::parent)
.unwrap();
let seeds = repo_root.join("fuzz/seeds");
let write_seed = |target: &str, name: &str, data: &[u8]| {
let dir = seeds.join(target);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join(name), data).unwrap();
};
write_seed(
"tar",
"basic.bin",
&create_test_tar(vec![("file.txt", b"hello world")]),
);
write_seed(
"tar",
"builder.bin",
&TarTestBuilder::new()
.add_file("file.txt", b"content")
.add_directory("dir/")
.build(),
);
write_seed(
"tar",
"pax-record.bin",
&tar_with_pax_record(
"path",
b"pax/override/path.txt",
b"ustar_name.txt",
b'0',
b"data",
),
);
write_seed(
"tar",
"pax-nul-path.bin",
&tar_with_pax_nul_path(b"path/with\0nul", b"data"),
);
write_seed(
"tar",
"pax-linkpath-symlink.bin",
&tar_with_pax_linkpath(b"../../etc/passwd", b'2'),
);
write_seed(
"tar",
"pax-linkpath-hardlink.bin",
&tar_with_pax_linkpath(b"../../etc/passwd", b'1'),
);
write_seed(
"tar",
"nonutf8-name.bin",
&tar_with_nonutf8_name(&[0xff, 0xfe, b'/', b'x'], b"data"),
);
write_seed(
"zip",
"basic.bin",
&create_test_zip(vec![("file.txt", b"hello world")]),
);
write_seed(
"zip",
"builder.bin",
&ZipTestBuilder::new()
.add_file("file.txt", b"content")
.add_directory("dir/")
.build(),
);
write_seed(
"zip",
"raw-traversal.bin",
&create_raw_zip_entry("../../etc/passwd", b"data"),
);
write_seed(
"zip",
"symlink-escape.bin",
&create_zip_with_symlink("link", "../../etc/passwd"),
);
for (name, zip) in structural_zip_seeds() {
write_seed("zip", name, &zip.build().bytes);
}
}
}