use crate::archive::Archive;
use crate::detect::sniff;
use crate::error::{ArchiveError, Result};
use crate::peel::{peel_bytes, PeelOutcome};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Node {
File { name: String, bytes: Vec<u8> },
Dir { name: String },
}
#[derive(Debug, Clone, Copy)]
pub struct Limits {
pub max_depth: usize,
pub max_total_inflated: u64,
pub max_entries: usize,
pub max_index_bytes: usize,
}
impl Default for Limits {
fn default() -> Self {
Limits {
max_depth: 8,
max_total_inflated: 4 << 30, max_entries: 1_000_000,
max_index_bytes: 512 << 20, }
}
}
struct Budget {
total_inflated: u64,
entries: usize,
}
impl Budget {
fn add_inflated(&mut self, n: usize, limits: &Limits) -> Result<()> {
self.total_inflated = self.total_inflated.saturating_add(n as u64);
if self.total_inflated > limits.max_total_inflated {
return Err(ArchiveError::TotalInflatedExceeded {
cap: limits.max_total_inflated,
});
}
Ok(())
}
fn add_entry(&mut self, limits: &Limits) -> Result<()> {
self.entries = self.entries.saturating_add(1);
if self.entries > limits.max_entries {
return Err(ArchiveError::TooManyEntries {
max: limits.max_entries,
});
}
Ok(())
}
}
pub fn resolve(data: &[u8], name: Option<&str>, limits: &Limits) -> Result<Vec<Node>> {
let mut out = Vec::new();
let mut budget = Budget {
total_inflated: 0,
entries: 0,
};
let chain = name.unwrap_or("<input>").to_string();
resolve_into(data, name, limits, 0, &chain, &mut budget, &mut out)?;
Ok(out)
}
#[allow(clippy::too_many_arguments)]
fn resolve_into(
data: &[u8],
name: Option<&str>,
limits: &Limits,
depth: usize,
chain: &str,
budget: &mut Budget,
out: &mut Vec<Node>,
) -> Result<()> {
if depth > limits.max_depth {
return Err(ArchiveError::DepthExceeded {
max: limits.max_depth,
chain: chain.to_string(),
});
}
let format = sniff(name, data);
if format.is_compression_wrapper() {
let inner = match peel_bytes(data, name)? {
PeelOutcome::Peeled { inner, .. } => inner,
PeelOutcome::NotPacked => {
out.push(leaf(name, data));
return Ok(());
}
};
budget.add_inflated(inner.len(), limits)?;
let inner_name = strip_compression_ext(name);
let child = format!("{chain} -> {}", inner_name.as_deref().unwrap_or("<peeled>"));
resolve_into(
&inner,
inner_name.as_deref(),
limits,
depth + 1,
&child,
budget,
out,
)?;
return Ok(());
}
if format.is_archive() {
let mut archive = Archive::open(data, name)?.ok_or_else(|| ArchiveError::Open {
format: "archive",
detail: format!("{format:?} sniffed as an archive but did not open"),
})?;
let members = archive.entries().to_vec();
for (i, entry) in members.iter().enumerate() {
budget.add_entry(limits)?;
if entry.is_dir {
out.push(Node::Dir {
name: entry.name.clone(),
});
continue;
}
let bytes = archive.read(i)?;
budget.add_inflated(bytes.len(), limits)?;
let child = format!("{chain} -> {}", entry.name);
resolve_into(
&bytes,
Some(&entry.name),
limits,
depth + 1,
&child,
budget,
out,
)?;
}
return Ok(());
}
out.push(leaf(name, data));
Ok(())
}
fn leaf(name: Option<&str>, data: &[u8]) -> Node {
Node::File {
name: name.unwrap_or_default().to_string(),
bytes: data.to_vec(),
}
}
fn strip_compression_ext(name: Option<&str>) -> Option<String> {
let name = name?;
let lower = name.to_ascii_lowercase();
for ext in [".gz", ".bz2", ".z"] {
if lower.ends_with(ext) {
return Some(name[..name.len() - ext.len()].to_string());
}
}
Some(name.to_string())
}