Skip to main content

document_svg/document/
flatopc.rs

1//! Bounded Flat OPC (Open XML package serialized as one XML document).
2//!
3//! Flat OPC stores the parts of a DOCX/XLSX/PPTX package as `pkg:part`
4//! elements. This adapter reconstructs an in-memory ZIP only after validating
5//! the package namespace, part names, XML/base64 payloads and aggregate byte
6//! limits, then delegates to the existing Office Open XML renderers.
7
8use 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}