From 9ee25b1561b57644cc9f91e557b76f10f8e2bbac Mon Sep 17 00:00:00 2001 From: Zaxaerith Date: Fri, 30 Oct 2026 11:52:30 +0800 Subject: [PATCH 1/2] fix(fonts): find document fonts with bare file paths (#2014) --- ROADMAP.md | 1 + crates/engine/src/cmd/file/open.rs | 71 ++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/ROADMAP.md b/ROADMAP.md index ad3f52a14..5bcc7b176 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -12,6 +12,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, open (documented JSON format + IDML), scriptable by agents (MCP), and available on the web. +- **2026-10-30** · Document Fonts lookup treats a bare document filename's empty parent as the current directory (#2014), preserving case-insensitive discovery and automation read-root checks. No readiness percentage change. - **2026-10-28** · Inline table rows leave room for column and spanning footnotes, continuing in the next column or frame when needed (#1901). Cell-local footnotes contribute to row height and sit inside the cell rather than outside its measuring frame. Vertical justification keeps diff --git a/crates/engine/src/cmd/file/open.rs b/crates/engine/src/cmd/file/open.rs index 75a7ce627..c7e8264ba 100644 --- a/crates/engine/src/cmd/file/open.rs +++ b/crates/engine/src/cmd/file/open.rs @@ -128,6 +128,8 @@ pub(crate) fn load_document_fonts(d: &mut Document, path: &str) -> (Option std::path::PathBuf { use designcraft_fonts::DOCUMENT_FONTS_FOLDER; + // A bare file name has an empty parent; directory traversal needs an explicit `.`. + let dir = if dir.as_os_str().is_empty() { std::path::Path::new(".") } else { dir }; let exact = dir.join(DOCUMENT_FONTS_FOLDER); if crate::io::is_dir(&exact) { return exact; @@ -286,6 +288,75 @@ mod document_fonts_tests { let _ = std::fs::remove_dir_all(&dir); } + #[cfg(not(target_arch = "wasm32"))] + #[test] + fn document_fonts_are_found_for_bare_and_explicit_paths() { + // Only the child changes its working directory; parallel tests keep theirs. + let dir = tempfile::tempdir().unwrap(); + let output = std::process::Command::new(std::env::current_exe().unwrap()) + .args(["--exact", "cmd::file::open::document_fonts_tests::document_font_paths_child", "--ignored", "--nocapture"]) + .env("DESIGNCRAFT_TEST_DOCUMENT_FONT_PATHS", "1") + .current_dir(dir.path()) + .output() + .unwrap(); + assert!(output.status.success(), "child failed: {}{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr)); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("test result: ok. 1 passed; 0 failed;"), "the subprocess helper must run exactly once: {stdout}"); + } + + #[cfg(not(target_arch = "wasm32"))] + #[test] + #[ignore = "subprocess helper, executed by document_fonts_are_found_for_bare_and_explicit_paths"] + fn document_font_paths_child() { + const FAMILY: &str = "DocFont Bare Path Test"; + assert!(std::env::var_os("DESIGNCRAFT_TEST_DOCUMENT_FONT_PATHS").is_some(), "run through the parent test"); + let cwd = std::env::current_dir().unwrap(); + std::fs::create_dir("nested").unwrap(); + std::fs::create_dir("other").unwrap(); + for extension in ["idml", "designcraft"] { + let path = PathBuf::from(format!("t.{extension}")); + write_document(&cwd.join(&path), FAMILY); + write_document(&cwd.join("other").join(&path), FAMILY); + assert_eq!(open(&mut Session::new(), &path)["documentFonts"], 0, "no font folder: {path:?}"); + } + + let roots = crate::file_access::AutomationRoots::new(&[cwd.as_path()], &[]).unwrap(); + for folder in ["Document fonts", DOCUMENT_FONTS_FOLDER] { + std::fs::create_dir_all(folder).unwrap(); + std::fs::write(Path::new(folder).join("case.ttf"), font_with(FAMILY, &['H', 'e', 'l', 'o']).unwrap()).unwrap(); + if folder == DOCUMENT_FONTS_FOLDER { + // Both spellings exist on case-sensitive file systems; the exact name still wins. + assert_eq!(super::document_fonts_folder(Path::new("")), Path::new(".").join(DOCUMENT_FONTS_FOLDER)); + } + for extension in ["idml", "designcraft"] { + let name = format!("t.{extension}"); + for path in [PathBuf::from(&name), Path::new(".").join(&name), Path::new("nested/..").join(&name), cwd.join(&name)] { + let mut s = Session::new(); + let r = crate::file_access::confine(roots.as_ref(), || open(&mut s, &path)); + assert_eq!(r["documentFonts"], 1, "{folder}, {path:?}: {r}"); + let f = font_entry(&mut s, FAMILY); + assert_eq!((f["missing"].clone(), f["source"].clone()), (json!(false), json!("document")), "{path:?}: {f}"); + } + } + } + + // Opening a second document without the folder must not inherit the first one's font. + let mut s = Session::new(); + assert_eq!(open(&mut s, Path::new("t.idml"))["documentFonts"], 1); + assert_eq!(open(&mut s, Path::new("other/t.designcraft"))["documentFonts"], 0); + assert_eq!(font_entry(&mut s, FAMILY)["missing"], true); + s.execute("file.activate", &json!({"index": 0})).unwrap(); + assert_eq!(font_entry(&mut s, FAMILY)["source"], "document"); + + // Normalising the directory must not grant access outside the automation read root. + let restricted = crate::file_access::AutomationRoots::new(&[Path::new("other")], &[]).unwrap(); + let mut doc = s.doc().unwrap().doc.clone(); + let (scope, faces, _) = crate::file_access::confine(restricted.as_ref(), || super::load_document_fonts(&mut doc, "t.idml")); + assert!(scope.is_none()); + assert_eq!(faces, 0); + assert_eq!(doc.font_scope, 0); + } + #[test] fn a_document_without_the_folder_opens_as_before() { let dir = std::env::temp_dir().join(format!("dc-open-docfonts-none-{}", std::process::id())); From 1e3c5fb0ef9c22d3908f7526db4f8b4ae58f2e18 Mon Sep 17 00:00:00 2001 From: aiongg <67543387+aiongg@users.noreply.github.com> Date: Tue, 3 Nov 2026 17:56:11 +0000 Subject: [PATCH 2/2] Apply review fixes - crates/engine/src/cmd/file/open.rs --- crates/engine/src/cmd/file/open.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/engine/src/cmd/file/open.rs b/crates/engine/src/cmd/file/open.rs index c7e8264ba..ab90f7e95 100644 --- a/crates/engine/src/cmd/file/open.rs +++ b/crates/engine/src/cmd/file/open.rs @@ -350,7 +350,7 @@ mod document_fonts_tests { // Normalising the directory must not grant access outside the automation read root. let restricted = crate::file_access::AutomationRoots::new(&[Path::new("other")], &[]).unwrap(); - let mut doc = s.doc().unwrap().doc.clone(); + let mut doc = s.doc().unwrap().doc.as_ref().clone(); let (scope, faces, _) = crate::file_access::confine(restricted.as_ref(), || super::load_document_fonts(&mut doc, "t.idml")); assert!(scope.is_none()); assert_eq!(faces, 0);