Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 29 additions & 12 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,15 @@ pdf-extract = { version = "0.12", optional = true }
# Bounded OOXML intake reads only selected XML parts, never extracts files.
zip = { version = "9", default-features = false, features = ["deflate"], optional = true }
# Streaming XML events avoid building attacker-controlled DOM trees.
quick-xml = { version = "0.41", optional = true }
quick-xml = { version = "0.42", optional = true }
# Workbook row conversion for document-memory Markdown; implementation only.
calamine = { version = "0.38", optional = true }
# Pure Rust PDF rasterization, isolated from the default extraction build.
hayro = { version = "0.5", optional = true }

[dev-dependencies]
# Decode raster fixtures to assert scanned-page pixels survive rendering.
png = "0.17"
png = "0.18"
# `.docx` output is a zip container; the tests re-open the produced bytes and
# assert on the OOXML parts inside.
zip = { version = "9", default-features = false, features = ["deflate"] }
Expand Down
48 changes: 21 additions & 27 deletions src/intake/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,40 +244,37 @@ fn xml_text(xml: &[u8], shared: &[String], limit: usize) -> Result<TextSink> {
return Err(Error::extraction_failed("XML nesting limit exceeded"));
}
let local = e.local_name();
if local.as_ref() == b"rPh" && phonetic_depth.is_none() {
if local.as_ref() == "rPh" && phonetic_depth.is_none() {
phonetic_depth = Some(depth);
}
in_text = local.as_ref() == b"t" && phonetic_depth.is_none();
in_text = local.as_ref() == "t" && phonetic_depth.is_none();
if phonetic_depth.is_none() {
match local.as_ref() {
b"br" | b"cr" => output.append("\n"),
b"tab" => output.append("\t"),
"br" | "cr" => output.append("\n"),
"tab" => output.append("\t"),
_ => {}
}
}
value = local.as_ref() == b"v";
if local.as_ref() == b"c" {
value = local.as_ref() == "v";
if local.as_ref() == "c" {
shared_cell = false;
for attr in e.attributes() {
let attr = attr.map_err(failed)?;
if attr.key.as_ref() == b"t" && attr.value.as_ref() == b"s" {
if attr.key.as_ref() == "t" && attr.value == "s" {
shared_cell = true;
}
}
}
}
Event::Text(e) if in_text || value => {
let decoded = e.decode().map_err(failed)?;
output.append_value(&mut current, value && shared_cell, &decoded)?;
output.append_value(&mut current, value && shared_cell, &e)?;
}
Event::GeneralRef(e) if in_text || value => {
let name = e.decode().map_err(failed)?;
let decoded = decode_reference(&name)?;
let decoded = decode_reference(&e)?;
output.append_value(&mut current, value && shared_cell, &decoded)?;
}
Event::CData(e) if in_text || value => {
let decoded = e.decode().map_err(failed)?;
output.append_value(&mut current, value && shared_cell, &decoded)?;
output.append_value(&mut current, value && shared_cell, &e)?;
}
Event::End(e) => {
if phonetic_depth == Some(depth) {
Expand All @@ -287,7 +284,7 @@ fn xml_text(xml: &[u8], shared: &[String], limit: usize) -> Result<TextSink> {
.checked_sub(1)
.ok_or_else(|| Error::extraction_failed("invalid XML nesting"))?;
let local = e.local_name();
if local.as_ref() == b"t" || local.as_ref() == b"v" {
if local.as_ref() == "t" || local.as_ref() == "v" {
if value && shared_cell {
let index: usize = current.parse().map_err(failed)?;
output.append(shared.get(index).ok_or_else(|| {
Expand All @@ -298,13 +295,13 @@ fn xml_text(xml: &[u8], shared: &[String], limit: usize) -> Result<TextSink> {
in_text = false;
value = false;
}
if [b"p".as_slice(), b"row", b"c"].contains(&local.as_ref()) {
if ["p", "row", "c"].contains(&local.as_ref()) {
output.append("\n");
}
}
Event::Empty(e) if phonetic_depth.is_none() => match e.local_name().as_ref() {
b"br" | b"cr" => output.append("\n"),
b"tab" => output.append("\t"),
"br" | "cr" => output.append("\n"),
"tab" => output.append("\t"),
_ => {}
},
Event::DocType(_) => {
Expand Down Expand Up @@ -348,10 +345,10 @@ fn xml_events(xml: &[u8], mut consume: impl FnMut(&str, Option<&str>) -> Result<
return Err(Error::extraction_failed("XML nesting limit exceeded"));
}
let local = e.local_name();
if local.as_ref() == b"rPh" && phonetic_depth.is_none() {
if local.as_ref() == "rPh" && phonetic_depth.is_none() {
phonetic_depth = Some(depth);
}
in_text = local.as_ref() == b"t" && phonetic_depth.is_none();
in_text = local.as_ref() == "t" && phonetic_depth.is_none();
}
Event::End(e) => {
if phonetic_depth == Some(depth) {
Expand All @@ -360,17 +357,14 @@ fn xml_events(xml: &[u8], mut consume: impl FnMut(&str, Option<&str>) -> Result<
depth = depth
.checked_sub(1)
.ok_or_else(|| Error::extraction_failed("invalid XML nesting"))?;
consume(
std::str::from_utf8(e.local_name().as_ref()).map_err(failed)?,
None,
)?;
consume(e.local_name().as_ref(), None)?;
in_text = false;
}
Event::Text(e) if in_text => consume("", Some(&e.decode().map_err(failed)?))?,
Event::CData(e) if in_text => consume("", Some(&e.decode().map_err(failed)?))?,
Event::Empty(e) if e.local_name().as_ref() == b"si" => consume("si", None)?,
Event::Text(e) if in_text => consume("", Some(&e))?,
Event::CData(e) if in_text => consume("", Some(&e))?,
Event::Empty(e) if e.local_name().as_ref() == "si" => consume("si", None)?,
Event::GeneralRef(e) if in_text => {
consume("", Some(&decode_reference(&e.decode().map_err(failed)?)?))?;
consume("", Some(&decode_reference(&e)?))?;
}
Event::DocType(_) => {
return Err(Error::extraction_failed("XML DTDs are not supported"));
Expand Down
11 changes: 3 additions & 8 deletions src/intake/office_order.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ fn elements(
match event {
Event::Start(element) | Event::Empty(element) => {
let local = element.local_name();
let name = std::str::from_utf8(local.as_ref()).map_err(failed)?;
let name = local.as_ref();
if name.len() > 256 || parents.len() >= 128 {
return Err(invalid("XML nesting or name limit exceeded"));
}
Expand All @@ -160,14 +160,9 @@ fn elements(
return Err(invalid("XML metadata limit exceeded"));
}
attrs.push((
std::str::from_utf8(attribute.key.as_ref())
.map_err(failed)?
.to_owned(),
attribute.key.as_ref().to_owned(),
attribute
.decoded_and_normalized_value(
quick_xml::XmlVersion::Implicit1_0,
reader.decoder(),
)
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.map_err(failed)?
.into_owned(),
));
Expand Down
10 changes: 8 additions & 2 deletions src/intake/zip_admission_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,9 @@ fn central_directory_metadata_and_zip64_extensions_have_fixed_caps() {

#[test]
fn payload_footer_cannot_be_retried_when_primary_directory_is_invalid() {
let nested = fixture(&[("nested.xml", b"<t>unchecked</t>")]);
// The nested archive is itself a readable DOCX, so a fallback to its
// footer would surface its text rather than merely fail.
let nested = fixture(&[("word/document.xml", b"<t>unchecked</t>")]);
let bytes = fixture(&[
("nested.zip", &nested),
("word/document.xml", b"<t>visible</t>"),
Expand All @@ -200,7 +202,11 @@ fn payload_footer_cannot_be_retried_when_primary_directory_is_invalid() {
// footers; the embedded ZIP must not become a second admission candidate.
corrupt[central + 10..central + 12].copy_from_slice(&99u16.to_le_bytes());
zip_preflight(&corrupt).unwrap();
assert!(extract_docx(&corrupt).is_err());
// zip 9.2 tolerates the corrupt record on a member DOCX never reads, so
// the outer document is extracted; the nested one must never be read.
let document = extract_docx(&corrupt).unwrap();
assert_eq!(document.sections.len(), 1, "{:?}", document.sections);
assert_eq!(document.sections[0].text, "visible");
}

#[test]
Expand Down
14 changes: 6 additions & 8 deletions src/markdown/ooxml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,23 +130,21 @@ fn xml_text(xml: &str, paragraph_tag: &str, text_tag: &str) -> String {
let mut in_text = false;
loop {
match reader.read_event() {
Ok(Event::Start(tag)) if tag.name().as_ref() == text_tag.as_bytes() => in_text = true,
Ok(Event::End(tag)) if tag.name().as_ref() == text_tag.as_bytes() => in_text = false,
Ok(Event::End(tag)) if tag.name().as_ref() == paragraph_tag.as_bytes() => {
Ok(Event::Start(tag)) if tag.name().as_ref() == text_tag => in_text = true,
Ok(Event::End(tag)) if tag.name().as_ref() == text_tag => in_text = false,
Ok(Event::End(tag)) if tag.name().as_ref() == paragraph_tag => {
out.push_str("\n\n");
}
Ok(Event::Text(text)) if in_text => {
out.push_str(&text.decode().unwrap_or_default());
out.push_str(&text);
}
// The parser reports each `&amp;` / `&#38;` as its own event;
// dropping them would turn "Q&A" into "QA".
Ok(Event::GeneralRef(reference)) if in_text => {
if let Ok(Some(ch)) = reference.resolve_char_ref() {
out.push(ch);
} else if let Some(entity) = reference
.decode()
.ok()
.and_then(|name| quick_xml::escape::resolve_predefined_entity(&name))
} else if let Some(entity) =
quick_xml::escape::resolve_predefined_entity(&reference)
{
out.push_str(entity);
}
Expand Down
2 changes: 1 addition & 1 deletion src/pdf_render/mod_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ fn renders_only_requested_pages_in_order_with_png_dimensions() {
assert_eq!(images.pages[1].page, 1);
let decoder = png::Decoder::new(std::io::Cursor::new(&images.pages[0].bytes));
let mut reader = decoder.read_info().unwrap();
let mut pixels = vec![0; reader.output_buffer_size()];
let mut pixels = vec![0; reader.output_buffer_size().unwrap()];
let frame = reader.next_frame(&mut pixels).unwrap();
assert!(
pixels[..frame.buffer_size()]
Expand Down
Loading