From 4c2296491eddb85909afd563c8b189c0be34099b Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Mon, 2 Nov 2026 23:08:25 -0500 Subject: [PATCH] Links: key the image check cache on the resolved file, not the link text A relative link names another file once the document is saved elsewhere, so a status remembered for the old file must not answer for the new one. Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/cmd/links.rs | 16 ++++++++-------- crates/engine/src/lib.rs | 3 ++- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/crates/engine/src/cmd/links.rs b/crates/engine/src/cmd/links.rs index 1ad60b30f..ec7bf6ba8 100644 --- a/crates/engine/src/cmd/links.rs +++ b/crates/engine/src/cmd/links.rs @@ -155,13 +155,13 @@ pub fn link_missing(a: &designcraft_doc::Asset, document: Option<&Path>) -> bool } } -/// Link status of an asset in the document at `document`. Only a regular file is compared, so a -/// link to a device (`/dev/zero`) or a FIFO is missing rather than read. -/// Linked images checked this session: (asset, the identity of its bytes, link) → (the file's size -/// and time when it was read, whether it matched the bytes). The bytes' identity (their `Arc`'s -/// address, as the render cache keys them) changes with every relink, update or undo, so a -/// remembered answer can never describe other bytes. -pub(crate) type ImageChecks = std::collections::HashMap<(AssetId, usize, String), (Fingerprint, bool)>; +/// Linked images checked this session: (asset, the identity of its bytes, the file its link +/// resolves to) → (the file's size and time when it was read, whether it matched the bytes). The +/// resolved file, not the link text, is the key: a relative link names another file once the +/// document is saved elsewhere. The bytes' identity (their `Arc`'s address, as the render cache +/// keys them) changes with every relink, update or undo, so a remembered answer can never +/// describe other bytes. +pub(crate) type ImageChecks = std::collections::HashMap<(AssetId, usize, std::path::PathBuf), (Fingerprint, bool)>; /// Link status of a placed graphic (`ok`, `modified`, `missing`, `embedded`), reading the file /// only when its size is the asset's and its time isn't the one remembered from the last read @@ -188,7 +188,7 @@ pub(crate) fn status_cached(a: &designcraft_doc::Asset, document: Option<&Path>, if now.size != a.data.len() as u64 { return "modified"; } - let key = (a.id, Arc::as_ptr(&a.data) as usize, link.clone()); + let key = (a.id, Arc::as_ptr(&a.data) as usize, path.clone()); if let Some((at, same)) = checks.get(&key) && *at == now { diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index aee87f2ee..d0922e484 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -437,7 +437,8 @@ pub struct Session { pub(crate) text_link_checks: std::collections::HashMap, /// Linked images checked against their files this session (`links.list` polls twice a second): /// a file of the recorded size and time isn't read again. - pub(crate) image_link_checks: std::collections::HashMap<(designcraft_doc::AssetId, usize, String), (designcraft_doc::Fingerprint, bool)>, + pub(crate) image_link_checks: + std::collections::HashMap<(designcraft_doc::AssetId, usize, std::path::PathBuf), (designcraft_doc::Fingerprint, bool)>, } impl Default for Session {