mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 02:07:09 -06:00
File > Place read text files only as plain `.txt`, and an RTF file
could not be placed. The reference app lists RTF among the formats it
places as text, keeping the file's font and style settings.
File > Place now reads RTF files, written from the Rich Text Format
Specification 1.9.1, and sets their text as area type the way a text
file is placed. Each run keeps its font from the font table, its size,
bold and italic, underline, strikethrough, all caps, small caps,
superscript or subscript (in Document Setup's proportions), baseline
shift and its color from the color table. Each paragraph keeps its
alignment, left, right and first-line indents, space before and after,
and direction. Table cells are separated by tabs and each table row is
a paragraph. Unicode characters, Windows-1252 and Mac Roman bytes and
the special-character control words are read, and of a \upr group's
ANSI and Unicode text, the Unicode text is read. Hidden text, pictures,
objects, headers, footers, notes, field instructions and other
destinations are left out, and unknown control words are skipped.
A file is read as RTF when it starts with {\rtf, whatever its name,
except a .txt file, which stays plain text. RTF files share the 4 MiB
limit of text files. Groups nested more than 512 deep share the
formatting of the deepest one read, the font and color tables keep at
most 4,096 entries, and a file with more than 100,000 paragraphs or
runs is refused. The Place dialog lists RTF files, and file.place,
file.place.info and file.place.queue report the format `rtf`.
The tests in tests_textimport.rs fail without the change, and
text_import_fuzz.rs places random RTF token streams.
Docs: docs/file-format-parity.md, docs/mcp.md,
docs/target-app-parity.md.
88 lines
4.3 KiB
Rust
88 lines
4.3 KiB
Rust
//! 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.
|
||
//!
|
||
//! `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 _ = s.execute("file.place.info", &p);
|
||
let _ = 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 _ = s.execute("file.place.info", &p);
|
||
let _ = s.execute("file.place", &p);
|
||
});
|
||
prop_assert!(r.is_ok(), "panicked: {:?}", r.err());
|
||
}
|
||
}
|
||
|
||
/// 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),
|
||
]
|
||
}
|