stella_docx_kernel/projection/
archive.rs1use miniz_oxide::inflate::decompress_to_vec_with_limit;
2use rawzip::{CompressionMethod, ZipArchive, ZipSliceArchive, crc32};
3
4use crate::ProjectionError;
5use crate::projection::relationships::main_document_path;
6
7const DOCUMENT_XML_PATH: &[u8] = b"word/document.xml";
8const STYLES_XML_PATH: &[u8] = b"word/styles.xml";
9const ROOT_RELATIONSHIPS_PATH: &[u8] = b"_rels/.rels";
10const MAXIMUM_ROOT_RELATIONSHIPS_BYTES: usize = 1024 * 1024;
11
12#[derive(Clone, Copy, Debug, Eq, PartialEq)]
13pub struct DocxLimits {
14 pub maximum_archive_bytes: usize,
15 pub maximum_document_xml_bytes: usize,
16 pub maximum_styles_xml_bytes: usize,
17 pub maximum_entries: u64,
18 pub maximum_compression_ratio: u64,
19 pub maximum_paragraphs: usize,
20 pub maximum_structural_facts: usize,
21}
22
23impl Default for DocxLimits {
24 fn default() -> Self {
25 Self {
26 maximum_archive_bytes: 64 * 1024 * 1024,
27 maximum_document_xml_bytes: 32 * 1024 * 1024,
28 maximum_styles_xml_bytes: 8 * 1024 * 1024,
29 maximum_entries: 4096,
30 maximum_compression_ratio: 200,
31 maximum_paragraphs: 250_000,
32 maximum_structural_facts: 1_000_000,
33 }
34 }
35}
36
37#[derive(Clone, Copy)]
38struct XmlEntry {
39 wayfinder: rawzip::ZipArchiveEntryWayfinder,
40 method: CompressionMethod,
41 compressed_size: u64,
42 uncompressed_size: u64,
43 crc32: u32,
44 encrypted: bool,
45}
46
47struct PackageEntry {
48 path: Vec<u8>,
49 xml: XmlEntry,
50}
51
52struct EntryExtractionErrors {
53 invalid_entry: ProjectionError,
54 too_large: ProjectionError,
55 integrity: ProjectionError,
56}
57
58impl EntryExtractionErrors {
59 const DOCUMENT_XML: Self = Self {
60 invalid_entry: ProjectionError::InvalidDocumentXmlEntry,
61 too_large: ProjectionError::DocumentXmlTooLarge,
62 integrity: ProjectionError::DocumentXmlIntegrity,
63 };
64 const PACKAGE_RELATIONSHIPS: Self = Self {
65 invalid_entry: ProjectionError::InvalidPackageRelationships,
66 too_large: ProjectionError::PackageRelationshipsTooLarge,
67 integrity: ProjectionError::InvalidPackageRelationships,
68 };
69 const STYLES_XML: Self = Self {
70 invalid_entry: ProjectionError::InvalidStylesXmlEntry,
71 too_large: ProjectionError::StylesXmlTooLarge,
72 integrity: ProjectionError::StylesXmlIntegrity,
73 };
74}
75
76#[derive(Clone, Debug, Eq, PartialEq)]
77pub struct DocumentParts {
78 pub document_xml: Vec<u8>,
79 pub styles_xml: Option<Vec<u8>>,
80}
81
82pub fn extract_document_xml(bytes: &[u8], limits: DocxLimits) -> Result<Vec<u8>, ProjectionError> {
89 extract_document_parts(bytes, limits).map(|parts| parts.document_xml)
90}
91
92pub fn extract_document_parts(
104 bytes: &[u8],
105 limits: DocxLimits,
106) -> Result<DocumentParts, ProjectionError> {
107 if bytes.len() > limits.maximum_archive_bytes {
108 return Err(ProjectionError::ArchiveTooLarge);
109 }
110 let archive = ZipArchive::from_slice(bytes).map_err(|_| ProjectionError::InvalidArchive)?;
111 let package_entries = scan_package_entries(&archive, limits)?;
112
113 let document_path = resolve_document_path(&archive, &package_entries, limits)?;
114 let document = unique_entry(
115 &package_entries,
116 &document_path,
117 ProjectionError::DuplicateDocumentXml,
118 )?
119 .ok_or(ProjectionError::MissingDocumentXml)?;
120 validate_selected_entry(
121 document,
122 limits.maximum_document_xml_bytes,
123 ProjectionError::DocumentXmlTooLarge,
124 ProjectionError::EncryptedDocumentXml,
125 limits,
126 )?;
127 let document_xml = extract_entry(
128 &archive,
129 document,
130 &document_path,
131 limits.maximum_document_xml_bytes,
132 EntryExtractionErrors::DOCUMENT_XML,
133 )?;
134 let styles_xml = unique_entry(
135 &package_entries,
136 STYLES_XML_PATH,
137 ProjectionError::DuplicateStylesXml,
138 )?
139 .map(|styles| {
140 validate_selected_entry(
141 styles,
142 limits.maximum_styles_xml_bytes,
143 ProjectionError::StylesXmlTooLarge,
144 ProjectionError::InvalidStylesXmlEntry,
145 limits,
146 )?;
147 extract_entry(
148 &archive,
149 styles,
150 STYLES_XML_PATH,
151 limits.maximum_styles_xml_bytes,
152 EntryExtractionErrors::STYLES_XML,
153 )
154 })
155 .transpose()?;
156 Ok(DocumentParts {
157 document_xml,
158 styles_xml,
159 })
160}
161
162fn scan_package_entries(
163 archive: &ZipSliceArchive<&[u8]>,
164 limits: DocxLimits,
165) -> Result<Vec<PackageEntry>, ProjectionError> {
166 if archive.entries_hint() > limits.maximum_entries {
167 return Err(ProjectionError::TooManyArchiveEntries);
168 }
169
170 let mut package_entries = Vec::new();
171 let mut entries = archive.entries();
172 let mut entry_count = 0_u64;
173 while let Some(entry) = entries
174 .next_entry()
175 .map_err(|_| ProjectionError::InvalidArchive)?
176 {
177 entry_count = entry_count
178 .checked_add(1)
179 .ok_or(ProjectionError::TooManyArchiveEntries)?;
180 if entry_count > limits.maximum_entries {
181 return Err(ProjectionError::TooManyArchiveEntries);
182 }
183 package_entries.push(PackageEntry {
184 path: entry.file_path().as_ref().to_vec(),
185 xml: XmlEntry {
186 wayfinder: entry.wayfinder(),
187 method: entry.compression_method(),
188 compressed_size: entry.compressed_size_hint(),
189 uncompressed_size: entry.uncompressed_size_hint(),
190 crc32: entry.crc32(),
191 encrypted: entry.flags().is_encrypted(),
192 },
193 });
194 }
195 Ok(package_entries)
196}
197
198fn resolve_document_path(
199 archive: &ZipSliceArchive<&[u8]>,
200 package_entries: &[PackageEntry],
201 limits: DocxLimits,
202) -> Result<Vec<u8>, ProjectionError> {
203 if let Some(relationships) = unique_entry(
204 package_entries,
205 ROOT_RELATIONSHIPS_PATH,
206 ProjectionError::InvalidPackageRelationships,
207 )? {
208 validate_selected_entry(
209 relationships,
210 MAXIMUM_ROOT_RELATIONSHIPS_BYTES,
211 ProjectionError::PackageRelationshipsTooLarge,
212 ProjectionError::InvalidPackageRelationships,
213 limits,
214 )?;
215 let relationships_xml = extract_entry(
216 archive,
217 relationships,
218 ROOT_RELATIONSHIPS_PATH,
219 MAXIMUM_ROOT_RELATIONSHIPS_BYTES,
220 EntryExtractionErrors::PACKAGE_RELATIONSHIPS,
221 )?;
222 main_document_path(&relationships_xml)
223 } else {
224 Ok(DOCUMENT_XML_PATH.to_vec())
225 }
226}
227
228fn unique_entry(
229 entries: &[PackageEntry],
230 path: &[u8],
231 duplicate: ProjectionError,
232) -> Result<Option<XmlEntry>, ProjectionError> {
233 let mut matches = entries
234 .iter()
235 .filter(|entry| entry.path == path)
236 .map(|entry| entry.xml);
237 let first = matches.next();
238 if matches.next().is_some() {
239 return Err(duplicate);
240 }
241 Ok(first)
242}
243
244fn validate_selected_entry(
245 entry: XmlEntry,
246 maximum_bytes: usize,
247 too_large: ProjectionError,
248 encrypted: ProjectionError,
249 limits: DocxLimits,
250) -> Result<(), ProjectionError> {
251 if entry.encrypted {
252 return Err(encrypted);
253 }
254 if entry.method != CompressionMethod::STORE && entry.method != CompressionMethod::DEFLATE {
255 return Err(ProjectionError::UnsupportedCompression(
256 entry.method.as_u16(),
257 ));
258 }
259 validate_declared_sizes(
260 entry.compressed_size,
261 entry.uncompressed_size,
262 maximum_bytes,
263 too_large,
264 limits,
265 )
266}
267
268fn extract_entry(
269 archive: &ZipSliceArchive<&[u8]>,
270 entry: XmlEntry,
271 expected_path: &[u8],
272 maximum_bytes: usize,
273 errors: EntryExtractionErrors,
274) -> Result<Vec<u8>, ProjectionError> {
275 let local = archive
276 .get_entry(entry.wayfinder)
277 .map_err(|_| errors.invalid_entry.clone())?;
278 let local_header = local.local_header();
279 if local_header.compression_method() != entry.method
280 || local_header.file_path().as_ref() != expected_path
281 || local_header.flags().is_encrypted()
282 {
283 return Err(errors.invalid_entry);
284 }
285 let output = match entry.method {
286 CompressionMethod::STORE => local.data().to_vec(),
287 CompressionMethod::DEFLATE => decompress_to_vec_with_limit(local.data(), maximum_bytes)
288 .map_err(|_| errors.invalid_entry.clone())?,
289 _ => {
290 return Err(ProjectionError::UnsupportedCompression(
291 entry.method.as_u16(),
292 ));
293 }
294 };
295 let expected_size = usize::try_from(entry.uncompressed_size).map_err(|_| errors.too_large)?;
296 if output.len() != expected_size || crc32(&output) != entry.crc32 {
297 return Err(errors.integrity);
298 }
299 if u64::try_from(local.data().len()).ok() != Some(entry.compressed_size) {
300 return Err(errors.integrity);
301 }
302 Ok(output)
303}
304
305fn validate_declared_sizes(
306 compressed_size: u64,
307 uncompressed_size: u64,
308 maximum_bytes: usize,
309 too_large: ProjectionError,
310 limits: DocxLimits,
311) -> Result<(), ProjectionError> {
312 let uncompressed_size_as_usize =
313 usize::try_from(uncompressed_size).map_err(|_| too_large.clone())?;
314 if uncompressed_size_as_usize > maximum_bytes {
315 return Err(too_large);
316 }
317 if uncompressed_size == 0 {
318 return Ok(());
319 }
320 if compressed_size == 0
321 || uncompressed_size > compressed_size.saturating_mul(limits.maximum_compression_ratio)
322 {
323 return Err(ProjectionError::SuspiciousCompressionRatio);
324 }
325 Ok(())
326}