use std::fs;
use std::io::{Cursor, Read, Write};
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::path::{Path, PathBuf};
use zip::write::SimpleFileOptions;
use zip::{ZipArchive, ZipWriter};
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
self.0 ^= self.0 >> 12;
self.0 ^= self.0 << 25;
self.0 ^= self.0 >> 27;
self.0.wrapping_mul(0x2545_F491_4F6C_DD1D)
}
fn below(&mut self, n: usize) -> usize {
if n == 0 {
0
} else {
(self.next() % n as u64) as usize
}
}
}
const ODD: &[&str] = &[
"<w:p>",
"</w:p>",
"<w:r><w:t>",
"</w:t></w:r>",
"<w:br/>",
"<w:tab/>",
"<w:footnoteReference w:id=\"99999999999999999999\"/>",
"<w:sz w:val=\"-1\"/>",
"<w:sz w:val=\"4294967296\"/>",
"<w:pStyle w:val=\"\"/>",
"<w:ind w:firstLine=\"NaN\"/>",
"<w:numPr><w:ilvl w:val=\"999\"/><w:numId w:val=\"-3\"/></w:numPr>",
"<w:sym w:font=\"Wingdings\" w:char=\"ZZZZ\"/>",
"<w:drawing><a:blip r:embed=\"rIdMissing\"/></w:drawing>",
"<w:fldChar w:fldCharType=\"begin\"/>",
"<w:instrText>HYPERLINK \"</w:instrText>",
"<w:sectPr><w:type w:val=\"nextPage\"/></w:sectPr>",
"<w:ins><w:del>",
"<x:unknown xmlns:x=\"urn:x\" x:a=\"�\">",
"&",
"&bogus;",
"�",
"�",
"<![CDATA[",
"]]>",
"<!--",
"-->",
"<?pi ?>",
"<!DOCTYPE w [<!ENTITY a \"aaaaaaaaaa\">]>",
"\u{FEFF}",
"\u{0}",
"\u{1F}",
"\"",
"<",
">",
"=",
];
fn fixtures() -> Vec<PathBuf> {
let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/docx");
let mut paths: Vec<_> = fs::read_dir(dir)
.unwrap()
.filter_map(Result::ok)
.map(|e| e.path())
.filter(|p| p.extension().is_some_and(|e| e == "docx"))
.collect();
paths.sort();
assert!(!paths.is_empty());
paths
}
fn unzip(bytes: &[u8]) -> Vec<(String, Vec<u8>)> {
let mut zip = ZipArchive::new(Cursor::new(bytes)).unwrap();
(0..zip.len())
.map(|i| {
let mut f = zip.by_index(i).unwrap();
let name = f.name().unwrap().into_owned();
let mut data = Vec::new();
f.read_to_end(&mut data).unwrap();
(name, data)
})
.collect()
}
fn rezip(entries: &[(String, Vec<u8>)]) -> Vec<u8> {
let mut zip = ZipWriter::new(Cursor::new(Vec::new()));
for (name, data) in entries {
zip.start_file(name.as_str(), SimpleFileOptions::default())
.unwrap();
zip.write_all(data).unwrap();
}
zip.finish().unwrap().into_inner()
}
fn mutate(rng: &mut Rng, data: &mut Vec<u8>) -> String {
let len = data.len();
let at = rng.below(len + 1);
match rng.below(6) {
0 => {
data.truncate(at);
format!("truncate at {at}")
}
1 => {
let n = 1 + rng.below(4);
for _ in 0..n {
let i = rng.below(len);
if let Some(b) = data.get_mut(i) {
*b ^= 1 << rng.below(8);
}
}
format!("flip {n} bits")
}
2 => {
let end = (at + 1 + rng.below(200)).min(len);
data.drain(at..end);
format!("delete {at}..{end}")
}
3 => {
let end = (at + 1 + rng.below(200)).min(len);
let copy = data[at..end].to_vec();
let to = rng.below(len + 1);
data.splice(to..to, copy);
format!("copy {at}..{end} to {to}")
}
4 => {
let odd = ODD[rng.below(ODD.len())];
data.splice(at..at, odd.bytes());
format!("insert {odd:?} at {at}")
}
_ => {
let names: [&[u8]; 8] = [
b"w:p",
b"w:r",
b"w:t",
b"w:pPr",
b"w:rPr",
b"w:tbl",
b"w:tc",
b"w:hyperlink",
];
let from = names[rng.below(names.len())];
let to = names[rng.below(names.len())];
let mut i = at;
while i + from.len() < data.len() && &data[i..i + from.len()] != from {
i += 1;
}
if i + from.len() < data.len() {
data.splice(i..i + from.len(), to.iter().copied());
}
format!("rename {} to {} near {at}", esc(from), esc(to))
}
}
}
fn esc(b: &[u8]) -> String {
String::from_utf8_lossy(b).into_owned()
}
fn check(bytes: &[u8], what: impl FnOnce() -> String, failures: &mut Vec<String>) {
if catch_unwind(AssertUnwindSafe(|| setzer_import::docx::import(bytes))).is_err() {
failures.push(what());
}
}
#[test]
fn truncated_files_never_panic() {
let mut failures = Vec::new();
{
for path in fixtures() {
let bytes = fs::read(&path).unwrap();
let name = path.file_name().unwrap().to_string_lossy().into_owned();
let n = bytes.len();
let cuts = (0..n.min(64))
.chain((n.saturating_sub(256)..n).step_by(3))
.chain((0..n).step_by((n / 97).max(1)));
for cut in cuts {
check(
&bytes[..cut],
|| format!("{name}: truncated to {cut} bytes"),
&mut failures,
);
}
}
}
assert!(failures.is_empty(), "panics:\n{}", failures.join("\n"));
}
#[test]
fn mutated_xml_parts_never_panic() {
const ROUNDS_PER_PART: usize = 14;
let mut failures = Vec::new();
let mut rng = Rng(0x5E72_E12D_0C0F_FEE5);
{
for path in fixtures() {
let name = path.file_name().unwrap().to_string_lossy().into_owned();
let entries = unzip(&fs::read(&path).unwrap());
let xml_parts: Vec<usize> = (0..entries.len())
.filter(|&i| entries[i].0.ends_with(".xml") || entries[i].0.ends_with(".rels"))
.collect();
for &part in &xml_parts {
for round in 0..ROUNDS_PER_PART {
let mut entries = entries.clone();
let mut steps = Vec::new();
for _ in 0..1 + rng.below(3) {
steps.push(mutate(&mut rng, &mut entries[part].1));
}
let bytes = rezip(&entries);
check(
&bytes,
|| {
format!(
"{name} {} round {round}: {}",
entries[part].0,
steps.join(", ")
)
},
&mut failures,
);
}
}
for &part in &xml_parts {
let mut entries = entries.clone();
let gone = entries.remove(part).0;
check(
&rezip(&entries),
|| format!("{name} without {gone}"),
&mut failures,
);
}
}
}
assert!(failures.is_empty(), "panics:\n{}", failures.join("\n"));
}
#[test]
fn degenerate_documents_never_panic() {
let doc = |body: &str| {
format!(
r#"<?xml version="1.0" encoding="UTF-8"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>{body}</w:body></w:document>"#
)
};
let deep = "<w:p>".repeat(10_000);
let many = "<w:p><w:r><w:t>x</w:t></w:r></w:p>".repeat(20_000);
let long_run = format!("<w:p><w:r><w:t>{}</w:t></w:r></w:p>", "a".repeat(1 << 20));
let breaks = format!(
"<w:p><w:r>{}</w:r></w:p>",
"<w:br w:type=\"page\"/>".repeat(5_000)
);
let cases: Vec<(&str, Vec<u8>)> = vec![
("empty document.xml", Vec::new()),
("not xml", b"\xFF\xFE\x00garbage".to_vec()),
("utf-16 bom only", vec![0xFF, 0xFE]),
("deep nesting", doc(&deep).into_bytes()),
("20k paragraphs", doc(&many).into_bytes()),
("1 MB run", doc(&long_run).into_bytes()),
("5k page breaks", doc(&breaks).into_bytes()),
("no body", br#"<w:document xmlns:w="x"/>"#.to_vec()),
];
let mut failures = Vec::new();
{
for (what, xml) in cases {
let bytes = rezip(&[("word/document.xml".into(), xml)]);
check(&bytes, || what.to_string(), &mut failures);
}
}
assert!(failures.is_empty(), "panics:\n{}", failures.join("\n"));
}