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

+46 -20
View File
@@ -131,6 +131,12 @@ pub(super) struct State {
/// Whether the parked capture actually waited for a redraw (vs. the
/// anti-hang timer). Reported in the capture result as `waited_for_frame`.
pub(super) capture_after_redraw: bool,
/// Web: when a parked capture gives up. Only a presented frame can be
/// read back on wasm, and a hidden or idle tab may never present one, so
/// the timer tick fails the capture past this instant instead of letting
/// it hold the control slot forever.
#[cfg(target_arch = "wasm32")]
pub(super) capture_deadline: Option<iced::time::Instant>,
}
/// The pending half of an interactive `user_select` operation (`interactive`
@@ -1212,6 +1218,11 @@ impl OpenCADStudio {
// rendered frame (or the timer fallback) so geometry changes are
// visible rather than returning a stale frame.
self.control.capture_frame_pending = true;
#[cfg(target_arch = "wasm32")]
{
self.control.capture_deadline =
Some(iced::time::Instant::now() + std::time::Duration::from_secs(5));
}
Task::none()
}
"stop" => {
@@ -1648,30 +1659,45 @@ impl OpenCADStudio {
// browser clears the WebGL buffer — so wait for a real frame.
#[cfg(target_arch = "wasm32")]
{
if !redrew {
let expired = self
.control
.capture_deadline
.is_some_and(|deadline| iced::time::Instant::now() >= deadline);
if !redrew && !expired && crate::sys::page_visible() {
return Task::none();
}
self.control.capture_frame_pending = false;
self.control.capture_after_redraw = true;
return Task::done(Message::ControlScreenshot(path, crate::sys::capture_canvas()));
}
self.control.capture_frame_pending = false;
self.control.capture_after_redraw = redrew;
let Some(window) = self.main_window else {
return Task::done(Message::ControlScreenshot(path, None));
};
// A minimized window has a 0x0 surface and the renderer panics reading
// it back, so report instead of capturing.
iced::window::size(window).then(move |size| {
let path = path.clone();
if size.width <= 0.0 || size.height <= 0.0 {
Task::done(Message::ControlScreenshot(path, None))
self.control.capture_deadline = None;
self.control.capture_after_redraw = redrew;
// No frame to read (hidden tab or nothing redrawing): report the
// capture as failed rather than hand back a blank canvas.
let screenshot = if redrew {
crate::sys::capture_canvas()
} else {
iced::window::screenshot(window).map(move |screenshot| {
Message::ControlScreenshot(path.clone(), Some(screenshot))
})
}
})
None
};
Task::done(Message::ControlScreenshot(path, screenshot))
}
#[cfg(not(target_arch = "wasm32"))]
{
self.control.capture_frame_pending = false;
self.control.capture_after_redraw = redrew;
let Some(window) = self.main_window else {
return Task::done(Message::ControlScreenshot(path, None));
};
// A minimized window has a 0x0 surface and the renderer panics
// reading it back, so report instead of capturing.
iced::window::size(window).then(move |size| {
let path = path.clone();
if size.width <= 0.0 || size.height <= 0.0 {
Task::done(Message::ControlScreenshot(path, None))
} else {
iced::window::screenshot(window).map(move |screenshot| {
Message::ControlScreenshot(path.clone(), Some(screenshot))
})
}
})
}
}
}
mod actions;