diff --git a/Cargo.lock b/Cargo.lock index c426742..0374aed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -424,7 +424,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1328,7 +1328,7 @@ dependencies = [ "getrandom 0.2.17", "libc", "untrusted", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -1347,7 +1347,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1604,7 +1604,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1693,9 +1693,9 @@ dependencies = [ "docx-rs", "hayro", "pdf-extract", - "png 0.17.16", + "png 0.18.1", "ppt-rs", - "quick-xml 0.41.0", + "quick-xml 0.42.0", "serde_json", "tinydocs-bus", "zip", @@ -1761,9 +1761,9 @@ checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" [[package]] name = "tokio" -version = "1.53.1" +version = "1.54.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3" dependencies = [ "bytes", "pin-project-lite", @@ -1868,9 +1868,9 @@ dependencies = [ [[package]] name = "typed-path" -version = "0.12.3" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" +checksum = "9ce898823d8cd0f028662d2f349694b1af0a5cc9be83f1d213a00d3147445e1e" [[package]] name = "typenum" @@ -2071,6 +2071,12 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-sys" version = "0.52.0" @@ -2080,6 +2086,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -2279,16 +2294,18 @@ dependencies = [ [[package]] name = "zip" -version = "9.0.5" +version = "9.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e04976da629d3b04204ba8105b396a193af8ad2583b2c79bfb3ab8af580a11e" +checksum = "3f5b58784007ff6e5f280ec82c8596686a3b9b5dd5836c63288d4479f84728b9" dependencies = [ "crc32fast", "flate2", + "getrandom 0.4.3", "indexmap", "memchr", "nix", "typed-path", + "windows-sys 0.61.2", "zopfli", ] diff --git a/Cargo.toml b/Cargo.toml index ef06ec2..a2aa61e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,7 +50,7 @@ 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. @@ -58,7 +58,7 @@ 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"] } diff --git a/src/intake/mod.rs b/src/intake/mod.rs index 279c013..ba23148 100644 --- a/src/intake/mod.rs +++ b/src/intake/mod.rs @@ -244,40 +244,37 @@ fn xml_text(xml: &[u8], shared: &[String], limit: usize) -> 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(); 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) { @@ -287,7 +284,7 @@ fn xml_text(xml: &[u8], shared: &[String], limit: usize) -> Result { .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(|| { @@ -298,13 +295,13 @@ fn xml_text(xml: &[u8], shared: &[String], limit: usize) -> Result { 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(_) => { @@ -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) { @@ -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")); diff --git a/src/intake/office_order.rs b/src/intake/office_order.rs index e00937c..f053921 100644 --- a/src/intake/office_order.rs +++ b/src/intake/office_order.rs @@ -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")); } @@ -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(), )); diff --git a/src/intake/zip_admission_tests.rs b/src/intake/zip_admission_tests.rs index 90de4c6..b2f3a38 100644 --- a/src/intake/zip_admission_tests.rs +++ b/src/intake/zip_admission_tests.rs @@ -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"unchecked")]); + // 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"unchecked")]); let bytes = fixture(&[ ("nested.zip", &nested), ("word/document.xml", b"visible"), @@ -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] diff --git a/src/markdown/ooxml.rs b/src/markdown/ooxml.rs index c4c7a2e..bef7893 100644 --- a/src/markdown/ooxml.rs +++ b/src/markdown/ooxml.rs @@ -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 `&` / `&` 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); } diff --git a/src/pdf_render/mod_tests.rs b/src/pdf_render/mod_tests.rs index 6956393..cad2f4e 100644 --- a/src/pdf_render/mod_tests.rs +++ b/src/pdf_render/mod_tests.rs @@ -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()]