Files
vectorcraft/crates/engine/tests/text_import_fuzz.rs
T
dotCooCoo c38607a5ae Tests: .doc import fuzzing also damages the compound-file container
A new text_import_fuzz case builds a valid Word 97-2003 document (version
3 or 4 compound file, its streams in the mini stream or in FAT sectors),
checks that it places, and then damages the container itself: header
fields, the sector and mini sector shifts, FAT and mini FAT entries,
directory entry fields and stream sizes, single bytes, and truncation.

Session::execute turns a panic into EngineError::Internal, so the cases
in this file did not fail when an import panicked. They now go through
no_panic, as import_fuzz.rs does.
2026-11-01 09:24:09 +07:00

567 lines
26 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Untrusted text files placed with File → Place never crash: any bytes, with any Text Import
//! Options, place as area type or fail with an error, and so do hostile RTF files, Word documents
//! and Word 97-2003 documents, including ones whose compound file container is damaged.
//!
//! `PROPTEST_CASES=20000 cargo test -p vectorcraft-engine --test text_import_fuzz` runs a deeper search.
// Integration tests: unwrapping and panicking on failure is fine here, unlike in shipped code (AGENTS.md › Robustness).
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use proptest::prelude::*;
use serde_json::json;
use vectorcraft_testkit::catch_quiet;
proptest! {
#![proptest_config(ProptestConfig { cases: 64, failure_persistence: None, ..ProptestConfig::default() })]
#[test]
fn any_bytes_place_as_text_without_panics(
bytes in prop::collection::vec(prop_oneof![any::<u8>(), Just(b'\r'), Just(b'\n'), Just(b' ')], 0..400),
bom in prop::sample::select(vec![&b""[..], b"\xEF\xBB\xBF", b"\xFF\xFE", b"\xFE\xFF"]),
ansi in any::<bool>(),
mac in any::<bool>(),
lines in any::<bool>(),
paras in any::<bool>(),
spaces in prop::option::of(0u64..8),
) {
// Exercise import/options/thumbnails with bundled fonts, independently of host font collections.
// Font lookup and fallback have dedicated tests in vectorcraft-text.
vectorcraft_text::FontDb::global().set_system_fallback(false);
let data = [bom, &bytes[..]].concat();
let r = catch_quiet(|| {
let mut s = vectorcraft_engine::Session::new();
s.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
let text = json!({
"characterSet": if ansi { "ansi" } else { "unicode" },
"platform": if mac { "mac" } else { "windows" },
"removeLineReturns": lines,
"removeParagraphReturns": paras,
"replaceSpaces": spaces,
});
let p = json!({"name": "fuzz.txt", "dataBase64": vectorcraft_format::base64_encode(&data), "text": text, "thumbnail": 8});
let _ = no_panic(s.execute("file.place.info", &p));
let _ = no_panic(s.execute("file.place", &p));
});
prop_assert!(r.is_ok(), "panicked: {:?}", r.err());
}
#[test]
fn any_rtf_places_without_panics(
tokens in prop::collection::vec(arb_rtf_token(), 0..200),
name in prop::sample::select(vec!["fuzz.rtf", "fuzz.doc"]),
) {
vectorcraft_text::FontDb::global().set_system_fallback(false);
let data = [&b"{\\rtf1"[..], &tokens.concat()].concat();
let r = catch_quiet(|| {
let mut s = vectorcraft_engine::Session::new();
s.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
let p = json!({"name": name, "dataBase64": vectorcraft_format::base64_encode(&data), "thumbnail": 8});
let _ = no_panic(s.execute("file.place.info", &p));
let _ = no_panic(s.execute("file.place", &p));
});
prop_assert!(r.is_ok(), "panicked: {:?}", r.err());
}
#[test]
fn any_docx_places_without_panics(
body in prop_oneof![1 => prop::collection::vec(arb_docx_token(), 0..120), 3 => prop::collection::vec(arb_docx_paragraph(), 0..12)],
styles in prop_oneof![1 => prop::collection::vec(arb_docx_token(), 0..40), 3 => prop::collection::vec(arb_docx_style(), 0..6)],
changes in prop_oneof![
3 => Just(vec![]),
1 => prop::collection::vec((any::<prop::sample::Index>(), any::<u8>()), 1..3),
],
name in prop::sample::select(vec!["fuzz.docx", "fuzz.bin"]),
) {
vectorcraft_text::FontDb::global().set_system_fallback(false);
let w = "xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\"";
let document = format!("<w:document {w}><w:body>{}</w:body></w:document>", body.concat());
let styles = format!("<w:styles {w}>{}</w:styles>", styles.concat());
let files = [("word/document.xml", document.as_bytes()), ("word/styles.xml", styles.as_bytes())];
let mut data = vectorcraft_engine::cmd::fileio::zip::store(&files).unwrap();
// A changed byte anywhere: in a header, a size, the directory or the XML.
for (at, byte) in changes {
let i = at.index(data.len());
data[i] = byte;
}
let r = catch_quiet(|| {
let mut s = vectorcraft_engine::Session::new();
s.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
let p = json!({"name": name, "dataBase64": vectorcraft_format::base64_encode(&data), "thumbnail": 8});
let _ = no_panic(s.execute("file.place.info", &p));
let _ = no_panic(s.execute("file.place", &p));
});
prop_assert!(r.is_ok(), "panicked: {:?}", r.err());
}
#[test]
fn any_doc_places_without_panics(
text in prop::collection::vec(prop::sample::select(DOC_CHARS.chars().collect::<Vec<_>>()), 1..60),
compressed in any::<bool>(),
chpx in arb_grpprl(),
papx in arb_grpprl(),
istd in 0u16..4,
stsh in prop_oneof![1 => prop::collection::vec(any::<u8>(), 0..120), 3 => arb_stsh()],
ffn in prop::collection::vec(any::<u8>(), 0..80),
changes in prop_oneof![
3 => Just(vec![]),
1 => prop::collection::vec((any::<bool>(), any::<prop::sample::Index>(), any::<u8>()), 1..4),
],
name in prop::sample::select(vec!["fuzz.doc", "fuzz.bin"]),
) {
vectorcraft_text::FontDb::global().set_system_fallback(false);
let text: String = text.into_iter().collect();
let (mut wd, mut table) = word97(&text, compressed, &chpx, istd, &papx, &stsh, &ffn);
// A changed byte anywhere in either stream: in the FIB, the text, a property page or a
// table.
for (in_table, at, byte) in changes {
let stream = if in_table { &mut table } else { &mut wd };
let i = at.index(stream.len());
stream[i] = byte;
}
let data = compound_file(&wd, &table, cfb::Version::V4);
let r = catch_quiet(|| {
let mut s = vectorcraft_engine::Session::new();
s.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
let p = json!({"name": name, "dataBase64": vectorcraft_format::base64_encode(&data), "thumbnail": 8});
let _ = no_panic(s.execute("file.place.info", &p));
let _ = no_panic(s.execute("file.place", &p));
});
prop_assert!(r.is_ok(), "panicked: {:?}", r.err());
}
#[test]
fn any_damaged_doc_container_places_without_panics(
text in prop::collection::vec(prop::sample::select(DOC_CHARS.chars().collect::<Vec<_>>()), 1..60),
compressed in any::<bool>(),
v3 in any::<bool>(),
large in any::<bool>(),
damage in prop::collection::vec(arb_cfb_damage(), 1..4),
name in prop::sample::select(vec!["fuzz.doc", "fuzz.bin"]),
) {
vectorcraft_text::FontDb::global().set_system_fallback(false);
// A letter first, so that the undamaged document has text to place.
let text: String = std::iter::once('a').chain(text).collect();
let (mut wd, table) = word97(&text, compressed, &[], 0, &[], &[], &[]);
// Past the mini stream cutoff (4,096 bytes) the WordDocument stream is kept in sectors the
// FAT chains; below it, both streams are kept in the mini stream the mini FAT chains.
if large {
wd.resize(9000, 0);
}
let mut data = compound_file(&wd, &table, if v3 { cfb::Version::V3 } else { cfb::Version::V4 });
let mut s = vectorcraft_engine::Session::new();
s.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
let p = json!({"name": "fuzz.doc", "dataBase64": vectorcraft_format::base64_encode(&data)});
let placed = s.execute("file.place", &p);
prop_assert!(placed.is_ok(), "the undamaged document doesn't place: {:?}", placed.err());
let layout = CfbLayout::of(&data);
for d in &damage {
d.apply(&mut data, &layout);
}
let r = catch_quiet(|| {
let mut s = vectorcraft_engine::Session::new();
s.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
let p = json!({"name": name, "dataBase64": vectorcraft_format::base64_encode(&data), "thumbnail": 8});
let _ = no_panic(s.execute("file.place.info", &p));
let _ = no_panic(s.execute("file.place", &p));
});
prop_assert!(r.is_ok(), "panicked: {:?}", r.err());
}
}
/// `r`, after failing the case if the command panicked. The engine's guard reports a panic as
/// `EngineError::Internal`.
fn no_panic(r: Result<serde_json::Value, vectorcraft_engine::EngineError>) -> Result<serde_json::Value, vectorcraft_engine::EngineError> {
if let Err(vectorcraft_engine::EngineError::Internal { cmd, msg }) = &r {
panic!("{cmd} panicked: {msg}");
}
r
}
/// A compound file of `version` holding the WordDocument stream `wd` and the table stream `table`.
fn compound_file(wd: &[u8], table: &[u8], version: cfb::Version) -> Vec<u8> {
let mut cf = cfb::CompoundFile::create_with_version(version, std::io::Cursor::new(Vec::new())).unwrap();
std::io::Write::write_all(&mut cf.create_stream("WordDocument").unwrap(), wd).unwrap();
std::io::Write::write_all(&mut cf.create_stream("1Table").unwrap(), table).unwrap();
cf.flush().unwrap();
cf.into_inner().into_inner()
}
/// The compound file header's fields (\[MS-CFB\] §2.2) a damage changes: their offset and width.
const CFB_HEADER: [(usize, usize); 17] = [
(0, 4), // signature
(24, 2), // minor version
(26, 2), // major version
(28, 2), // byte order
(30, 2), // sector shift
(32, 2), // mini sector shift
(40, 4), // directory sectors
(44, 4), // FAT sectors
(48, 4), // first directory sector
(52, 4), // transaction signature
(56, 4), // mini stream cutoff
(60, 4), // first mini FAT sector
(64, 4), // mini FAT sectors
(68, 4), // first DIFAT sector
(72, 4), // DIFAT sectors
(76, 4), // DIFAT[0]
(80, 4), // DIFAT[1]
];
/// A directory entry's fields (\[MS-CFB\] §2.6.1) a damage changes: their offset and width.
const CFB_ENTRY: [(usize, usize); 10] = [
(0, 2), // first character of the name
(64, 2), // name length
(66, 1), // object type
(67, 1), // color
(68, 4), // left sibling
(72, 4), // right sibling
(76, 4), // child
(116, 4), // starting sector
(120, 4), // stream size, low
(124, 4), // stream size, high
];
/// Where an undamaged compound file keeps its directory, FAT and mini FAT.
struct CfbLayout {
/// The bytes of one sector.
sector: usize,
/// The offsets of the first directory sector, the first FAT sector and the first mini FAT
/// sector.
directory: usize,
fat: usize,
mini_fat: usize,
}
impl CfbLayout {
fn of(data: &[u8]) -> Self {
let u32_at = |at: usize| u32::from_le_bytes(data[at..at + 4].try_into().unwrap()) as usize;
let sector = 1 << u16::from_le_bytes(data[30..32].try_into().unwrap());
let at = |n: usize| (n + 1) * sector;
let layout = Self { sector, directory: at(u32_at(48)), fat: at(u32_at(76)), mini_fat: at(u32_at(60)) };
// The root entry's name opens the directory, the FAT marks its own sector, and the mini FAT
// has a sector.
assert_eq!(&data[layout.directory..layout.directory + 4], b"R\0o\0");
assert_eq!(u32_at(layout.fat + 4 * u32_at(76)), 0xFFFF_FFFD);
assert!(u32_at(64) >= 1);
layout
}
}
/// A change to the container of a compound file rather than to the bytes of its streams.
#[derive(Clone, Debug)]
enum CfbDamage {
/// A header field set to a value.
Header(prop::sample::Index, u32),
/// A field of one of the first directory sector's entries set to a value.
Entry(prop::sample::Index, prop::sample::Index, u32),
/// An entry of the first FAT sector (the next sector of a chain) set to a value.
Fat(prop::sample::Index, u32),
/// An entry of the first mini FAT sector set to a value.
MiniFat(prop::sample::Index, u32),
/// A byte anywhere set to a value.
Byte(prop::sample::Index, u8),
/// The file cut short.
Truncate(prop::sample::Index),
}
impl CfbDamage {
fn apply(&self, data: &mut Vec<u8>, layout: &CfbLayout) {
let mut put = |at: usize, width: usize, value: u32| {
if let Some(b) = data.get_mut(at..at + width) {
b.copy_from_slice(&value.to_le_bytes()[..width]);
}
};
match self {
Self::Header(field, value) => {
let (at, width) = CFB_HEADER[field.index(CFB_HEADER.len())];
put(at, width, *value);
}
Self::Entry(entry, field, value) => {
let (at, width) = CFB_ENTRY[field.index(CFB_ENTRY.len())];
put(layout.directory + 128 * entry.index(layout.sector / 128) + at, width, *value);
}
Self::Fat(entry, value) => put(layout.fat + 4 * entry.index(layout.sector / 4), 4, *value),
Self::MiniFat(entry, value) => put(layout.mini_fat + 4 * entry.index(layout.sector / 4), 4, *value),
// A file cut to nothing has no byte to change and nothing to cut.
Self::Byte(..) | Self::Truncate(_) if data.is_empty() => {}
Self::Byte(at, value) => {
let i = at.index(data.len());
data[i] = *value;
}
Self::Truncate(at) => data.truncate(at.index(data.len())),
}
}
}
/// A sector number, size, count or shift: the special sector numbers (\[MS-CFB\] §2.1), small and
/// huge values, or any.
fn arb_cfb_value() -> impl Strategy<Value = u32> {
prop_oneof![
3 => prop::sample::select(vec![
0,
1,
2,
3,
6,
7,
9,
12,
13,
16,
31,
64,
0x1000,
0x1001,
0x7FFF_FFFF,
0xFFFF_FFFA,
0xFFFF_FFFB,
0xFFFF_FFFC,
0xFFFF_FFFD,
0xFFFF_FFFE,
0xFFFF_FFFF,
]),
1 => any::<u32>(),
]
}
/// A [`CfbDamage`].
fn arb_cfb_damage() -> impl Strategy<Value = CfbDamage> {
let index = any::<prop::sample::Index>;
prop_oneof![
3 => (index(), arb_cfb_value()).prop_map(|(f, v)| CfbDamage::Header(f, v)),
3 => (index(), index(), arb_cfb_value()).prop_map(|(e, f, v)| CfbDamage::Entry(e, f, v)),
2 => (index(), arb_cfb_value()).prop_map(|(e, v)| CfbDamage::Fat(e, v)),
2 => (index(), arb_cfb_value()).prop_map(|(e, v)| CfbDamage::MiniFat(e, v)),
1 => (index(), any::<u8>()).prop_map(|(at, v)| CfbDamage::Byte(at, v)),
1 => index().prop_map(CfbDamage::Truncate),
]
}
/// Characters of a Word document's text: letters, marks of paragraphs, cells and breaks, fields,
/// pictures, hyphens and surrogate-free Unicode.
const DOC_CHARS: &str = "ab \r\u{7}\u{B}\u{C}\u{13}\u{14}\u{15}\u{1}\u{8}\t\u{1E}\u{1F}(é“✓";
/// Character and paragraph properties: Prls the Word 97-2003 reader knows, with operands in and out
/// of range, and raw bytes.
fn arb_grpprl() -> impl Strategy<Value = Vec<u8>> {
let prl = prop_oneof![
prop::sample::select(vec![
&[0x35, 0x08, 0x81][..],
&[0x35, 0x08, 0x80],
&[0x3C, 0x08, 0x01],
&[0x00, 0x08, 0x01],
&[0x01, 0x08, 0x01],
&[0x55, 0x08, 0x01],
&[0x30, 0x4A, 0x0A, 0x00],
&[0x30, 0x4A, 0xFF, 0xFF],
&[0x43, 0x4A, 0xFF, 0xFF],
&[0x4F, 0x4A, 0x07, 0x00],
&[0x42, 0x2A, 0x63],
&[0x70, 0x68, 1, 2, 3, 0xFF],
&[0x45, 0x48, 0x00, 0x80],
&[0x03, 0x24, 0x09],
&[0x61, 0x24, 0x08],
&[0x17, 0x24, 0x01],
&[0x4B, 0x24, 0x01],
&[0x41, 0x24, 0x01],
&[0x0F, 0x84, 0x00, 0x80],
&[0x13, 0xA4, 0xFF, 0xFF],
&[0x08, 0xD6, 0xFF, 0xFF],
&[0x15, 0xC6, 0xFF, 0x7F],
&[0x2A, 0xC6, 0xFF],
])
.prop_map(<[u8]>::to_vec),
prop::collection::vec(any::<u8>(), 0..4),
];
prop::collection::vec(prl, 0..6).prop_map(|p| p.concat())
}
/// A style sheet of up to four styles (paragraph or character, based on another or none) with
/// properties of [`arb_grpprl`], or empty slots.
fn arb_stsh() -> impl Strategy<Value = Vec<u8>> {
let style = prop::option::of((1u16..3, prop_oneof![Just(0x0FFFu16), 0u16..5], arb_grpprl(), arb_grpprl()));
prop::collection::vec(style, 0..5).prop_map(|styles| {
let words = |v: &[u16]| v.iter().flat_map(|w| w.to_le_bytes()).collect::<Vec<u8>>();
let mut stsh = words(&[18, styles.len() as u16, 10, 1, 0, 15, 0, 0, 0, 0]);
for s in styles {
let Some((stk, base, papx, chpx)) = s else {
stsh.extend([0, 0]);
continue;
};
let upx = |b: Vec<u8>| [words(&[b.len() as u16]), b.clone(), vec![0; b.len() % 2]].concat();
let (cupx, grlpupx) = if stk == 1 { (2, [upx([vec![0, 0], papx].concat()), upx(chpx)].concat()) } else { (1, upx(chpx)) };
let mut std = words(&[0x0FFE, stk | (base << 4), cupx | (0x0FFF << 4), 0, 0, 1, u16::from(b'S'), 0]);
std.extend(grlpupx);
stsh.extend((std.len() as u16).to_le_bytes());
stsh.extend(std);
}
stsh
})
}
/// The WordDocument and table streams of a Word 97-2003 document whose main text is `text` in one
/// piece, with one page of character properties `chpx` and one of paragraph properties `papx` of
/// style `istd` covering it, and the style sheet `stsh` and font table `ffn`.
fn word97(text: &str, compressed: bool, chpx: &[u8], istd: u16, papx: &[u8], stsh: &[u8], ffn: &[u8]) -> (Vec<u8>, Vec<u8>) {
let mut wd = vec![0u8; 1024];
let at = wd.len() as u32;
if compressed {
wd.extend(text.chars().map(|c| if c == '“' { 0x93 } else { u32::from(c).min(0xFF) as u8 }));
} else {
wd.extend(text.encode_utf16().flat_map(u16::to_le_bytes));
}
let ccp = text.encode_utf16().count() as u32;
let end = wd.len() as u32;
// One page each: a run of every character, its properties near the page's end.
let page = |props: Vec<u8>, papx: bool| {
let mut p = vec![0u8; 512];
p[0..4].copy_from_slice(&at.to_le_bytes());
p[4..8].copy_from_slice(&end.to_le_bytes());
let blob = if papx { [vec![0, (props.len() / 2) as u8], props].concat() } else { [vec![props.len() as u8], props].concat() };
let top = (511 - blob.len().min(400)) & !1;
let n = blob.len().min(511 - top);
p[top..top + n].copy_from_slice(&blob[..n]);
p[8] = (top / 2) as u8;
p[511] = 1;
p
};
let papx_props = [istd.to_le_bytes().to_vec(), papx.to_vec(), vec![0; papx.len() % 2]].concat();
wd.resize(wd.len().div_ceil(512) * 512, 0);
let chpx_page = (wd.len() / 512) as u32;
wd.extend(page(chpx.iter().copied().take(200).collect(), false));
wd.extend(page(papx_props.into_iter().take(400).collect(), true));
let bte = |pn: u32| [at, end, pn].into_iter().flat_map(u32::to_le_bytes).collect::<Vec<u8>>();
let fc = if compressed { (at * 2) | 0x4000_0000 } else { at };
let plc = [&0u32.to_le_bytes()[..], &ccp.to_le_bytes(), &[0, 0], &fc.to_le_bytes(), &[0, 0]].concat();
let clx = [&[0x02][..], &(plc.len() as u32).to_le_bytes(), &plc].concat();
let mut table = vec![];
let mut pairs = vec![(0u32, 0u32); 0x5D];
for (index, part) in [(1, stsh.to_vec()), (12, bte(chpx_page)), (13, bte(chpx_page + 1)), (15, ffn.to_vec()), (33, clx)] {
pairs[index] = (table.len() as u32, part.len() as u32);
table.extend(part);
}
let words = |v: &[u16]| v.iter().flat_map(|w| w.to_le_bytes()).collect::<Vec<u8>>();
let mut put = |at: usize, bytes: &[u8]| wd[at..at + bytes.len()].copy_from_slice(bytes);
put(0, &words(&[0xA5EC, 0x00C1]));
put(10, &words(&[0x0200, 0x00BF]));
put(32, &words(&[14]));
put(62, &words(&[22]));
put(76, &ccp.to_le_bytes());
put(80, &1u32.to_le_bytes());
put(152, &words(&[0x5D]));
for (i, (fc, lcb)) in pairs.iter().enumerate() {
put(154 + 8 * i, &[fc.to_le_bytes(), lcb.to_le_bytes()].concat());
}
(wd, table)
}
/// A well-formed paragraph, or a table row of two such paragraphs.
fn arb_docx_paragraph() -> impl Strategy<Value = String> {
prop_oneof![
3 => arb_docx_para(),
1 => (arb_docx_para(), arb_docx_para()).prop_map(|(a, b)| format!("<w:tbl><w:tr><w:tc>{a}</w:tc><w:tc>{b}</w:tc></w:tr></w:tbl>")),
]
}
/// A well-formed paragraph: properties and runs of empty elements and text.
fn arb_docx_para() -> impl Strategy<Value = String> {
let leaves = || prop::collection::vec(arb_docx_leaf(), 0..4);
(leaves(), prop::collection::vec((leaves(), arb_docx_leaf()), 0..6)).prop_map(|(ppr, runs)| {
let runs: String = runs.iter().map(|(rpr, t)| format!("<w:r><w:rPr>{}</w:rPr><w:t>{t}</w:t></w:r>", rpr.concat())).collect();
format!("<w:p><w:pPr>{}</w:pPr>{runs}</w:p>", ppr.concat())
})
}
/// Elements the Word document reader knows that hold others, opened or closed by
/// [`arb_docx_token`].
const DOCX_TAGS: &str = "p r t pPr rPr tbl tr tc sdt sdtContent hyperlink del ins style docDefaults rPrDefault pPrDefault";
/// Empty elements the Word document reader knows.
const DOCX_EMPTY: &str = "b i caps smallCaps strike dstrike vanish tab ptab br cr noBreakHyphen softHyphen bidi p";
/// Well-formed document defaults, or a paragraph or character style `S` or `C` (based on `S` or
/// not), with properties of empty elements.
fn arb_docx_style() -> impl Strategy<Value = String> {
let leaves = || prop::collection::vec(arb_docx_leaf(), 0..4).prop_map(|l| l.concat());
let kind = prop::sample::select(vec![("paragraph", "S"), ("character", "C"), ("paragraph", "Normal\" w:default=\"1")]);
prop_oneof![
(leaves(), leaves()).prop_map(|(r, p)| format!(
"<w:docDefaults><w:rPrDefault><w:rPr>{r}</w:rPr></w:rPrDefault><w:pPrDefault><w:pPr>{p}</w:pPr></w:pPrDefault></w:docDefaults>"
)),
(kind, any::<bool>(), leaves(), leaves()).prop_map(|((t, id), based, r, p)| {
let based = if based { "<w:basedOn w:val=\"S\"/>" } else { "" };
format!("<w:style w:type=\"{t}\" w:styleId=\"{id}\">{based}<w:rPr>{r}</w:rPr><w:pPr>{p}</w:pPr></w:style>")
}),
]
}
/// Pieces of WordprocessingML the Word document reader knows (paragraphs, runs, properties,
/// tables, fields, styles), a cut-off tag, entities and text.
fn arb_docx_token() -> impl Strategy<Value = String> {
let tags = prop::sample::select(DOCX_TAGS.split_whitespace().collect::<Vec<_>>());
let open = (tags, any::<bool>()).prop_map(|(t, end)| format!("<{}w:{t}>", if end { "/" } else { "" }));
let styles = prop::sample::select(vec!["<w:style w:type=\"paragraph\" w:styleId=\"S\">", "<w:style w:type=\"character\" w:styleId=\"C\">"]);
prop_oneof![open, styles.prop_map(str::to_string), arb_docx_leaf(), Just("<w:t".to_string())]
}
/// An empty WordprocessingML element the reader knows, with or without a value in or out of
/// range, entities or text.
fn arb_docx_leaf() -> impl Strategy<Value = String> {
let empty = prop::sample::select(DOCX_EMPTY.split_whitespace().collect::<Vec<_>>());
let valued = prop::sample::select(vec![
("pStyle", "val", "S"),
("rStyle", "val", "C"),
("basedOn", "val", "S"),
("sz", "val", "-7"),
("sz", "val", "1e400"),
("sz", "val", "12pt"),
("position", "val", "99999999999"),
("color", "val", "FF00"),
("color", "val", "auto"),
("u", "val", "none"),
("vertAlign", "val", "superscript"),
("jc", "val", "distribute"),
("jc", "val", "end"),
("ind", "hanging", "-360"),
("ind", "left", "3in"),
("spacing", "after", "x"),
("fldChar", "fldCharType", "begin"),
("fldChar", "fldCharType", "separate"),
("fldChar", "fldCharType", "end"),
("rFonts", "asciiTheme", "majorHAnsi"),
("rFonts", "ascii", ""),
("br", "type", "page"),
("b", "val", "0"),
]);
prop_oneof![
empty.prop_map(|e| format!("<w:{e}/>")),
valued.prop_map(|(e, a, v)| format!("<w:{e} w:{a}=\"{v}\"/>")),
Just("&amp;&#x1F600;&lt;".to_string()),
"[a-z ]{0,6}",
]
}
/// Control words the RTF reader knows, and one it doesn't.
const RTF_WORDS: &str = "par line pard plain b i ul ulnone fs f cf u uc qc qj li fi sb sa rtlpar cell row tab fonttbl colortbl red blue \
pict bin deff super sub up dn scaps caps ansicpg mac stylesheet upr ud zzunknown";
/// A piece of an RTF file: a brace, a control word the reader knows or doesn't (with a parameter
/// in or out of range, or none), a control symbol, text or raw bytes.
fn arb_rtf_token() -> impl Strategy<Value = Vec<u8>> {
let words = prop::sample::select(RTF_WORDS.split_whitespace().collect::<Vec<_>>());
let param = prop::option::of(prop_oneof![-70_000i64..70_000, Just(i64::from(i32::MAX)), Just(-99_999_999_999)]);
let word = |(w, p, space): (&str, Option<i64>, bool)| {
let p = p.map(|v| v.to_string()).unwrap_or_default();
format!("\\{w}{p}{}", if space { " " } else { "" }).into_bytes()
};
prop_oneof![
Just(b"{".to_vec()),
Just(b"}".to_vec()),
Just(b"\\*".to_vec()),
Just(b";".to_vec()),
(words, param, any::<bool>()).prop_map(word),
prop::sample::select(vec!["\\'e9", "\\'9", "\\~", "\\-", "\\_", "\\\\", "\\{", "\\}", "\\\r\n", "\\|"]).prop_map(|s| s.as_bytes().to_vec()),
"[a-z ]{0,6}".prop_map(String::into_bytes),
prop::collection::vec(any::<u8>(), 0..4),
]
}