fix: close the adversarial findings on the save/paste wave

Ported from da950460 (#1717).

In-place save:
- A stale retained handle could receive bytes serialized for a
  DIFFERENT format (Save As .dwg->.dxf whose re-pick failed wrote DXF
  bytes through the .dwg handle) or silently save in place while the
  user was told a copy was downloading. The serialized format now
  travels to completion (tab-keyed pending map), the write requires a
  matching handle extension AND a live tab, and a failed re-pick
  drops the old handle before the download fallback.
- Discarding an unsaved tab leaked its handle, and a serialization
  still in flight could then overwrite the real file the user just
  declined to save. Discard drops the handle; a tab-gone completion
  downloads instead of writing.
- Overlapping saves had no guard: a wasm in-flight slot refuses a
  second save with the same localized message, cleared on every exit
  path.

Paste: pasted_image_name built its timestamp from Date::new(0) — a
CONSTANT name, so every paste overwrote every earlier paste's image.
Real clock, milliseconds, and a collision-suffixed unique name now.

Capture: a web capture in a hidden tab, or one with nothing redrawing,
could hold the automation slot forever. It fails fast when the page is
hidden and gives up after 5 s, on main's frame-parked capture.

The XREF manager reports registry-loaded xrefs with their size on the
web instead of NotFound.

Left out: the always-warm print snapshot (a frames subscription that
emits a message on every frame keeps the app redrawing at the display
rate), so the browser-menu print wiring from the previous commit goes
too — its beforeprint readback printed blank pages on WebGL2. The
merge-residue hunks (re-added XREF bind tests, snap endpoint map and
3D-snap gate) belong to code main already has in its own form.

(cherry picked from commit da950460)
This commit is contained in:
Afonsop authored and Hakan Seven committed 2026-10-12 12:02:05 +03:00
1 parent 835e8d469f
commit 1c0963b68d
8 files changed
+382 -90

No files matched your search

+23
View File
@@ -198,6 +198,14 @@ impl OpenCADStudio {
let Some(idx) = self.tabs.iter().position(|t| t.id == tab_id) else {
return Task::batch([close_win, self.continue_tab_close_queue()]);
};
// …and, on the web, its retained in-place save handle — the
// same hook on_tab_close runs: the user declined the save,
// so the picked file must never receive a later write, and
// the next tab to reuse this id must start with a fresh
// pick instead of silently writing over the discarded
// drawing's file.
#[cfg(target_arch = "wasm32")]
crate::io::fs_access::drop_handle(tab_id);
// Discarded — drop this tab's autosave recovery copy.
#[cfg(not(target_arch = "wasm32"))]
let _ = std::fs::remove_file(self.autosave_target(idx));
@@ -319,6 +327,21 @@ impl OpenCADStudio {
#[cfg(target_arch = "wasm32")]
{
// Same guard the native half runs against active_save_jobs
// above: while a web save is between capture and completion,
// refuse a second one (the window is seconds — refuse, don't
// queue) instead of letting two serializer rounds race for the
// same tab's bookkeeping.
if let Some(busy_tab) = crate::io::fs_access::web_save_in_flight() {
if self.tabs.iter().any(|tab| tab.id == busy_tab) {
self.command_line
.push_info(crate::t!("Save already running for this drawing.").as_ref());
return Task::none();
}
// Stale marker (its tab closed before settling): clear it
// rather than block this save forever.
crate::io::fs_access::end_web_save(busy_tab);
}
match self.pending_close.take() {
Some(crate::app::PendingClose::Tab(tab_id)) => {
let Some(idx) = self.tabs.iter().position(|t| t.id == tab_id) else {