1use crate::archive::Archive;
12use crate::detect::sniff;
13use crate::error::{ArchiveError, Result};
14use crate::peel::{peel_bytes, PeelOutcome};
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum Node {
19 File { name: String, bytes: Vec<u8> },
21 Dir { name: String },
23}
24
25#[derive(Debug, Clone, Copy)]
29pub struct Limits {
30 pub max_depth: usize,
32 pub max_total_inflated: u64,
34 pub max_entries: usize,
36 pub max_index_bytes: usize,
40}
41
42impl Default for Limits {
43 fn default() -> Self {
44 Limits {
45 max_depth: 8,
46 max_total_inflated: 4 << 30, max_entries: 1_000_000,
48 max_index_bytes: 512 << 20, }
50 }
51}
52
53struct Budget {
55 total_inflated: u64,
56 entries: usize,
57}
58
59impl Budget {
60 fn add_inflated(&mut self, n: usize, limits: &Limits) -> Result<()> {
61 self.total_inflated = self.total_inflated.saturating_add(n as u64);
62 if self.total_inflated > limits.max_total_inflated {
63 return Err(ArchiveError::TotalInflatedExceeded {
64 cap: limits.max_total_inflated,
65 });
66 }
67 Ok(())
68 }
69
70 fn add_entry(&mut self, limits: &Limits) -> Result<()> {
71 self.entries = self.entries.saturating_add(1);
72 if self.entries > limits.max_entries {
73 return Err(ArchiveError::TooManyEntries {
74 max: limits.max_entries,
75 });
76 }
77 Ok(())
78 }
79}
80
81pub fn resolve(data: &[u8], name: Option<&str>, limits: &Limits) -> Result<Vec<Node>> {
89 let mut out = Vec::new();
90 let mut budget = Budget {
91 total_inflated: 0,
92 entries: 0,
93 };
94 let chain = name.unwrap_or("<input>").to_string();
95 resolve_into(data, name, limits, 0, &chain, &mut budget, &mut out)?;
96 Ok(out)
97}
98
99#[allow(clippy::too_many_arguments)]
100fn resolve_into(
101 data: &[u8],
102 name: Option<&str>,
103 limits: &Limits,
104 depth: usize,
105 chain: &str,
106 budget: &mut Budget,
107 out: &mut Vec<Node>,
108) -> Result<()> {
109 if depth > limits.max_depth {
110 return Err(ArchiveError::DepthExceeded {
111 max: limits.max_depth,
112 chain: chain.to_string(),
113 });
114 }
115
116 let format = sniff(name, data);
117
118 if format.is_compression_wrapper() {
119 let inner = match peel_bytes(data, name)? {
122 PeelOutcome::Peeled { inner, .. } => inner,
123 PeelOutcome::NotPacked => {
125 out.push(leaf(name, data));
126 return Ok(());
127 }
128 };
129 budget.add_inflated(inner.len(), limits)?;
130 let inner_name = strip_compression_ext(name);
131 let child = format!("{chain} -> {}", inner_name.as_deref().unwrap_or("<peeled>"));
132 resolve_into(
133 &inner,
134 inner_name.as_deref(),
135 limits,
136 depth + 1,
137 &child,
138 budget,
139 out,
140 )?;
141 return Ok(());
142 }
143
144 if format.is_archive() {
145 let mut archive = Archive::open(data, name)?.ok_or_else(|| ArchiveError::Open {
146 format: "archive",
147 detail: format!("{format:?} sniffed as an archive but did not open"),
148 })?;
149 let members = archive.entries().to_vec();
150 for (i, entry) in members.iter().enumerate() {
151 budget.add_entry(limits)?;
152 if entry.is_dir {
153 out.push(Node::Dir {
154 name: entry.name.clone(),
155 });
156 continue;
157 }
158 let bytes = archive.read(i)?;
159 budget.add_inflated(bytes.len(), limits)?;
160 let child = format!("{chain} -> {}", entry.name);
161 resolve_into(
162 &bytes,
163 Some(&entry.name),
164 limits,
165 depth + 1,
166 &child,
167 budget,
168 out,
169 )?;
170 }
171 return Ok(());
172 }
173
174 out.push(leaf(name, data));
175 Ok(())
176}
177
178fn leaf(name: Option<&str>, data: &[u8]) -> Node {
180 Node::File {
181 name: name.unwrap_or_default().to_string(),
182 bytes: data.to_vec(),
183 }
184}
185
186fn strip_compression_ext(name: Option<&str>) -> Option<String> {
191 let name = name?;
192 let lower = name.to_ascii_lowercase();
193 for ext in [".gz", ".bz2", ".z"] {
194 if lower.ends_with(ext) {
195 return Some(name[..name.len() - ext.len()].to_string());
196 }
197 }
198 Some(name.to_string())
199}