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: | diff --git a/Cargo.lock b/Cargo.lock index 5dcec2532..71523bae0 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 eba807670..ddacaf0d7 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 } 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..424b2bcee --- /dev/null +++ b/crates/engine/tests/bsaveas_source.rs @@ -0,0 +1,37 @@ +//! Saving an edited block as a copy must retain its source's last saved definition. + +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(); + 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); +}