1use std::collections::HashSet;
9use std::io::{Cursor, Write};
10use std::path::Path;
11
12use base64::Engine;
13use quick_xml::Writer;
14use quick_xml::events::{BytesStart, Event};
15use quick_xml::{Reader, XmlVersion};
16use zip::ZipWriter;
17use zip::write::SimpleFileOptions;
18
19use crate::convert::{ConvertOptions, PageConsumer, read_limited_file};
20use crate::error::{Error, Result};
21use crate::ooxml::local_name;
22
23const FLAT_OPC_NAMESPACE: &[u8] = b"http://schemas.microsoft.com/office/2006/xmlPackage";
24const MAX_FLAT_OPC_BYTES: u64 = 128 * 1024 * 1024;
25const MAX_FLAT_OPC_ENTRY_BYTES: usize = 16 * 1024 * 1024;
26const MAX_FLAT_OPC_EXPANDED_BYTES: usize = 128 * 1024 * 1024;
27const MAX_FLAT_OPC_PARTS: usize = 100_000;
28const MAX_FLAT_OPC_EVENTS: usize = 500_000;
29const MAX_FLAT_OPC_NAME_BYTES: usize = 1_024;
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32enum OfficeKind {
33 Docx,
34 Xlsx,
35 Pptx,
36}
37
38struct Part {
39 name: String,
40 bytes: Vec<u8>,
41}
42
43struct FlatOpcPageSink<'a> {
44 inner: &'a mut dyn PageConsumer,
45 warnings: &'a [String],
46}
47
48impl PageConsumer for FlatOpcPageSink<'_> {
49 fn consume(&mut self, mut page: crate::ir::Page) -> Result<()> {
50 page.source_format = "flat-opc".into();
51 page.description = "Flat OPC was validated and reconstructed as a bounded inert Open XML package; macros and external resources are not executed".into();
52 for warning in self.warnings {
53 page.warn(warning.clone());
54 }
55 self.inner.consume(page)
56 }
57}
58
59pub(crate) fn looks_like_prefix(bytes: &[u8]) -> bool {
60 let text = String::from_utf8_lossy(bytes);
61 text.contains("xmlPackage")
62 && text.contains("package")
63 && (text.contains("<pkg:part") || text.contains(":part"))
64}
65
66pub(crate) fn convert(
67 path: &Path,
68 options: &ConvertOptions,
69 sink: &mut dyn PageConsumer,
70) -> Result<Vec<String>> {
71 let bytes = read_limited_file(
72 path,
73 options.max_input_bytes.min(MAX_FLAT_OPC_BYTES),
74 "Flat OPC input",
75 )?;
76 convert_bytes(&bytes, options, sink)
77}
78
79pub(crate) fn convert_bytes(
80 bytes: &[u8],
81 options: &ConvertOptions,
82 sink: &mut dyn PageConsumer,
83) -> Result<Vec<String>> {
84 if bytes.len() as u64 > options.max_input_bytes.min(MAX_FLAT_OPC_BYTES) {
85 return Err(Error::LimitExceeded("Flat OPC input exceeds limit".into()));
86 }
87 let (zip_bytes, kind) = build_zip(bytes)?;
88 let warnings = vec![
89 "Flat OPC parts were reconstructed as a bounded DOCX/XLSX/PPTX package; package XML is rendered but macros, external relationships, URLs and embedded active content remain inert".into(),
90 "Flat OPC XML and base64 parts are validated before reconstruction; part names stay confined to the in-memory package and no filesystem extraction occurs".into(),
91 ];
92 let mut page_sink = FlatOpcPageSink {
93 inner: sink,
94 warnings: &warnings,
95 };
96 let mut renderer_warnings = match kind {
97 OfficeKind::Docx => crate::ooxml::docx::convert_bytes(&zip_bytes, options, &mut page_sink)?,
98 OfficeKind::Xlsx => crate::ooxml::xlsx::convert_bytes(&zip_bytes, options, &mut page_sink)?,
99 OfficeKind::Pptx => crate::ooxml::pptx::convert_bytes(&zip_bytes, options, &mut page_sink)?,
100 };
101 renderer_warnings.extend(warnings);
102 renderer_warnings.sort();
103 renderer_warnings.dedup();
104 Ok(renderer_warnings)
105}
106
107fn build_zip(bytes: &[u8]) -> Result<(Vec<u8>, OfficeKind)> {
108 let mut reader = Reader::from_reader(Cursor::new(bytes));
109 reader.config_mut().trim_text(false);
110 let mut buffer = Vec::new();
111 let mut events = 0usize;
112 let root = loop {
113 let event = next_event(&mut reader, &mut buffer, &mut events)?;
114 match event {
115 Event::Decl(_) | Event::Comment(_) | Event::PI(_) => continue,
116 Event::Text(text)
117 if text
118 .decode()
119 .map_err(|error| {
120 Error::InvalidInput(format!("invalid Flat OPC text: {error}"))
121 })?
122 .trim()
123 .is_empty() =>
124 {
125 continue;
126 }
127 other => break other,
128 }
129 };
130 let Event::Start(root_start) = root else {
131 return Err(Error::InvalidInput(
132 "Flat OPC document has no package root".into(),
133 ));
134 };
135 if local_name(root_start.name().as_ref()) != b"package"
136 || !has_flat_opc_namespace(&root_start, reader.decoder())?
137 {
138 return Err(Error::InvalidInput(
139 "Flat OPC root must be pkg:package in the Office XML package namespace".into(),
140 ));
141 }
142 let mut parts = Vec::new();
143 let mut names = HashSet::new();
144 let mut expanded_bytes = 0usize;
145 loop {
146 let event = next_event(&mut reader, &mut buffer, &mut events)?;
147 match event {
148 Event::Start(start) if local_name(start.name().as_ref()) == b"part" => {
149 if parts.len() >= MAX_FLAT_OPC_PARTS {
150 return Err(Error::LimitExceeded(format!(
151 "Flat OPC parts exceed {MAX_FLAT_OPC_PARTS}"
152 )));
153 }
154 let part = parse_part(
155 &mut reader,
156 &mut buffer,
157 &mut events,
158 start,
159 &mut expanded_bytes,
160 )?;
161 if !names.insert(part.name.clone()) {
162 return Err(Error::InvalidInput(format!(
163 "Flat OPC contains duplicate part {}",
164 part.name
165 )));
166 }
167 parts.push(part);
168 }
169 Event::End(end) if local_name(end.name().as_ref()) == b"package" => break,
170 Event::Text(text)
171 if text
172 .decode()
173 .map_err(|error| {
174 Error::InvalidInput(format!("invalid Flat OPC text: {error}"))
175 })?
176 .trim()
177 .is_empty() => {}
178 Event::Comment(_) | Event::PI(_) => {}
179 Event::DocType(_) | Event::GeneralRef(_) => {
180 return Err(Error::InvalidInput(
181 "Flat OPC document type declarations are unsupported".into(),
182 ));
183 }
184 Event::Eof => {
185 return Err(Error::InvalidInput(
186 "Flat OPC package root is not closed".into(),
187 ));
188 }
189 _ => {
190 return Err(Error::InvalidInput(
191 "Flat OPC package contains an unexpected child".into(),
192 ));
193 }
194 }
195 buffer.clear();
196 }
197 loop {
198 let tail = next_event(&mut reader, &mut buffer, &mut events)?;
199 match tail {
200 Event::Eof => break,
201 Event::Comment(_) | Event::PI(_) => {}
202 Event::Text(text)
203 if text
204 .decode()
205 .map_err(|error| {
206 Error::InvalidInput(format!("invalid Flat OPC text: {error}"))
207 })?
208 .trim()
209 .is_empty() => {}
210 _ => {
211 return Err(Error::InvalidInput(
212 "Flat OPC contains data after the package root".into(),
213 ));
214 }
215 }
216 buffer.clear();
217 }
218 let kind = choose_kind(&names)?;
219 let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
220 let options = SimpleFileOptions::default();
221 for part in parts {
222 writer.start_file(&part.name, options)?;
223 writer.write_all(&part.bytes)?;
224 }
225 Ok((writer.finish()?.into_inner(), kind))
226}
227
228fn parse_part<R: std::io::BufRead>(
229 reader: &mut Reader<R>,
230 buffer: &mut Vec<u8>,
231 events: &mut usize,
232 start: BytesStart<'static>,
233 expanded_bytes: &mut usize,
234) -> Result<Part> {
235 let name = attribute_value(&start, b"name", reader)?;
236 let _content_type = attribute_value(&start, b"contentType", reader)?;
237 let name = normalize_part_name(&name)?;
238 let mut xml_data = None;
239 let mut binary_data = None;
240 loop {
241 let event = next_event(reader, buffer, events)?;
242 match event {
243 Event::Start(child) if local_name(child.name().as_ref()) == b"xmlData" => {
244 if xml_data.is_some() || binary_data.is_some() {
245 return Err(Error::InvalidInput(
246 "Flat OPC part has multiple payloads".into(),
247 ));
248 }
249 xml_data = Some(capture_xml_data(reader, buffer, events)?);
250 }
251 Event::Start(child) if local_name(child.name().as_ref()) == b"binaryData" => {
252 if xml_data.is_some() || binary_data.is_some() {
253 return Err(Error::InvalidInput(
254 "Flat OPC part has multiple payloads".into(),
255 ));
256 }
257 binary_data = Some(capture_binary_data(reader, buffer, events)?);
258 }
259 Event::End(end) if local_name(end.name().as_ref()) == b"part" => break,
260 Event::Text(text)
261 if text
262 .decode()
263 .map_err(|error| {
264 Error::InvalidInput(format!("invalid Flat OPC text: {error}"))
265 })?
266 .trim()
267 .is_empty() => {}
268 Event::Comment(_) | Event::PI(_) => {}
269 Event::DocType(_) | Event::GeneralRef(_) => {
270 return Err(Error::InvalidInput(
271 "Flat OPC part contains a document type declaration".into(),
272 ));
273 }
274 _ => {
275 return Err(Error::InvalidInput(
276 "Flat OPC part contains an unexpected child".into(),
277 ));
278 }
279 }
280 buffer.clear();
281 }
282 let bytes = xml_data.or(binary_data).ok_or_else(|| {
283 Error::InvalidInput(format!("Flat OPC part {name} has no XML or binary payload"))
284 })?;
285 *expanded_bytes = expanded_bytes
286 .checked_add(bytes.len())
287 .ok_or_else(|| Error::LimitExceeded("Flat OPC expanded bytes overflow".into()))?;
288 if *expanded_bytes > MAX_FLAT_OPC_EXPANDED_BYTES {
289 return Err(Error::LimitExceeded(format!(
290 "Flat OPC expanded parts exceed {MAX_FLAT_OPC_EXPANDED_BYTES} bytes"
291 )));
292 }
293 Ok(Part { name, bytes })
294}
295
296fn capture_xml_data<R: std::io::BufRead>(
297 reader: &mut Reader<R>,
298 buffer: &mut Vec<u8>,
299 events: &mut usize,
300) -> Result<Vec<u8>> {
301 let mut writer = Writer::new(Vec::new());
302 let mut depth = 0usize;
303 let mut roots = 0usize;
304 loop {
305 let event = next_event(reader, buffer, events)?;
306 match event {
307 Event::Start(start) => {
308 if depth == 0 {
309 roots += 1;
310 }
311 depth = depth.saturating_add(1);
312 writer.write_event(Event::Start(start))?;
313 }
314 Event::Empty(empty) => {
315 if depth == 0 {
316 roots += 1;
317 }
318 writer.write_event(Event::Empty(empty))?;
319 }
320 Event::End(end) => {
321 if depth == 0 {
322 break;
323 }
324 depth -= 1;
325 writer.write_event(Event::End(end))?;
326 }
327 Event::Text(text) => writer.write_event(Event::Text(text))?,
328 Event::CData(data) => writer.write_event(Event::CData(data))?,
329 Event::Comment(comment) => writer.write_event(Event::Comment(comment))?,
330 Event::PI(pi) => writer.write_event(Event::PI(pi))?,
331 Event::Decl(decl) => writer.write_event(Event::Decl(decl))?,
332 Event::DocType(_) => {
333 return Err(Error::InvalidInput(
334 "Flat OPC XML payload contains a document type declaration".into(),
335 ));
336 }
337 Event::GeneralRef(_) => {
338 return Err(Error::InvalidInput(
339 "Flat OPC XML payload contains a general entity reference".into(),
340 ));
341 }
342 Event::Eof => {
343 return Err(Error::InvalidInput(
344 "Flat OPC XML payload is not closed".into(),
345 ));
346 }
347 }
348 if writer.get_ref().len() > MAX_FLAT_OPC_ENTRY_BYTES {
349 return Err(Error::LimitExceeded(format!(
350 "Flat OPC XML part exceeds {MAX_FLAT_OPC_ENTRY_BYTES} bytes"
351 )));
352 }
353 buffer.clear();
354 }
355 if roots != 1 || writer.get_ref().is_empty() {
356 return Err(Error::InvalidInput(
357 "Flat OPC xmlData must contain exactly one XML root".into(),
358 ));
359 }
360 Ok(writer.into_inner())
361}
362
363fn capture_binary_data<R: std::io::BufRead>(
364 reader: &mut Reader<R>,
365 buffer: &mut Vec<u8>,
366 events: &mut usize,
367) -> Result<Vec<u8>> {
368 let mut encoded = Vec::new();
369 loop {
370 let event = next_event(reader, buffer, events)?;
371 match event {
372 Event::Text(text) => encoded.extend_from_slice(text.as_ref()),
373 Event::CData(data) => encoded.extend_from_slice(data.as_ref()),
374 Event::End(_) => break,
375 Event::Comment(_) | Event::PI(_) => {}
376 Event::Start(_) | Event::Empty(_) => {
377 return Err(Error::InvalidInput(
378 "Flat OPC binaryData contains nested XML".into(),
379 ));
380 }
381 Event::DocType(_) => {
382 return Err(Error::InvalidInput(
383 "Flat OPC binaryData contains a document type declaration".into(),
384 ));
385 }
386 Event::GeneralRef(_) => {
387 return Err(Error::InvalidInput(
388 "Flat OPC binaryData contains a general entity reference".into(),
389 ));
390 }
391 Event::Eof => {
392 return Err(Error::InvalidInput(
393 "Flat OPC binaryData is not closed".into(),
394 ));
395 }
396 Event::Decl(_) => {
397 return Err(Error::InvalidInput(
398 "Flat OPC binaryData contains an XML declaration".into(),
399 ));
400 }
401 }
402 if encoded.len() > MAX_FLAT_OPC_ENTRY_BYTES.saturating_mul(2) {
403 return Err(Error::LimitExceeded(
404 "Flat OPC base64 payload exceeds entry limit".into(),
405 ));
406 }
407 buffer.clear();
408 }
409 let compact: Vec<u8> = encoded
410 .into_iter()
411 .filter(|byte| !byte.is_ascii_whitespace())
412 .collect();
413 let decoded = base64::engine::general_purpose::STANDARD
414 .decode(compact)
415 .map_err(|error| {
416 Error::InvalidInput(format!("invalid Flat OPC base64 payload: {error}"))
417 })?;
418 if decoded.len() > MAX_FLAT_OPC_ENTRY_BYTES {
419 return Err(Error::LimitExceeded(format!(
420 "Flat OPC binary part exceeds {MAX_FLAT_OPC_ENTRY_BYTES} bytes"
421 )));
422 }
423 Ok(decoded)
424}
425
426fn next_event<R: std::io::BufRead>(
427 reader: &mut Reader<R>,
428 buffer: &mut Vec<u8>,
429 events: &mut usize,
430) -> Result<Event<'static>> {
431 *events = events.saturating_add(1);
432 if *events > MAX_FLAT_OPC_EVENTS {
433 return Err(Error::LimitExceeded(format!(
434 "Flat OPC XML events exceed {MAX_FLAT_OPC_EVENTS}"
435 )));
436 }
437 Ok(reader.read_event_into(buffer)?.into_owned())
438}
439
440fn has_flat_opc_namespace(
441 start: &BytesStart<'_>,
442 decoder: quick_xml::encoding::Decoder,
443) -> Result<bool> {
444 for attribute in start.attributes().with_checks(true) {
445 let attribute = attribute.map_err(|error| {
446 Error::InvalidInput(format!("invalid Flat OPC package attribute: {error}"))
447 })?;
448 let key = attribute.key.as_ref();
449 if key == b"xmlns" || key.starts_with(b"xmlns:") {
450 let value = attribute
451 .decoded_and_normalized_value(XmlVersion::Implicit1_0, decoder)
452 .map_err(|error| {
453 Error::InvalidInput(format!("invalid Flat OPC namespace: {error}"))
454 })?;
455 if value.as_bytes() == FLAT_OPC_NAMESPACE {
456 return Ok(true);
457 }
458 }
459 }
460 Ok(false)
461}
462
463fn attribute_value<R: std::io::BufRead>(
464 start: &BytesStart<'_>,
465 wanted: &[u8],
466 reader: &Reader<R>,
467) -> Result<String> {
468 for attribute in start.attributes().with_checks(true) {
469 let attribute = attribute.map_err(|error| {
470 Error::InvalidInput(format!("invalid Flat OPC part attribute: {error}"))
471 })?;
472 if local_name(attribute.key.as_ref()) == wanted {
473 return Ok(attribute
474 .decoded_and_normalized_value(XmlVersion::Implicit1_0, reader.decoder())
475 .map_err(|error| {
476 Error::InvalidInput(format!("invalid Flat OPC part attribute value: {error}"))
477 })?
478 .into_owned());
479 }
480 }
481 Err(Error::InvalidInput(format!(
482 "Flat OPC part is missing {}",
483 String::from_utf8_lossy(wanted)
484 )))
485}
486
487fn normalize_part_name(value: &str) -> Result<String> {
488 let name = value.trim_start_matches('/');
489 if name.is_empty()
490 || name.len() > MAX_FLAT_OPC_NAME_BYTES
491 || name.contains('\\')
492 || name
493 .split('/')
494 .any(|component| component.is_empty() || component == "." || component == "..")
495 {
496 return Err(Error::InvalidInput(format!(
497 "unsafe Flat OPC part name {value:?}"
498 )));
499 }
500 Ok(name.to_owned())
501}
502
503fn choose_kind(names: &HashSet<String>) -> Result<OfficeKind> {
504 let docx = names.contains("word/document.xml");
505 let xlsx = names.contains("xl/workbook.xml");
506 let pptx = names.contains("ppt/presentation.xml");
507 match (docx, xlsx, pptx) {
508 (true, false, false) => Ok(OfficeKind::Docx),
509 (false, true, false) => Ok(OfficeKind::Xlsx),
510 (false, false, true) => Ok(OfficeKind::Pptx),
511 _ => Err(Error::InvalidInput(
512 "Flat OPC must contain exactly one DOCX, XLSX or PPTX main part".into(),
513 )),
514 }
515}