mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
test(automation): give sign_document a real digital ID and document in the hostile-arguments sweep (#4277)
* test(automation): give sign_document a real digital ID and document in the hostile-arguments sweep sign_id_create, earlier in the sweep, wrote a digital ID to the in.pdf placeholder that sign_document then took as its id. Almost every sign_document call failed to decrypt it and never reached the hostile value under test, and the decrypts took most of the test's time. Each tool now starts from an empty root, and sign_document gets a test ID with its password and a fresh blank document per call. 370 of its 440 calls get past the ID (1 before) and 340 past the document (0 before); the sweep takes 7 s instead of 89 s. * test(automation): empty the sweep's root through its canonical path form_merge_data, given the hostile name " ", writes a file of that name into the root through the canonical \\?\ path, where Windows takes it literally. Emptying the root through the plain path dropped the trailing space, so the entry named the root itself and removing it deleted the root. Entries are now reached through the canonical path, their type is read from the directory entry, and a failed removal is reported. * test(automation): fail the sweep when the root can't be emptied Listing, typing or removing an entry left in the root now fails the test with the entry's path, so no tool runs against an earlier tool's files.
This commit is contained in:
@@ -190,6 +190,54 @@ fn guarded(a: &mut Automation, tool: &str, args: &Value) -> std::result::Result<
|
||||
}
|
||||
}
|
||||
|
||||
/// A new one-page blank document, by id.
|
||||
fn blank_doc(a: &mut Automation) -> u64 {
|
||||
let created = a.call("doc_create", &json!({ "from": "blank", "pages": 1 })).expect("create a blank document");
|
||||
created
|
||||
.into_iter()
|
||||
.find_map(|c| match c {
|
||||
pdfcraft_automation::Content::Json(v) => v["doc"].as_u64(),
|
||||
pdfcraft_automation::Content::Png { .. } => None,
|
||||
})
|
||||
.expect("the new document has an id")
|
||||
}
|
||||
|
||||
/// Delete everything inside `dir`, so one tool never sees files an earlier tool wrote: a digital ID
|
||||
/// that `sign_id_create` wrote to the `in.pdf` placeholder would stop every later `sign_document`
|
||||
/// call at its decrypt, before the argument under test.
|
||||
///
|
||||
/// The entries are reached through the canonical path, as the tools reach them: on Windows that
|
||||
/// is a `\\?\` path, whose names are taken literally. Through the plain path a file the tools
|
||||
/// named `" "` would lose its trailing space and name `dir` itself, which would then be deleted.
|
||||
fn empty_dir(dir: &Path) {
|
||||
let dir = dir.canonicalize().expect("the root exists");
|
||||
for entry in std::fs::read_dir(&dir).expect("read the root") {
|
||||
let entry = entry.unwrap_or_else(|e| panic!("list {}: {e}", dir.display()));
|
||||
let path = entry.path();
|
||||
let is_dir = entry.file_type().unwrap_or_else(|e| panic!("file type of {}: {e}", path.display())).is_dir();
|
||||
let removed = if is_dir { std::fs::remove_dir_all(&path) } else { std::fs::remove_file(&path) };
|
||||
removed.unwrap_or_else(|e| panic!("remove {} before the next tool: {e}", path.display()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Real values for the arguments a tool checks before the others, so a hostile value reaches the
|
||||
/// argument it is meant for instead of failing on a placeholder. `sign_document` checks its
|
||||
/// digital ID first and its document second: this ID opens with its password, and each call gets
|
||||
/// a fresh document (which the caller closes), since a successful call signs it. The ID is a test
|
||||
/// key with a low key derivation count, so the hundreds of calls that open it stay fast.
|
||||
fn fixture(a: &mut Automation, root: &Path, tool: &str) -> serde_json::Map<String, Value> {
|
||||
match tool {
|
||||
"sign_document" => {
|
||||
let id = Path::new(env!("CARGO_MANIFEST_DIR")).join("../sign/tests/data/ec-p256.p12");
|
||||
std::fs::copy(&id, root.join("id.p12")).expect("copy the test digital ID");
|
||||
let doc = blank_doc(a);
|
||||
let args = json!({ "doc": doc, "id": "id.p12", "password": "test", "out": "signed.pdf" });
|
||||
args.as_object().cloned().unwrap_or_default()
|
||||
}
|
||||
_ => serde_json::Map::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_tool_panics_on_hostile_arguments() {
|
||||
let root = tmp("args");
|
||||
@@ -199,6 +247,7 @@ fn no_tool_panics_on_hostile_arguments() {
|
||||
let mut calls = 0usize;
|
||||
|
||||
for def in tools() {
|
||||
empty_dir(&root);
|
||||
// The whole arguments object, not just its properties.
|
||||
for bad in [json!(null), json!([]), json!("string"), json!(7), json!(true), json!({})] {
|
||||
calls += 1;
|
||||
@@ -210,13 +259,24 @@ fn no_tool_panics_on_hostile_arguments() {
|
||||
for prop in &props {
|
||||
for value in &values {
|
||||
let mut obj = plausible(&def);
|
||||
let fixed = fixture(&mut a, &root, def.name);
|
||||
let fixture_doc = fixed.get("doc").cloned();
|
||||
obj.extend(fixed);
|
||||
obj.insert(prop.clone(), value.clone());
|
||||
calls += 1;
|
||||
if let Err(e) = guarded(&mut a, def.name, &Value::Object(obj)) {
|
||||
failures.push(e);
|
||||
}
|
||||
// Later tools pass placeholder document ids, which must not find this one open.
|
||||
if let Some(doc) = fixture_doc {
|
||||
a.call("doc_close", &json!({ "doc": doc, "discard_changes": true })).expect("close the fixture's document");
|
||||
}
|
||||
}
|
||||
}
|
||||
// A signed file shows the fixture got calls past the ID and document checks.
|
||||
if def.name == "sign_document" {
|
||||
assert!(root.join("signed.pdf").is_file(), "no sign_document call signed: the hostile values stop at the ID or document");
|
||||
}
|
||||
}
|
||||
|
||||
eprintln!("hostile arguments: {calls} calls over {} tools, {} panics", tools().len(), failures.len());
|
||||
@@ -256,14 +316,7 @@ fn no_tool_escapes_the_root() {
|
||||
let mut a = Automation::new().with_root(&root).expect("root");
|
||||
|
||||
// A real document, so tools reach their path arguments instead of failing on `doc` first.
|
||||
let created = a.call("doc_create", &json!({ "from": "blank", "pages": 1 })).expect("create a blank document");
|
||||
let doc = created
|
||||
.into_iter()
|
||||
.find_map(|c| match c {
|
||||
pdfcraft_automation::Content::Json(v) => v["doc"].as_u64(),
|
||||
pdfcraft_automation::Content::Png { .. } => None,
|
||||
})
|
||||
.expect("the new document has an id");
|
||||
let doc = blank_doc(&mut a);
|
||||
|
||||
let mut leaked = Vec::new();
|
||||
let mut tried = 0usize;
|
||||
|
||||
Reference in New Issue
Block a user