fix: final pre-PR review sweep

The final holistic + quality audit (three reviewers) returned GO with
four majors; all fixed.

- Start-page recents were the last unguarded writer of the open slot
  on web: clicking one under the open overlay clobbered the in-flight
  open and silently lost its bytes. Refused with the same in-progress
  notice the picker uses.
- The web quit-after-save continuation stalled: a Quit pending now
  survives the tab-save AND continues — offering to save the next
  dirty tab or finishing the quit, mirroring native.
- Data-extraction's multi-file pick is capped (16): every picked file
  becomes a full parsed document held for the session, and one giant
  pick could exhaust the wasm heap.
- The xref registry tests raced each other on the process-global
  registry (reproduced 3/10 at high thread counts — CI roulette); they
  now hold a module-level guard.

Minors: the collision warning is cataloged in all 21 locales; the
docs' parallelism detail header no longer contradicts its own table;
the drop-filter and name-hash helpers gained inline tests (their only
prior coverage was a verbatim copy in the red-team suites).
This commit is contained in:
Afonsop committed 2026-09-18 03:33:12 +01:00
1 parent b635d56c00
commit f70d3fda44
29 files changed
+101 -1

No files matched your search

+10
View File
@@ -1589,6 +1589,16 @@ impl OpenCADStudio {
.pick_files()
.await
.unwrap_or_default();
// Each picked file becomes a full parsed document held in
// the registry for the session; a bound keeps a giant
// multi-pick from exhausting the wasm heap (tab death).
const MAX_EXTRACTION_SOURCES: usize = 16;
if handles.len() > MAX_EXTRACTION_SOURCES {
return Err(crate::tf!(
"At most {MAX_EXTRACTION_SOURCES} drawings can be added in the browser."
)
.into_owned());
}
let mut paths = Vec::new();
for handle in handles {
let name = handle.file_name();
+7
View File
@@ -2283,6 +2283,13 @@ impl OpenCADStudio {
}
return self.continue_tab_close_queue();
}
} else if matches!(self.pending_close, Some(crate::app::PendingClose::Quit)) {
// Mirror the native Quit continuation: offer to save the
// next dirty tab, or finish the quit.
if self.tabs.iter().any(|tab| tab.dirty) {
return self.open_unsaved_dialog_window();
}
return Task::batch([self.close_unsaved_dialog_window(), self.exit_app()]);
} else if self.pending_close.is_some() {
return self.open_unsaved_dialog_window();
}
+10
View File
@@ -572,6 +572,16 @@ impl OpenCADStudio {
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| path.to_string_lossy().into_owned());
// The start page stays clickable under the open overlay,
// so a recent can race an in-flight open and clobber it
// (its WebFileOpened then fails the id check and the
// bytes are lost) — refuse like on_open_file does.
if self.opening.is_some() {
self.command_line.push_info(
crate::t!("An open is already in progress.").as_ref(),
);
return Task::none();
}
let state = std::sync::Arc::new(crate::io::OpenProgressState::new(
crate::app::OPEN_PHASE_READING,
));
+17
View File
@@ -290,3 +290,20 @@ fn is_image_name(name: &str) -> bool {
"png" | "jpg" | "jpeg" | "bmp" | "tiff" | "tif"
)
}
#[cfg(test)]
mod tests {
use super::is_drawing_name;
#[test]
fn drawing_names_use_the_worker_convention() {
assert!(is_drawing_name("plan.dwg"));
assert!(is_drawing_name("PLAN.DXF"));
assert!(is_drawing_name("DWG"));
assert!(is_drawing_name("dxf"));
assert!(!is_drawing_name("plan.png"));
assert!(!is_drawing_name("dwgx"));
assert!(!is_drawing_name("plan.dwg.txt"));
assert!(!is_drawing_name(""));
}
}
+20
View File
@@ -312,3 +312,23 @@ pub(crate) async fn modified_ms(name: &str) -> Option<f64> {
.ok()?;
Some(file.last_modified())
}
#[cfg(test)]
mod tests {
use super::name_hash;
/// Canonical FNV-1a 64 over the raw name bytes — the reference vector
/// from the spec's test suite.
#[test]
fn name_hash_is_fnv1a64() {
assert_eq!(name_hash(""), "cbf29ce484222325");
assert_eq!(name_hash("a"), "af63dc4c8601ec8c");
assert_eq!(name_hash("foobar"), "85944171f73967e8");
}
#[test]
fn name_hash_distinguishes_case_and_extension() {
assert_ne!(name_hash("a.dwg"), name_hash("A.dwg"));
assert_ne!(name_hash("a.dwg"), name_hash("a.dxf"));
}
}
+14
View File
@@ -790,6 +790,17 @@ fn set_handle(entity: &mut EntityType, h: Handle) {
mod tests {
use super::*;
/// The registries are process-global and the default harness runs tests
/// in parallel; the eviction test's inserts can evict a sibling test's
/// entry mid-assert. Registry tests hold this lock for their duration.
fn registry_guard() -> std::sync::MutexGuard<'static, ()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
fn outcome() -> acadrust::ReadOutcome {
acadrust::ReadOutcome::new(CadDocument::new(), acadrust::ReadStats::default())
}
@@ -821,6 +832,7 @@ mod tests {
#[test]
fn registry_finds_a_registered_source_by_trailing_name() {
let _registry = registry_guard();
let registered = "Registry-Find-Site.dwg";
register_source(registered, Arc::new(vec![1, 2, 3]));
// The drawing's stored path is the author's absolute foreign path;
@@ -840,6 +852,7 @@ mod tests {
/// re-registration of a live name neither evicts nor duplicates it.
#[test]
fn source_registry_evicts_oldest_beyond_the_cap() {
let _registry = registry_guard();
for i in 0..(SOURCE_CAP + 2) {
register_source(&format!("f{i}.dwg"), Arc::new(vec![i as u8]));
}
@@ -879,6 +892,7 @@ mod tests {
#[test]
fn register_source_supersedes_a_cached_parse_of_the_same_file() {
let _registry = registry_guard();
let name = "Registry-Supersede-Plan.dwg";
register_parsed(name, outcome());
// Re-registering the same file under a differently-cased path name
+1
View File
@@ -1885,6 +1885,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
"Opening OCS Discussions for help and questions..." => Some(("view", "opening-ocs-discussions-for-help-and-questions")),
"Opening OCS Web..." => Some(("view", "opening-ocs-web")),
"Opening the command reference..." => Some(("view", "opening-the-command-reference")),
"\"{name}\" is already loaded with different contents; the existing file was kept." => Some(("file", "already-loaded-different-contents-kept")),
"Opening Patreon page..." => Some(("view", "opening-patreon-page")),
"Opening feedback page..." => Some(("view", "opening-feedback-page")),
"Opening file" => Some(("ui", "opening-file")),