1use std::collections::HashMap;
5use std::io::{Cursor, Read, Write};
6
7use crate::xlsx::IoError;
8use crate::xlsx::xml::{self, El};
9
10pub const MAX_PART: u64 = 512 * 1024 * 1024;
12pub const MAX_TOTAL: u64 = 1024 * 1024 * 1024;
14pub const MAX_ENTRIES: usize = 200_000;
16
17pub struct Package<'a> {
18 zip: zip::ZipArchive<Cursor<&'a [u8]>>,
19 names: HashMap<String, usize>,
21 total: u64,
22}
23
24#[derive(Clone, Debug)]
26pub struct Rel {
27 pub id: String,
28 pub kind: String,
31 pub target: String,
33 pub external: bool,
34}
35
36pub fn normalize(name: &str) -> String {
37 name.replace('\\', "/").trim_start_matches('/').to_ascii_lowercase()
38}
39
40impl<'a> Package<'a> {
41 pub fn open(bytes: &'a [u8]) -> Result<Package<'a>, IoError> {
42 let zip = zip::ZipArchive::new(Cursor::new(bytes)).map_err(|e| IoError::Zip(e.to_string()))?;
43 if zip.len() > MAX_ENTRIES {
44 return Err(IoError::TooLarge(format!("{} zip entries", zip.len())));
45 }
46 let mut names = HashMap::new();
47 for i in 0..zip.len() {
48 if let Some(Ok(n)) = zip.name_for_index(i) {
50 names.entry(normalize(&n)).or_insert(i);
51 }
52 }
53 Ok(Package { zip, names, total: 0 })
54 }
55
56 #[cfg(test)]
58 pub fn names(&self) -> Vec<String> {
59 let mut v: Vec<String> = self.names.keys().cloned().collect();
60 v.sort();
61 v
62 }
63
64 pub fn has(&self, part: &str) -> bool {
65 self.names.contains_key(&normalize(part))
66 }
67
68 pub fn read(&mut self, part: &str) -> Result<Option<Vec<u8>>, IoError> {
70 self.read_limited(part, MAX_PART)
71 }
72
73 pub fn read_optional_limited(&mut self, part: &str, maximum: u64) -> Result<Option<Vec<u8>>, IoError> {
78 let before = self.total;
79 let read = self.read_limited(part, maximum);
80 if let Err(IoError::Zip(_)) = &read {
81 self.total = before.saturating_add(maximum.min(MAX_PART).saturating_add(1)).min(MAX_TOTAL);
82 }
83 read
84 }
85
86 pub fn read_limited(&mut self, part: &str, maximum: u64) -> Result<Option<Vec<u8>>, IoError> {
88 let maximum = maximum.min(MAX_PART);
89 let Some(&idx) = self.names.get(&normalize(part)) else {
90 return Ok(None);
91 };
92 let mut f = self.zip.by_index(idx).map_err(|e| IoError::Zip(format!("{part}: {e}")))?;
93 if f.size() > maximum {
94 return Err(IoError::TooLarge(format!("{part} expands to {} bytes", f.size())));
95 }
96 let remaining = MAX_TOTAL.saturating_sub(self.total);
97 let limit = maximum.min(remaining);
98 let mut out = Vec::with_capacity(f.size().min(16 * 1024 * 1024) as usize);
99 let read = (&mut f).take(limit + 1).read_to_end(&mut out).map_err(|e| {
100 self.total = MAX_TOTAL;
104 IoError::Zip(format!("{part}: {e}"))
105 });
106 if out.len() as u64 > limit {
107 self.total = MAX_TOTAL;
110 return Err(IoError::TooLarge(if remaining < maximum {
111 "package expands to more than 1 GB".into()
112 } else {
113 format!("{part} expands to more than {maximum} bytes")
114 }));
115 }
116 read?;
117 self.total += out.len() as u64;
118 Ok(Some(out))
119 }
120
121 pub fn read_xml(&mut self, part: &str) -> Result<Option<El>, IoError> {
122 match self.read(part)? {
123 Some(b) => xml::parse(&b).map(Some).map_err(|e| prefix(part, e)),
124 None => Ok(None),
125 }
126 }
127
128 pub fn rels(&mut self, part: &str) -> Result<Vec<Rel>, IoError> {
131 let (dir, file) = split_dir(part);
132 let rels_name = if dir.is_empty() { format!("_rels/{file}.rels") } else { format!("{dir}/_rels/{file}.rels") };
133 let root = match self.read(&rels_name)? {
134 Some(b) => match xml::parse(&b) {
135 Ok(r) => r,
136 Err(_) => return Ok(vec![]),
137 },
138 None => return Ok(vec![]),
139 };
140 Ok(root
141 .kids("Relationship")
142 .filter_map(|r| {
143 let id = r.attr("Id")?.to_string();
144 let ty = r.attr("Type").unwrap_or("");
145 let kind = ty.rsplit('/').next().unwrap_or("").to_string();
146 let target_raw = r.attr("Target")?;
147 let external = r.attr("TargetMode").is_some_and(|m| m.eq_ignore_ascii_case("External"));
148 let target = if external { target_raw.to_string() } else { resolve(dir, target_raw) };
149 Some(Rel { id, kind, target, external })
150 })
151 .collect())
152 }
153}
154
155pub fn oversized_part(bytes: &[u8]) -> Option<(String, u64)> {
157 let mut zip = zip::ZipArchive::new(Cursor::new(bytes)).ok()?;
158 (0..zip.len().min(MAX_ENTRIES)).find_map(|i| {
159 let f = zip.by_index_raw(i).ok()?;
160 (f.size() > MAX_PART).then(|| (f.name().map_or_else(|_| String::from_utf8_lossy(f.name_raw()).into_owned(), |n| n.into_owned()), f.size()))
161 })
162}
163
164fn prefix(part: &str, e: IoError) -> IoError {
165 match e {
166 IoError::Xml(m) => IoError::Xml(format!("{part}: {m}")),
167 o => o,
168 }
169}
170
171pub fn split_dir(part: &str) -> (&str, &str) {
173 let p = part.trim_start_matches('/');
174 match p.rfind('/') {
175 Some(i) => (p.get(..i).unwrap_or(""), p.get(i + 1..).unwrap_or("")),
176 None => ("", p),
177 }
178}
179
180pub fn resolve(dir: &str, target: &str) -> String {
182 let target = target.replace('\\', "/");
183 let target = target.split('#').next().unwrap_or("").to_string();
184 let mut segs: Vec<&str> = if target.starts_with('/') { vec![] } else { dir.split('/').filter(|s| !s.is_empty()).collect() };
185 for s in target.split('/') {
186 match s {
187 "" | "." => {}
188 ".." => {
189 segs.pop();
190 }
191 s => segs.push(s),
192 }
193 }
194 segs.join("/")
195}
196
197pub struct ZipOut {
200 zip: zip::ZipWriter<Cursor<Vec<u8>>>,
201}
202
203impl ZipOut {
204 pub fn new() -> ZipOut {
205 ZipOut { zip: zip::ZipWriter::new(Cursor::new(Vec::new())) }
206 }
207 pub fn add(&mut self, name: &str, data: &[u8]) -> Result<(), IoError> {
208 let opts = zip::write::SimpleFileOptions::default()
209 .compression_method(zip::CompressionMethod::Deflated)
210 .compression_level(Some(6))
211 .last_modified_time(zip::DateTime::default())
212 .large_file(data.len() as u64 >= u32::MAX as u64);
213 self.zip.start_file(name, opts).map_err(|e| IoError::Zip(e.to_string()))?;
214 self.zip.write_all(data).map_err(|e| IoError::Zip(e.to_string()))?;
215 Ok(())
216 }
217 pub fn finish(self) -> Result<Vec<u8>, IoError> {
218 self.zip.finish().map(|c| c.into_inner()).map_err(|e| IoError::Zip(e.to_string()))
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 #[test]
227 fn resolving() {
228 assert_eq!(resolve("xl", "worksheets/sheet1.xml"), "xl/worksheets/sheet1.xml");
229 assert_eq!(resolve("xl/worksheets", "../drawings/d1.xml"), "xl/drawings/d1.xml");
230 assert_eq!(resolve("xl/worksheets", "/xl/tables/t.xml"), "xl/tables/t.xml");
231 assert_eq!(resolve("", "xl/workbook.xml"), "xl/workbook.xml");
232 assert_eq!(resolve("a", "../../../x"), "x");
233 assert_eq!(split_dir("xl/workbook.xml"), ("xl", "workbook.xml"));
234 assert_eq!(split_dir("x.xml"), ("", "x.xml"));
235 }
236
237 #[test]
241 fn a_failed_part_ends_the_package_budget() {
242 let z = crate::xlsx::tests::make_zip(&[("a.xml", b"0123456789"), ("b.xml", b"hello")]);
243 let mut pkg = Package::open(&z).unwrap();
244 assert!(pkg.read("a.xml").unwrap().is_some());
245 pkg.total = MAX_TOTAL - 1;
247 assert!(pkg.read("b.xml").is_err());
248 assert_eq!(pkg.total, MAX_TOTAL, "the failed read is charged");
249 assert!(matches!(pkg.read("b.xml"), Err(IoError::TooLarge(_))));
251 }
252
253 #[test]
256 fn a_damaged_optional_part_costs_only_its_allowance() {
257 use std::io::Write as _;
258 let mut w = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
259 let stored = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
260 w.start_file("a.xml", stored).unwrap();
261 w.write_all(b"<a>damaged</a>").unwrap();
262 w.start_file("b.xml", zip::write::SimpleFileOptions::default()).unwrap();
263 w.write_all(b"hello").unwrap();
264 let mut z = w.finish().unwrap().into_inner();
265 let at = z.windows(7).position(|x| x == b"damaged").unwrap();
266 z[at] ^= 0x20;
267 let mut pkg = Package::open(&z).unwrap();
268 assert!(matches!(pkg.read_optional_limited("a.xml", 1024), Err(IoError::Zip(_))));
269 assert_eq!(pkg.total, 1025, "the failed read is charged its allowance");
270 assert_eq!(pkg.read("b.xml").unwrap().as_deref(), Some(&b"hello"[..]));
271 assert!(pkg.read("a.xml").is_err());
273 assert_eq!(pkg.total, MAX_TOTAL);
274 }
275}