From 650ae383f27d0669d07bf383f42d9fe09de88989 Mon Sep 17 00:00:00 2001 From: V1dalTheAbidal <97257872+V1dalTheAbidal@users.noreply.github.com> Date: Wed, 11 Nov 2026 01:32:16 +0200 Subject: [PATCH 1/6] Block Editor: BSAVEAS keeps the source block's last saved definition After BSAVE the editor's block table can still hold the older source definition. Restore the saved source block from the host when composing a copy and when reconciling the editor and its history, so a later save of the copy keeps it. --- crates/engine/src/blockedit.rs | 21 +++++++--------- crates/engine/tests/bsaveas_source.rs | 35 +++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 12 deletions(-) create mode 100644 crates/engine/tests/bsaveas_source.rs diff --git a/crates/engine/src/blockedit.rs b/crates/engine/src/blockedit.rs index 89da207f0..8edfee34a 100644 --- a/crates/engine/src/blockedit.rs +++ b/crates/engine/src/blockedit.rs @@ -144,10 +144,10 @@ impl BlockEdit { } let host = &self.host.doc; let mut d = editor.clone(); - // When the block is saved under another name, the edited block keeps the drawing's - // description and block `name` gets the description changed in the editor. + // When saving a copy, the source keeps its last saved definition. The editor's own + // table entry can still be the one from before BSAVE; the copy gets the edited data. if !name.eq_ignore_ascii_case(&self.name) { - keep_description(&mut d, host, &self.name); + restore_saved_definition(&mut d, host, &self.name); } d.model = host.model.clone(); d.layouts = host.layouts.clone(); @@ -176,16 +176,13 @@ impl BlockEdit { } } -/// Give block `name` of `d` the description `host` has for it, or, when `host` has no such block, -/// drop the empty definition a description gives a block the editor started and never saved. -fn keep_description(d: &mut Drawing, host: &Drawing, name: &str) { +/// Keep the source block's saved objects and metadata when the edited objects are saved as a copy. +/// Without a saved source, drop the empty definition an editor description may have created. +fn restore_saved_definition(d: &mut Drawing, host: &Drawing, name: &str) { match host.block(name) { Some(h) => { - if let Some((_, b)) = d.blocks.iter_mut().find(|(k, _)| k.eq_ignore_ascii_case(name)) - && b.description != h.description - { - Arc::make_mut(b).description = h.description.clone(); - } + let key = d.blocks.keys().find(|key| key.eq_ignore_ascii_case(name)).cloned().unwrap_or_else(|| h.name.clone()); + d.blocks.insert(key, h.clone()); } None => d.blocks.retain(|k, b| !(k.eq_ignore_ascii_case(name) && b.entities.is_empty())), } @@ -302,7 +299,7 @@ impl DocState { let was = be.name.clone(); let reconcile = |doc: &mut Arc| { let editor = Arc::make_mut(doc); - keep_description(editor, &d, &was); + restore_saved_definition(editor, &d, &was); editor.blocks.retain(|k, _| !k.eq_ignore_ascii_case(&name)); editor.blocks.insert(name.clone(), b.clone()); }; diff --git a/crates/engine/tests/bsaveas_source.rs b/crates/engine/tests/bsaveas_source.rs new file mode 100644 index 000000000..b449c5d1b --- /dev/null +++ b/crates/engine/tests/bsaveas_source.rs @@ -0,0 +1,35 @@ +//! Saving an edited block as a copy must retain its source's last saved definition. + +use cadcraft_engine::Session; +use serde_json::json; + +#[test] +fn save_as_preserves_the_last_saved_source_definition() { + let mut s = Session::new(); + let mut handles = Vec::new(); + for y in [5, 9] { + let line = s.execute("line", &json!({ "points": [[5, y], [15, y]] })).unwrap(); + handles.push(line["handles"][0].clone()); + } + s.execute("block", &json!({ "name": "Frame", "base": [5, 5], "handles": handles, "keep": "delete" })).unwrap(); + s.execute("insert", &json!({ "name": "Frame", "at": [100, 20] })).unwrap(); + s.execute("bedit", &json!({ "name": "Frame" })).unwrap(); + assert_eq!(s.doc().unwrap().model.len(), 2); + + s.execute("line", &json!({ "points": [[0, 3], [3, 3]] })).unwrap(); + s.execute("bsave", &json!({})).unwrap(); + let saved_source = s.state().unwrap().drawing().block("Frame").unwrap().as_ref().clone(); + assert_eq!(saved_source.entities.len(), 3); + s.execute("bsaveas", &json!({ "name": "Copy" })).unwrap(); + assert_eq!(s.state().unwrap().block_editor(), Some("Copy")); + assert_eq!(s.state().unwrap().drawing().block("Frame").unwrap().as_ref(), &saved_source, "copy composition keeps the saved source"); + + // A later save of Copy must not reintroduce the editor table's older source definition. + s.execute("line", &json!({ "points": [[0, 6], [4, 6]] })).unwrap(); + s.execute("bsave", &json!({})).unwrap(); + s.execute("bclose", &json!({ "save": false })).unwrap(); + assert_eq!(s.doc().unwrap().block("Frame").unwrap().as_ref(), &saved_source, "editor reconciliation keeps the saved source"); + assert_eq!(s.doc().unwrap().block("Copy").unwrap().entities.len(), 4, "the copy keeps its own later edit"); + s.execute("bedit", &json!({ "name": "Frame" })).unwrap(); + assert_eq!(s.doc().unwrap().model.len(), 3); +} From f132d816091a417a03bd8018529c8d7ce55df65d Mon Sep 17 00:00:00 2001 From: V1dalTheAbidal <97257872+V1dalTheAbidal@users.noreply.github.com> Date: Wed, 11 Nov 2026 11:14:22 +0200 Subject: [PATCH 2/6] Document the BSAVEAS source test --- crates/engine/tests/bsaveas_source.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/engine/tests/bsaveas_source.rs b/crates/engine/tests/bsaveas_source.rs index b449c5d1b..424b2bcee 100644 --- a/crates/engine/tests/bsaveas_source.rs +++ b/crates/engine/tests/bsaveas_source.rs @@ -3,6 +3,8 @@ use cadcraft_engine::Session; use serde_json::json; +/// Add a third line to a block and BSAVE it, BSAVEAS a copy, then edit and save the copy: the +/// source block keeps the definition of its own last save. #[test] fn save_as_preserves_the_last_saved_source_definition() { let mut s = Session::new(); From ecb362aa558d217e8b51e182d1b1eb07a8d3ea48 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Wed, 11 Nov 2026 12:09:19 -0500 Subject: [PATCH 3/6] ci: run Harden-Runner on every job (#3058) Adds step-security/harden-runner (audit, pinned to v2.22.4 like craft-common-actions) as the first step of each job that did not have it, so every workflow in the org's public repos reports its egress to StepSecurity. Audit, not block: block-mode allow lists come from each job's observed traffic. --- .github/workflows/nix.yml | 4 ++++ .github/workflows/packaging-lint.yml | 4 ++++ .github/workflows/release.yml | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 318f6bba3..8f118327b 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -27,6 +27,10 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 steps: + - name: Harden runner + uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4 + with: + egress-policy: audit - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Git dependencies come from storytold run: | diff --git a/.github/workflows/packaging-lint.yml b/.github/workflows/packaging-lint.yml index ea61d8430..1771a35fe 100644 --- a/.github/workflows/packaging-lint.yml +++ b/.github/workflows/packaging-lint.yml @@ -27,6 +27,10 @@ jobs: name: Windows installation policy (no app build) runs-on: windows-latest steps: + - name: Harden runner + uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4 + with: + egress-policy: audit - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a0ae76e01..9b3201785 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,6 +30,10 @@ jobs: permissions: contents: read steps: + - name: Harden runner + uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4 + with: + egress-policy: audit - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Git dependencies come from storytold run: | From 20b4472f0bd8b0d9c72c5fec8b48de66b8de0eb1 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Wed, 11 Nov 2026 12:43:30 -0500 Subject: [PATCH 4/6] Updates: take craft-update from craft-libs main at 6e03f65bf --- Cargo.lock | 3 ++- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 148dd3705..e274b357a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1214,7 +1214,7 @@ dependencies = [ [[package]] name = "craft-update" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=1b34eba41e62138fc2438d2734523b302840b45d#1b34eba41e62138fc2438d2734523b302840b45d" +source = "git+https://github.com/storytold/craft-libs?rev=6e03f65bf45181db77ba6229716e6624db9eb25b#6e03f65bf45181db77ba6229716e6624db9eb25b" dependencies = [ "chrono", "dunce", @@ -1224,6 +1224,7 @@ dependencies = [ "image", "log", "plist", + "rustix 1.1.5", "semver", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index e7241a93e..2b7a8fefb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,7 +71,7 @@ egui-wgpu = "0.37" # Monotonic time on native and web (performance.now on wasm). web-time = "1.1" # craft-update: app-specific pretend versions apply to startup and manual checks. -craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "1b34eba41e62138fc2438d2734523b302840b45d", features = ["egui"] } +craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "6e03f65bf45181db77ba6229716e6624db9eb25b", features = ["egui"] } rfd = "0.17" arboard = { version = "3.6", default-features = false } From dc7f382cecb4d2f0e3930ac41565c886a5df2086 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Wed, 11 Nov 2026 14:24:01 -0500 Subject: [PATCH 5/6] Fix Flatpak permissions for update checks and self-updates (#3075) * fix(flatpak): allow release checks and self-update helpers * test: keep Flatpak permission parsing within strict Clippy rules --- crates/ui-egui/tests/flatpak_updates.rs | 27 +++++++++++++++++++ .../ai.storyteller.cadcraft.bundle.yml | 2 ++ .../linux/flatpak/ai.storyteller.cadcraft.yml | 2 ++ 3 files changed, 31 insertions(+) create mode 100644 crates/ui-egui/tests/flatpak_updates.rs diff --git a/crates/ui-egui/tests/flatpak_updates.rs b/crates/ui-egui/tests/flatpak_updates.rs new file mode 100644 index 000000000..1362e8148 --- /dev/null +++ b/crates/ui-egui/tests/flatpak_updates.rs @@ -0,0 +1,27 @@ +//! Release and source Flatpaks must allow release checks and host update helpers. +fn finish_args(manifest: &str) -> Vec<&str> { + manifest + .split("finish-args:") + .nth(1) + .unwrap_or_default() + .lines() + .take_while(|line| line.is_empty() || line.starts_with(' ') || line.starts_with('#')) + .filter_map(|line| line.trim().strip_prefix("- ")) + .map(|arg| arg.trim_matches(['\'', '"'])) + .collect() +} + +#[test] +fn flatpak_update_permissions() { + let manifests = [ + include_str!("../../../packaging/linux/flatpak/ai.storyteller.cadcraft.bundle.yml"), + include_str!("../../../packaging/linux/flatpak/ai.storyteller.cadcraft.yml"), + ]; + let args = manifests.map(finish_args); + assert_eq!(args[0], args[1], "source and release permissions must agree"); + for permissions in args { + for required in ["--share=network", "--talk-name=org.freedesktop.Flatpak"] { + assert!(permissions.contains(&required), "missing update permission: {required}"); + } + } +} diff --git a/packaging/linux/flatpak/ai.storyteller.cadcraft.bundle.yml b/packaging/linux/flatpak/ai.storyteller.cadcraft.bundle.yml index 448ba058d..118f11e49 100644 --- a/packaging/linux/flatpak/ai.storyteller.cadcraft.bundle.yml +++ b/packaging/linux/flatpak/ai.storyteller.cadcraft.bundle.yml @@ -17,6 +17,8 @@ runtime-version: '26.08' sdk: org.freedesktop.Sdk command: cadcraft finish-args: + # The shared updater inspects and updates this installation via flatpak-spawn --host. + - --talk-name=org.freedesktop.Flatpak - --filesystem=xdg-data/ArtCraft/versions:ro # Display: Wayland first; X11 only when no Wayland compositor is running. - --socket=wayland diff --git a/packaging/linux/flatpak/ai.storyteller.cadcraft.yml b/packaging/linux/flatpak/ai.storyteller.cadcraft.yml index e4b19cb81..08ccdffde 100644 --- a/packaging/linux/flatpak/ai.storyteller.cadcraft.yml +++ b/packaging/linux/flatpak/ai.storyteller.cadcraft.yml @@ -18,6 +18,8 @@ sdk-extensions: - org.freedesktop.Sdk.Extension.rust-stable command: cadcraft finish-args: + # The shared updater inspects and updates this installation via flatpak-spawn --host. + - --talk-name=org.freedesktop.Flatpak - --filesystem=xdg-data/ArtCraft/versions:ro # Display: Wayland first; X11 only when no Wayland compositor is running. - --socket=wayland From e1d379ad9fbf8f907c7c3ef1cd674a37ee04f60d Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Wed, 11 Nov 2026 14:55:43 -0500 Subject: [PATCH 6/6] chore: bump version to 0.20.1 for the Flatpak update fix (#3084) --- Cargo.lock | 32 ++++++++++++++++---------------- Cargo.toml | 2 +- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e274b357a..71523bae0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -614,7 +614,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadcraft" -version = "0.20.0" +version = "0.20.1" dependencies = [ "cadcraft-engine", "cadcraft-io", @@ -645,7 +645,7 @@ dependencies = [ [[package]] name = "cadcraft-cli" -version = "0.20.0" +version = "0.20.1" dependencies = [ "cadcraft-engine", "cadcraft-io", @@ -661,14 +661,14 @@ dependencies = [ [[package]] name = "cadcraft-color" -version = "0.20.0" +version = "0.20.1" dependencies = [ "serde", ] [[package]] name = "cadcraft-constraints" -version = "0.20.0" +version = "0.20.1" dependencies = [ "cadcraft-doc", "cadcraft-geom", @@ -678,7 +678,7 @@ dependencies = [ [[package]] name = "cadcraft-doc" -version = "0.20.0" +version = "0.20.1" dependencies = [ "cadcraft-color", "cadcraft-geom", @@ -689,7 +689,7 @@ dependencies = [ [[package]] name = "cadcraft-dwg" -version = "0.20.0" +version = "0.20.1" dependencies = [ "acadrust", "log", @@ -697,7 +697,7 @@ dependencies = [ [[package]] name = "cadcraft-dxf" -version = "0.20.0" +version = "0.20.1" dependencies = [ "encoding_rs", "log", @@ -706,7 +706,7 @@ dependencies = [ [[package]] name = "cadcraft-engine" -version = "0.20.0" +version = "0.20.1" dependencies = [ "cadcraft-color", "cadcraft-constraints", @@ -727,7 +727,7 @@ dependencies = [ [[package]] name = "cadcraft-fonts" -version = "0.20.0" +version = "0.20.1" dependencies = [ "cadcraft-dxf", "cadcraft-geom", @@ -740,14 +740,14 @@ dependencies = [ [[package]] name = "cadcraft-geom" -version = "0.20.0" +version = "0.20.1" dependencies = [ "serde", ] [[package]] name = "cadcraft-io" -version = "0.20.0" +version = "0.20.1" dependencies = [ "acadrust", "cadcraft-color", @@ -769,7 +769,7 @@ dependencies = [ [[package]] name = "cadcraft-mcp" -version = "0.20.0" +version = "0.20.1" dependencies = [ "cadcraft-engine", "cadcraft-io", @@ -784,7 +784,7 @@ dependencies = [ [[package]] name = "cadcraft-render" -version = "0.20.0" +version = "0.20.1" dependencies = [ "cadcraft-color", "cadcraft-doc", @@ -798,7 +798,7 @@ dependencies = [ [[package]] name = "cadcraft-ui-egui" -version = "0.20.0" +version = "0.20.1" dependencies = [ "cadcraft-color", "cadcraft-doc", @@ -825,7 +825,7 @@ dependencies = [ [[package]] name = "cadcraft-web" -version = "0.20.0" +version = "0.20.1" dependencies = [ "cadcraft-engine", "cadcraft-fonts", @@ -5994,7 +5994,7 @@ checksum = "9440ea3e5aeabb0ac63af70daf835274065238cdd0cec83418f417eae38bacee" [[package]] name = "xtask" -version = "0.20.0" +version = "0.20.1" dependencies = [ "chrono", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 2b7a8fefb..ddacaf0d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ members = ["crates/*", "apps/*", "xtask"] # default-members [workspace.package] -version = "0.20.0" +version = "0.20.1" edition = "2024" license = "MIT OR Apache-2.0" rust-version = "1.95"