sim_lib_doc_core/
zip_package.rs1use std::collections::BTreeMap;
4use std::io::{Cursor, Read};
5
6use zip::ZipArchive;
7
8use crate::OfficeError;
9
10#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12pub struct ZipLimits {
13 pub max_entries: usize,
15 pub max_entry_bytes: u64,
17 pub max_total_bytes: u64,
19 pub max_ratio: u64,
21}
22
23impl ZipLimits {
24 #[must_use]
26 pub const fn office() -> Self {
27 Self {
28 max_entries: 4_096,
29 max_entry_bytes: 64 * 1024 * 1024,
30 max_total_bytes: 256 * 1024 * 1024,
31 max_ratio: 200,
32 }
33 }
34}
35
36pub fn read_zip_entries(
38 bytes: &[u8],
39 limits: &ZipLimits,
40) -> Result<BTreeMap<String, Vec<u8>>, OfficeError> {
41 let mut archive = ZipArchive::new(Cursor::new(bytes)).map_err(zip_error)?;
42 if archive.len() > limits.max_entries {
43 return Err(package_too_large(
44 "entry count",
45 format!(
46 "archive declares {} entries; limit is {}",
47 archive.len(),
48 limits.max_entries
49 ),
50 ));
51 }
52
53 let mut total = 0_u64;
54 let mut entries = BTreeMap::new();
55 for index in 0..archive.len() {
56 let mut file = archive.by_index(index).map_err(zip_error)?;
57 if file.is_dir() {
58 continue;
59 }
60 let name = file.name().replace('\\', "/");
61 let declared = file.size();
62 let compressed = file.compressed_size();
63 reject_declared_entry_size(&name, declared, limits)?;
64 reject_compression_ratio(&name, declared, compressed, limits)?;
65 reject_declared_total(&name, total, declared, limits)?;
66
67 let mut data = Vec::new();
68 let read_limit = limits.max_entry_bytes.saturating_add(1).min(
69 limits
70 .max_total_bytes
71 .saturating_sub(total)
72 .saturating_add(1),
73 );
74 (&mut file)
75 .take(read_limit)
76 .read_to_end(&mut data)
77 .map_err(|err| {
78 OfficeError::Kernel(format!("could not read zip entry {name}: {err}"))
79 })?;
80 let actual = data.len() as u64;
81 if actual > limits.max_entry_bytes {
82 return Err(package_too_large(
83 "entry bytes",
84 format!("entry {name} expands past {} bytes", limits.max_entry_bytes),
85 ));
86 }
87 total = total.checked_add(actual).ok_or_else(|| {
88 package_too_large(
89 "total bytes",
90 format!("entry {name} overflows total byte accounting"),
91 )
92 })?;
93 if total > limits.max_total_bytes {
94 return Err(package_too_large(
95 "total bytes",
96 format!(
97 "archive expands past {} bytes at entry {name}",
98 limits.max_total_bytes
99 ),
100 ));
101 }
102 entries.insert(name, data);
103 }
104 Ok(entries)
105}
106
107fn reject_declared_entry_size(
108 name: &str,
109 declared: u64,
110 limits: &ZipLimits,
111) -> Result<(), OfficeError> {
112 if declared > limits.max_entry_bytes {
113 return Err(package_too_large(
114 "entry bytes",
115 format!(
116 "entry {name} declares {declared} bytes; limit is {}",
117 limits.max_entry_bytes
118 ),
119 ));
120 }
121 Ok(())
122}
123
124fn reject_declared_total(
125 name: &str,
126 total: u64,
127 declared: u64,
128 limits: &ZipLimits,
129) -> Result<(), OfficeError> {
130 if total.saturating_add(declared) > limits.max_total_bytes {
131 return Err(package_too_large(
132 "total bytes",
133 format!(
134 "archive declares more than {} bytes at entry {name}",
135 limits.max_total_bytes
136 ),
137 ));
138 }
139 Ok(())
140}
141
142fn reject_compression_ratio(
143 name: &str,
144 declared: u64,
145 compressed: u64,
146 limits: &ZipLimits,
147) -> Result<(), OfficeError> {
148 if declared == 0 {
149 return Ok(());
150 }
151 let ratio_limit = compressed.saturating_mul(limits.max_ratio);
152 if compressed == 0 || declared > ratio_limit {
153 return Err(package_too_large(
154 "compression ratio",
155 format!(
156 "entry {name} declares {declared} bytes from {compressed} compressed bytes; ratio limit is {}",
157 limits.max_ratio
158 ),
159 ));
160 }
161 Ok(())
162}
163
164fn zip_error(error: zip::result::ZipError) -> OfficeError {
165 OfficeError::Kernel(format!("invalid ZIP package: {error}"))
166}
167
168fn package_too_large(limit: &'static str, detail: String) -> OfficeError {
169 OfficeError::PackageTooLarge { limit, detail }
170}