M6: keep the relink regression off the file guard's path

The workspace file guard scans test modules too when they are declared `pub(crate) mod` (it only
skips a plain `mod`), so a temp file written inside pdflayers.rs' tests failed
no_crate_touches_the_file_system_outside_the_checked_readers. The unit test now builds the state a
relink leaves behind - layer fields on an item whose asset is an image - and asserts the refusal
predicate ignores it while all three exports run. The user path (a real links.relink to a PNG) moves
to tests/pdf_optional_content.rs, where the guard exempts fixtures written by test files.
This commit is contained in:
skylar taylor-barrick
2026-10-24 17:05:13 -06:00
parent fa741a157d
commit ba8936e061
2 changed files with 46 additions and 17 deletions
+9 -17
View File
@@ -590,36 +590,28 @@ pub(crate) mod tests {
/// Relinking a placed PDF to an image leaves the item's layer fields behind. They are no longer a
/// placed-PDF choice, so they must not block an export: the refusal guard asks for a graphic whose
/// asset is a PDF, the same thing the export walks ask for.
/// asset is a PDF, the same thing the export walks ask for. The user path (a real relink to a PNG)
/// is covered in `tests/pdf_optional_content.rs`, where the file guard allows a test fixture on disk.
#[test]
fn a_stale_choice_on_a_relinked_image_does_not_block_exports() {
let pdf = designcraft_render::pdf_hide_layers(&two_layer_pdf(), &["Red".into()]).unwrap();
fn a_stale_choice_on_a_non_pdf_placement_does_not_block_exports() {
let png = designcraft_render::Rendered { width: 4, height: 3, pixels: [30, 90, 180, 255].repeat(12) }.to_png();
let mut s = Session::new();
s.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
let id = s
.execute("file.place", &json!({"base64": super::super::file::base64_encode(&pdf), "name": "art.pdf", "x": 0, "y": 0, "width": 100}))
.execute("file.place", &json!({"base64": super::super::file::base64_encode(&png), "name": "photo.png", "x": 0, "y": 0, "width": 100}))
.unwrap()["id"]
.clone();
s.execute("object.layerOptions", &json!({"ids": [id], "hidden": ["Red"]})).unwrap();
let asset = s.doc().unwrap().doc.item(designcraft_doc::ItemId(id.as_u64().unwrap())).unwrap().graphic().unwrap().asset;
let png = designcraft_render::Rendered { width: 4, height: 3, pixels: [30, 90, 180, 255].repeat(12) }.to_png();
let path = std::env::temp_dir().join(format!(
"dc-relinked-{}-{}.png",
std::process::id(),
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos()
));
std::fs::write(&path, &png).unwrap();
s.execute("links.relink", &json!({"asset": asset.0, "path": path.to_string_lossy()})).unwrap();
let item = designcraft_doc::ItemId(id.as_u64().unwrap());
// The state a relink from a PDF leaves behind: fields set, asset no longer a PDF.
std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc).item_mut(item).unwrap().pdf_hidden_layers = vec!["Red".into()];
let d = &s.doc().unwrap().doc;
assert!(!designcraft_render::is_pdf(&d.assets[&asset].data), "the placement is an image now");
assert!(d.item(designcraft_doc::ItemId(id.as_u64().unwrap())).unwrap().has_pdf_layer_override(), "the stale field is still on the item");
assert!(d.item(item).unwrap().has_pdf_layer_override(), "the stale field is on the item");
assert!(d.has_pdf_layer_overrides(), "the field is there");
assert!(!super::super::interchange::has_applicable_pdf_layer_overrides(d), "but it is not a placed-PDF choice");
// Both exports still run: nothing is dropped, and there is no choice to refuse.
s.execute("file.exportIdml", &json!({})).unwrap();
s.execute("file.exportPdf", &json!({"standard": "a2b"})).unwrap();
s.execute("file.exportPdf", &json!({})).unwrap();
std::fs::remove_file(&path).ok();
}
#[test]
@@ -14,6 +14,43 @@ use support::{catalog, document, fixture, painted};
const DEFAULT: &str = "/BaseState/ON/OFF[6 3 R]";
const USAGE_APPLICATIONS: &str = "/BaseState/ON/ON[5 0 R]/OFF[6 3 R]/AS[<</Event/View/Category[/View]/OCGs[5 0 R 6 3 R]>><</Event/Print/Category[/Print]/OCGs[5 0 R 6 3 R]>><</Event/Export/Category[/Export]/OCGs[5 0 R 6 3 R]>>]";
/// The user path behind the stale-choice guard: place a PDF, hide a layer, relink that asset to a
/// PNG (links.relink replaces the asset in place, so the item keeps its layer fields), then export.
/// The fields are no longer a placed-PDF choice, so nothing is refused.
#[test]
fn relinking_a_layered_pdf_to_an_image_still_exports() {
let source = fixture(DEFAULT, "Hidden");
let pdf_path = std::env::temp_dir().join(format!(
"dc-layered-{}-{}.pdf",
std::process::id(),
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos()
));
std::fs::write(&pdf_path, &source).unwrap();
let mut session = designcraft_engine::Session::new();
session.execute("file.new", &serde_json::json!({"width": 200, "height": 200})).unwrap();
let placed = session.execute("file.place", &serde_json::json!({"path": pdf_path.to_string_lossy(), "x": 0, "y": 0, "width": 100})).unwrap();
let id = designcraft_doc::ItemId(placed["id"].as_u64().unwrap());
session.execute("object.layerOptions", &serde_json::json!({"ids": [placed["id"]], "hidden": ["Hidden"]})).unwrap();
let asset = session.doc().unwrap().doc.item(id).unwrap().graphic().unwrap().asset;
let png = designcraft_render::Rendered { width: 4, height: 3, pixels: [30, 90, 180, 255].repeat(12) }.to_png();
let path = std::env::temp_dir().join(format!(
"dc-relinked-{}-{}.png",
std::process::id(),
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos()
));
std::fs::write(&path, &png).unwrap();
session.execute("links.relink", &serde_json::json!({"asset": asset.0, "path": path.to_string_lossy()})).unwrap();
let doc = session.doc().unwrap().doc.clone();
assert!(!designcraft_render::is_pdf(&doc.assets[&asset].data), "the placement is an image now");
assert!(doc.item(id).unwrap().has_pdf_layer_override(), "the stale fields are still on the item");
// All three exports still work: the field is not a placed-PDF choice any more.
session.execute("file.exportIdml", &serde_json::json!({})).unwrap();
session.execute("file.exportPdf", &serde_json::json!({"standard": "a2b"})).unwrap();
session.execute("file.exportPdf", &serde_json::json!({})).unwrap();
std::fs::remove_file(&path).ok();
std::fs::remove_file(&pdf_path).ok();
}
#[test]
fn source_layer_dictionaries_are_readable() {
let pdf = hayro_syntax::Pdf::new(fixture(DEFAULT, "Hidden")).unwrap();