use crate::archive;
use anyhow::{Context, Result, ensure};
use std::path::{Path, PathBuf};
pub trait Handler {
fn id(&self) -> &'static str;
fn matches(&self, head: &[u8]) -> bool;
fn unpack(&self, input: &Path, stage: &Path) -> Result<Vec<PathBuf>>;
}
#[derive(Debug, Clone)]
pub struct Limits {
pub max_depth: usize,
pub max_total_bytes: u64,
pub max_files: usize,
}
impl Default for Limits {
fn default() -> Self {
Limits {
max_depth: 8,
max_total_bytes: 8 << 30,
max_files: 100_000,
}
}
}
struct Queued {
path: PathBuf,
depth: usize,
}
pub fn run(
handlers: &[Box<dyn Handler>],
input: &Path,
stage: &Path,
limits: &Limits,
) -> Result<Vec<PathBuf>> {
let mut queue = vec![Queued {
path: input.to_path_buf(),
depth: 0,
}];
let mut done = Vec::new();
let mut total_bytes: u64 = 0;
while let Some(item) = queue.pop() {
ensure!(
item.depth <= limits.max_depth,
"container nesting deeper than {} levels",
limits.max_depth
);
let head = read_head(&item.path, 512)?;
let Some(handler) = handlers.iter().find(|h| h.matches(&head)) else {
done.push(item.path);
continue;
};
total_bytes += std::fs::metadata(&item.path)?.len();
ensure!(
total_bytes <= limits.max_total_bytes,
"unpacked output exceeds the limit"
);
let before = stage.read_dir().map(|d| d.count()).unwrap_or(0);
let produced = handler.unpack(&item.path, stage)?;
ensure!(
produced.len() <= limits.max_files,
"handler {} produced more than {} files",
handler.id(),
limits.max_files
);
let after = stage.read_dir().map(|d| d.count()).unwrap_or(0);
ensure!(
after - before <= limits.max_files,
"output file count exceeded the limit"
);
for p in produced {
queue.push(Queued {
path: p,
depth: item.depth + 1,
});
}
}
Ok(done)
}
fn read_head(path: &Path, n: usize) -> Result<Vec<u8>> {
use std::io::Read;
let mut f = std::fs::File::open(path).with_context(|| format!("opening {}", path.display()))?;
let mut v = vec![0u8; n];
let got = f.read(&mut v)?;
v.truncate(got);
Ok(v)
}
struct TarHandler;
impl Handler for TarHandler {
fn id(&self) -> &'static str {
"tar"
}
fn matches(&self, head: &[u8]) -> bool {
archive::kind_of(head).is_some() || head.windows(4).any(|w| w == b"ustar")
}
fn unpack(&self, input: &Path, stage: &Path) -> Result<Vec<PathBuf>> {
std::fs::create_dir_all(stage)?;
archive::extract(input, stage)?;
walk_files(stage)
}
}
pub fn unpack_into(
handlers: &[Box<dyn Handler>],
input: &Path,
out_dir: &Path,
limits: &Limits,
) -> Result<()> {
let staging = crate::treeout::prepare_staging(out_dir)?;
match run(handlers, input, &staging, limits) {
Ok(artifacts) => {
let _ = artifacts;
crate::treeout::publish(&staging, out_dir)?;
Ok(())
}
Err(e) => {
let _ = std::fs::remove_dir_all(&staging);
Err(e)
}
}
}
pub fn handlers() -> Vec<Box<dyn Handler>> {
vec![Box::new(TarHandler)]
}
fn walk_files(dir: &Path) -> Result<Vec<PathBuf>> {
let mut out = Vec::new();
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
let meta = entry.metadata()?;
if meta.is_file() {
out.push(path);
} else if meta.is_dir() {
out.extend(walk_files(&path)?);
}
}
Ok(out)
}