From 07bb6042145142b2f8a0f8ef44ec01274ce8d7dc Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Thu, 22 Oct 2026 18:18:43 -0400 Subject: [PATCH 01/19] security: guard the DXF->DWG conversion input and document decoder limits (CWE-789) (#1847) The native DWG bridge wraps the third-party `acadrust` decoder, which allocates tables and section buffers from sizes a file declares and cannot be interrupted once it starts. The DWG->DXF read path already rejects oversized input before decoding, caps the DXF output while writing, and runs inside `catch_unwind`. The DXF->DWG `convert` path had no input-size guard, so a very large DXF went straight into the dependency's reader. - `convert` now rejects DXF input larger than `Limits::max_dxf_bytes` before calling `DxfReader::read`, symmetric with the read path, and keeps the existing `catch_unwind` around the decoder and writer (rows 23, 3, 27, large-input variant). - Documented the residual risk: a small file that declares a huge internal table or section can still make the dependency allocate before any size is known. Fully fixing that needs an upstream fix in `acadrust` (validate declared sizes against a decoded-memory budget) or a separate worker with an OS memory limit; the in-crate limits keep the blast radius bounded until then. Addresses findings rows 3, 23, 27 (bounds the reachable input/output size; the deeper allocation fix belongs in the unmodified dependency). --- crates/dwg/src/lib.rs | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/crates/dwg/src/lib.rs b/crates/dwg/src/lib.rs index 7cbd40fc..835830c9 100644 --- a/crates/dwg/src/lib.rs +++ b/crates/dwg/src/lib.rs @@ -4,6 +4,25 @@ //! converted to DXF bytes, which CADCraft's own DXF reader maps to its document model; saving //! goes the other way. Keeping the dependency behind this byte-level API isolates it: nothing //! else in CADCraft depends on its types. Native targets only (it memory-maps files). +//! +//! # Guardrails around the third-party decoder +//! +//! The decoder allocates tables and section buffers from sizes a (possibly damaged or hostile) +//! file declares, and once it starts it cannot be interrupted (an allocation failure aborts the +//! process, which `catch_unwind` cannot recover). Because the dependency is used unmodified, this +//! crate guards it from the outside: +//! +//! - Both directions reject oversized input *before* the decoder runs ([`Limits::max_dwg_bytes`] +//! for DWG→DXF, [`Limits::max_dxf_bytes`] for DXF→DWG), and the DWG→DXF output is capped while +//! it is written (`CappedWriter`). +//! - The reader and writer run inside `catch_unwind`, so a decode failure is reported instead of +//! crashing. +//! +//! These bound the input- and output-size variants. A small file that declares a huge internal +//! table or section can still make the dependency allocate before any size is known; fully fixing +//! that requires an upstream fix in `acadrust` (validate declared sizes against a decoded-memory +//! budget) or running the decode in a separate worker with an OS memory limit. The limits here +//! keep the blast radius bounded in the meantime. #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] #![forbid(unsafe_code)] @@ -505,6 +524,13 @@ mod native { } fn convert(dxf: &[u8], version: Option) -> Result, String> { + // Reject a pathologically large DXF before handing it to the third-party decoder, which + // allocates tables and section buffers from the input's declared sizes and cannot be + // interrupted once it starts (CWE-789). This bounds the large-input variant; the decoder + // runs inside `catch_unwind` so a decode failure is reported rather than crashing. + if dxf.len() > Limits::DEFAULT.max_dxf_bytes { + return Err(format!("DXF: the input is {}, larger than the {} CADCraft converts to DWG", mb(dxf.len() as u64), mb(Limits::DEFAULT.max_dxf_bytes as u64))); + } let data = dxf.to_vec(); let mut doc = std::panic::catch_unwind(move || acadrust::DxfReader::from_reader(Cursor::new(data)).and_then(|r| r.read())) .map_err(|_| "the DXF→DWG conversion failed".to_string())? @@ -618,6 +644,15 @@ mod tests { assert!(dwg_to_dxf(b"").is_err()); } + #[cfg(not(target_arch = "wasm32"))] + #[test] + fn dxf_to_dwg_rejects_garbage_without_crashing() { + // The DXF→DWG path runs the third-party reader inside catch_unwind and reports failures + // rather than crashing (and rejects oversized input before decoding). + assert!(dxf_to_dwg(b"not a dxf file").is_err()); + assert!(dxf_to_dwg(b"").is_err()); + } + /// A DXF with one DIMENSION per (text, group 70, subclass, points) row. #[cfg(not(target_arch = "wasm32"))] fn dims_dxf(dims: &[(&str, i64, &str, &[(i32, [f64; 2])])]) -> String { From 15ba3dc5ccc2fb03796c5e477d09eb4717d13d3f Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Thu, 22 Oct 2026 18:19:13 -0400 Subject: [PATCH 02/19] security: submit XATTACH picked path as literal text, not a command line (CWE-74) (#1843) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The External References palette's XATTACH handler passed the file picker's result to `app.cmdline(&path)`. `cmdline` interprets command-line syntax, so a selected filename beginning with an apostrophe (for example `'closeall`) was run as an engine command rather than naming the file — closing every drawing without the UI's unsaved-change confirmation. The picked path is attacker-controlled (the picker returns whatever the user selected, and `relative_to_host` can reduce it to a bare basename). After starting XATTACH, submit it through `Session::input(Input::Text(path))` so it is delivered to the command's prompt as literal text and never interpreted as a command, matching how other picker/dialog bridges already behave. Addresses finding row 26. --- crates/ui-egui/src/xrefs.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/xrefs.rs b/crates/ui-egui/src/xrefs.rs index 5828169d..fb9cb9ef 100644 --- a/crates/ui-egui/src/xrefs.rs +++ b/crates/ui-egui/src/xrefs.rs @@ -24,7 +24,13 @@ pub fn route(app: &mut CadApp, id: &str, params: &Value) -> Option None, From 01bbdb24bc27b087893afc37dff07d79862a5b97 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Thu, 22 Oct 2026 18:42:51 -0400 Subject: [PATCH 03/19] security: confine craft-fonts manifest paths and reject newline directives (CWE-22, CWE-74) (#1841) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The optional `CRAFT_FONTS_DIR` build input lets a font manifest select the file each face is embedded from. `build.rs` canonicalised that file with no containment check, so an absolute path, a `..` traversal, or a symlink inside the checkout could make the generated `include_bytes!` read an arbitrary file on the build host and expose its bytes through `CRAFT_FONTS` (CWE-22). It also printed the resolved path straight into Cargo's newline-delimited build-script protocol (`cargo::rerun-if-changed=…`). A path containing an embedded newline — reachable through a crafted symlink target — could inject a second directive such as `cargo::rustc-env=OUT_DIR=…`, redirecting the generated-source include to attacker-supplied Rust (CWE-74). - `resolve_font` rejects absolute and `..` manifest paths and requires the canonicalised result to stay inside the canonicalised checkout root. - `rerun_if_changed` refuses any path containing CR/LF before emitting the directive; `one_line` collapses CR/LF in checkout-derived diagnostics. Addresses findings rows 5 (CWE-22) and 14 (CWE-74). --- crates/fonts/build.rs | 47 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/crates/fonts/build.rs b/crates/fonts/build.rs index 7eb2a784..658f5b7b 100644 --- a/crates/fonts/build.rs +++ b/crates/fonts/build.rs @@ -18,9 +18,9 @@ fn main() { match craft_fonts(&dir) { Ok(entries) => src.push_str(&entries), Err(e) if std::env::var_os("CRAFT_FONTS_REQUIRED").is_some() => { - println!("cargo::error=CRAFT_FONTS_DIR={}: {e}", dir.display()); + println!("cargo::error=CRAFT_FONTS_DIR={}: {}", one_line(&dir.display().to_string()), one_line(&e)); } - Err(e) => println!("cargo::warning=building without craft-fonts: CRAFT_FONTS_DIR={}: {e}", dir.display()), + Err(e) => println!("cargo::warning=building without craft-fonts: CRAFT_FONTS_DIR={}: {}", one_line(&dir.display().to_string()), one_line(&e)), } } src.push_str("];\n"); @@ -37,7 +37,9 @@ const BARRED_FAMILIES: [&str; 5] = ["Source Han", "Source Serif", "Source Sans", /// One `CraftFont { .. }` initialiser per manifest line, in manifest order. fn craft_fonts(dir: &std::path::Path) -> Result { let manifest = dir.join("fonts/manifest.txt"); - println!("cargo::rerun-if-changed={}", manifest.display()); + rerun_if_changed(&manifest)?; + // Resolve the checkout root once so every manifest file can be confined inside it. + let root = dir.canonicalize().map_err(|e| format!("{}: {e}", dir.display()))?; let text = std::fs::read_to_string(&manifest).map_err(|e| format!("{}: {e}", manifest.display()))?; let wasm = std::env::var("CARGO_CFG_TARGET_ARCH").is_ok_and(|a| a == "wasm32"); let mut out = String::new(); @@ -55,8 +57,8 @@ fn craft_fonts(dir: &std::path::Path) -> Result { if wasm && !web_face { continue; } - let path = dir.join(file).canonicalize().map_err(|e| format!("{file}: {e}"))?; - println!("cargo::rerun-if-changed={}", path.display()); + let path = resolve_font(&root, file)?; + rerun_if_changed(&path)?; let scripts: Vec = scripts.split(',').map(|s| format!("{:?}", s.trim())).collect(); let _ = writeln!( out, @@ -67,3 +69,38 @@ fn craft_fonts(dir: &std::path::Path) -> Result { } Ok(out) } + +/// Resolve a manifest `file` field to a real path *confined to the checkout* `root` (CWE-22). +/// +/// The field is rejected if it is absolute or contains a parent (`..`) component, and the +/// canonicalised result must still live inside `root`, so a crafted manifest or a symlink in the +/// checkout cannot make the generated `include_bytes!` read an arbitrary file on the build host. +fn resolve_font(root: &std::path::Path, file: &str) -> Result { + let rel = std::path::Path::new(file); + if rel.is_absolute() || rel.components().any(|c| matches!(c, std::path::Component::ParentDir)) { + return Err(format!("{file}: manifest paths must be relative and stay inside the checkout")); + } + let path = root.join(rel).canonicalize().map_err(|e| format!("{file}: {e}"))?; + if !path.starts_with(root) { + return Err(format!("{file}: resolves outside the craft-fonts checkout")); + } + Ok(path) +} + +/// Collapse CR/LF in a diagnostic derived from checkout contents to a single space, so it cannot +/// introduce an additional Cargo directive on the newline-delimited build-script stream (CWE-74). +fn one_line(s: &str) -> String { + s.replace(['\n', '\r'], " ") +} + +/// Emit a `cargo::rerun-if-changed` directive for `path`, refusing paths that contain a line +/// separator (CWE-74): Cargo's build-script protocol is newline-delimited, so a path with an +/// embedded CR/LF could smuggle an extra directive such as `cargo::rustc-env=OUT_DIR=…`. +fn rerun_if_changed(path: &std::path::Path) -> Result<(), String> { + let shown = path.display().to_string(); + if shown.contains(['\n', '\r']) { + return Err(format!("{shown:?}: path contains a line separator")); + } + println!("cargo::rerun-if-changed={shown}"); + Ok(()) +} From 48b4165591360b4e7d513a5276c2d08dead09a38 Mon Sep 17 00:00:00 2001 From: skylartaylor <26588250+skylartaylor@users.noreply.github.com> Date: Fri, 23 Oct 2026 01:59:55 +0200 Subject: [PATCH 04/19] docs: document security limits and regression guarantees --- apps/cadcraft/src/control_server.rs | 10 ++++++++++ crates/constraints/src/dof.rs | 3 +++ crates/constraints/src/lib.rs | 2 ++ crates/constraints/src/model.rs | 5 +++++ crates/constraints/src/residual.rs | 1 + crates/constraints/tests/dof.rs | 1 + crates/dwg/src/lib.rs | 6 +++++- crates/engine/src/capped_read.rs | 3 +++ crates/engine/src/cmd/file.rs | 1 + crates/engine/src/lisp/builtins.rs | 9 +++++++++ crates/engine/src/lisp/drawing.rs | 3 +++ crates/engine/src/lisp/mod.rs | 1 + crates/engine/src/lisp/tests.rs | 5 +++++ crates/engine/src/lisp/value.rs | 1 + crates/fonts/build.rs | 4 +++- crates/fonts/src/complex.rs | 1 + crates/fonts/src/mtext.rs | 1 + crates/fonts/src/ttf.rs | 16 ++++++++++++++++ crates/io/src/dxf_image.rs | 2 ++ crates/io/src/dxf_read.rs | 6 ++++++ crates/io/src/svg_read.rs | 2 ++ crates/io/tests/svg_arc_budget.rs | 2 ++ crates/mcp/src/backend.rs | 7 +++++++ crates/mcp/src/control_token.rs | 13 +++++++++++++ crates/render/src/dim.rs | 6 ++++++ crates/render/src/hatch.rs | 4 ++++ crates/render/src/linetype.rs | 1 + crates/render/src/table.rs | 4 ++++ xtask/src/stats.rs | 6 ++++++ 29 files changed, 124 insertions(+), 2 deletions(-) diff --git a/apps/cadcraft/src/control_server.rs b/apps/cadcraft/src/control_server.rs index d11671e1..14283e6f 100644 --- a/apps/cadcraft/src/control_server.rs +++ b/apps/cadcraft/src/control_server.rs @@ -48,6 +48,8 @@ impl Default for Limits { } } +/// Start the loopback server with a fresh session token and a bounded request queue. +/// Binding or token-issuance failure returns a disconnected receiver. pub fn start(port: u16, ctx: egui::Context) -> Receiver { match TcpListener::bind(("127.0.0.1", port)) { Ok(l) => { @@ -101,6 +103,7 @@ impl Drop for Slot { } } +/// Accept clients within the connection limit and give each worker the same session token. fn accept_loop(listener: TcpListener, tx: SyncSender, ctx: egui::Context, limits: Limits, token: Arc) { let open = Arc::new(AtomicUsize::new(0)); for mut stream in accepted(listener.incoming(), || std::thread::sleep(ACCEPT_RETRY)) { @@ -162,6 +165,7 @@ enum Line { Reject(String), } +/// Parse a UTF-8 JSON request and its optional token, distinguishing blank lines from rejected input. fn parse_line(bytes: &[u8]) -> Line { let Ok(text) = std::str::from_utf8(bytes) else { return Line::Reject("not a control request (invalid UTF-8); closing the connection".into()); @@ -198,6 +202,8 @@ fn is_http_request_line(text: &str) -> bool { || text.rsplit_once(' ').is_some_and(|(_, version)| version.starts_with("HTTP/")) } +/// Serve bounded JSON lines, authenticating each request before enqueueing it for the UI. +/// Invalid credentials close the connection; queue and reply limits report whether a request ran. fn serve(stream: TcpStream, tx: &SyncSender, ctx: &egui::Context, limits: Limits, token: &str) { if stream.set_read_timeout(Some(limits.idle_timeout)).is_err() || stream.set_write_timeout(Some(limits.write_timeout)).is_err() { return; @@ -385,6 +391,7 @@ mod tests { assert!(t.elapsed() < Duration::from_secs(4)); } + /// Authenticated requests rejected by a full queue retain their IDs and report that they did not run. #[test] fn a_full_queue_answers_busy() { let (addr, rx) = serve_with(short()); @@ -407,6 +414,7 @@ mod tests { drop(rx); } + /// Replies on one authenticated connection preserve the ID of each request. #[test] fn requests_are_answered_with_their_ids() { let addr = serve_echo(); @@ -423,6 +431,7 @@ mod tests { } } + /// Missing and incorrect session tokens are rejected before dispatch. #[test] fn a_request_without_the_token_is_unauthorized() { // A local peer that does not hold the session token cannot drive the app (CWE-306). @@ -437,6 +446,7 @@ mod tests { assert!(wrong[0]["error"].as_str().unwrap().contains("unauthorized"), "{wrong:?}"); } + /// A request carrying the session token reaches the control dispatcher. #[test] fn a_request_with_the_token_is_dispatched() { let addr = serve_echo(); diff --git a/crates/constraints/src/dof.rs b/crates/constraints/src/dof.rs index 5be5cd57..f0a34b9f 100644 --- a/crates/constraints/src/dof.rs +++ b/crates/constraints/src/dof.rs @@ -89,6 +89,8 @@ struct CompDof { objects: Vec>, } +/// Compute component and object freedom only when rank and residual-evaluation work fit their budgets. +/// Return unknown freedom before building the Jacobian when either work limit is exceeded. fn analyse(comp: &Comp) -> CompDof { let free = comp.free(None); let rows = comp.rows(None); @@ -213,6 +215,7 @@ mod budget_tests { use cadcraft_doc::{Common, ConstraintKind, EntityKind, GeomRef, Space}; use cadcraft_geom::{Spline, Vec2}; + /// An expensive fit-spline endpoint residual produces unknown freedom before numerical differentiation. #[test] fn endpoint_analysis_rejects_cubic_fit_work_before_differentiation() { let mut d = Drawing::default(); diff --git a/crates/constraints/src/lib.rs b/crates/constraints/src/lib.rs index 33821946..81e81fc2 100644 --- a/crates/constraints/src/lib.rs +++ b/crates/constraints/src/lib.rs @@ -328,6 +328,8 @@ pub(crate) fn build(d: &Drawing, cons: &[&Constraint], opts: &SolveOptions) -> R build_budgeted(d, cons, opts, &mut members_left) } +/// Build constraint components with one caller-owned rigid-member sampling budget shared by every model. +/// Return constraints that cannot be interpreted as skipped entries. pub(crate) fn build_budgeted( d: &Drawing, cons: &[&Constraint], diff --git a/crates/constraints/src/model.rs b/crates/constraints/src/model.rs index 53ace75b..b7fe1432 100644 --- a/crates/constraints/src/model.rs +++ b/crates/constraints/src/model.rs @@ -117,6 +117,8 @@ pub fn rigid_points(d: &Drawing, ins: &Insert) -> Vec { rigid_points_budgeted(d, ins, &mut members_left) } +/// Sample finite block points within per-insert and caller-shared member limits. +/// Members that contribute no points still consume traversal work; nested blocks are not entered. fn rigid_points_budgeted(d: &Drawing, ins: &Insert, members_left: &mut usize) -> Vec { let Some(block) = d.block(&ins.block) else { return Vec::new() }; let m = ins.transform(block.base.xy()); @@ -187,6 +189,7 @@ impl Model { Self::build_budgeted(d, handles, &mut members_left) } + /// Build solver parameters for supported entities while sharing rigid-member sampling work with other models. pub(crate) fn build_budgeted(d: &Drawing, handles: &[Handle], members_left: &mut usize) -> Model { let mut m = Model::default(); for &h in handles { @@ -620,6 +623,7 @@ mod budget_tests { use cadcraft_geom::Vec3; use std::sync::Arc; + /// Writing a model back preserves partial rational weights and the spline they describe. #[test] fn write_back_keeps_partial_rational_weights() { let mut drawing = Drawing::default(); @@ -635,6 +639,7 @@ mod budget_tests { } } + /// A second model receives no sampled block points after the first model spends the shared member budget. #[test] fn rigid_sampling_budget_is_shared_between_models() { let mut d = Drawing::default(); diff --git a/crates/constraints/src/residual.rs b/crates/constraints/src/residual.rs index 164ada07..be5723fe 100644 --- a/crates/constraints/src/residual.rs +++ b/crates/constraints/src/residual.rs @@ -116,6 +116,7 @@ fn r_at(c: &Constraint, i: usize) -> Option<&GeomRef> { } impl Built { + /// Estimate spline reconstruction work for endpoint and join residuals; other residual forms add no such cost. pub(crate) fn spline_reconstruct_cost(&self, m: &Model) -> f64 { let cost = |i: usize| self.refs.get(i).map_or(0.0, |r| m.spline_reconstruct_cost(r.handle)); match self.form { diff --git a/crates/constraints/tests/dof.rs b/crates/constraints/tests/dof.rs index f863d9b8..ea78ca93 100644 --- a/crates/constraints/tests/dof.rs +++ b/crates/constraints/tests/dof.rs @@ -364,6 +364,7 @@ fn hostile_new_geometry_never_panics() { let _ = dof(&d, &[Handle(0), Handle(u64::MAX)]); } +/// Fit-point vertex constraints retain a known freedom count because they do not reconstruct the spline. #[test] fn vertex_constraints_on_large_fit_spline_do_not_charge_reconstruction() { let mut d = Drawing::default(); diff --git a/crates/dwg/src/lib.rs b/crates/dwg/src/lib.rs index 835830c9..27322924 100644 --- a/crates/dwg/src/lib.rs +++ b/crates/dwg/src/lib.rs @@ -529,7 +529,11 @@ mod native { // interrupted once it starts (CWE-789). This bounds the large-input variant; the decoder // runs inside `catch_unwind` so a decode failure is reported rather than crashing. if dxf.len() > Limits::DEFAULT.max_dxf_bytes { - return Err(format!("DXF: the input is {}, larger than the {} CADCraft converts to DWG", mb(dxf.len() as u64), mb(Limits::DEFAULT.max_dxf_bytes as u64))); + return Err(format!( + "DXF: the input is {}, larger than the {} CADCraft converts to DWG", + mb(dxf.len() as u64), + mb(Limits::DEFAULT.max_dxf_bytes as u64) + )); } let data = dxf.to_vec(); let mut doc = std::panic::catch_unwind(move || acadrust::DxfReader::from_reader(Cursor::new(data)).and_then(|r| r.read())) diff --git a/crates/engine/src/capped_read.rs b/crates/engine/src/capped_read.rs index d82344bb..05c7267c 100644 --- a/crates/engine/src/capped_read.rs +++ b/crates/engine/src/capped_read.rs @@ -44,6 +44,7 @@ pub fn read_capped(path: &Path, max: usize) -> io::Result> { mod tests { use super::*; + /// Regular-file reads accept the byte boundary and reject any larger payload. #[test] fn reads_within_limit_and_rejects_oversize() { let dir = std::env::temp_dir().join(format!("cadcraft-cr-{}", std::process::id())); @@ -57,6 +58,7 @@ mod tests { std::fs::remove_dir_all(&dir).ok(); } + /// An endless device is rejected before its contents can be buffered. #[cfg(unix)] #[test] fn rejects_non_regular_file() { @@ -66,6 +68,7 @@ mod tests { assert!(read_capped(p, 8 << 20).is_err()); } } + /// Opening a FIFO without a writer returns an error without waiting for a peer. #[cfg(unix)] #[test] fn rejects_fifo_without_a_writer() { diff --git a/crates/engine/src/cmd/file.rs b/crates/engine/src/cmd/file.rs index 96b86f4e..b6998f42 100644 --- a/crates/engine/src/cmd/file.rs +++ b/crates/engine/src/cmd/file.rs @@ -337,6 +337,7 @@ fn opened(s: &Session, i: usize, entities: bool) -> Value { } } +/// Open a drawing from bounded regular-file bytes or base64 data and resolve its external references. fn run_open(s: &mut Session, p: &Value) -> Result { if let Some(_path) = str_param(p, "path") { #[cfg(not(target_arch = "wasm32"))] diff --git a/crates/engine/src/lisp/builtins.rs b/crates/engine/src/lisp/builtins.rs index 9ef90f3b..e421686c 100644 --- a/crates/engine/src/lisp/builtins.rs +++ b/crates/engine/src/lisp/builtins.rs @@ -698,11 +698,14 @@ fn wcmatch(a: &[Value]) -> R { /// cover), but finite so truly exponential `*`-heavy patterns are rejected rather than hanging. const WCMATCH_BUDGET: u64 = 200_000_000; +/// Spend one wildcard work unit, returning an error when the shared budget is exhausted. fn charge_match(budget: &mut u64) -> Result<(), ()> { *budget = budget.checked_sub(1).ok_or(())?; Ok(()) } +/// Split comma-separated wildcard alternatives while preserving escapes and bracket sets. +/// Every consumed character spends from the same budget used for matching. fn split_patterns(pat: &str, budget: &mut u64) -> Result>, ()> { let mut out = vec![Vec::new()]; let mut chars = pat.chars().peekable(); @@ -742,6 +745,7 @@ fn split_patterns(pat: &str, budget: &mut u64) -> Result>, ()> { Ok(out) } +/// Match one wildcard alternative, including leading `~` negation, within the shared budget. fn match_pattern(text: &[char], pat: &[char], budget: &mut u64) -> Result { if let Some(('~', rest)) = pat.split_first().map(|(c, r)| (*c, r)) && !rest.is_empty() @@ -757,6 +761,7 @@ fn glob(text: &[char], pat: &[char], depth: usize, budget: &mut u64) -> Result R { Ok(if file_exists(&p) { Value::Str(p) } else { Value::Nil }) } +/// Read a regular text file up to 64 MiB, replacing invalid UTF-8 and returning `None` on read failure. #[cfg(not(target_arch = "wasm32"))] pub fn read_text_file(path: &str) -> Option { // Read through a byte-limited, regular-file-only reader so a path naming an endless device @@ -1068,6 +1075,7 @@ pub fn read_text_file(path: &str) -> Option { let bytes = crate::capped_read::read_capped(std::path::Path::new(path), MAX).ok()?; Some(String::from_utf8_lossy(&bytes).into_owned()) } +/// Return `None` on the web, where native filesystem paths are unavailable. #[cfg(target_arch = "wasm32")] pub fn read_text_file(_path: &str) -> Option { None @@ -1588,6 +1596,7 @@ pub fn wild_match(text: &str, pattern: &str) -> bool { mod wildcard_budget_tests { use super::*; + /// Pattern splitting, literal matching, and bracket scans all stop when their shared fuel runs out. #[test] fn all_native_scans_consume_fuel() { assert!(split_patterns("abcdefgh", &mut 4).is_err()); diff --git a/crates/engine/src/lisp/drawing.rs b/crates/engine/src/lisp/drawing.rs index 043b611a..53c7af86 100644 --- a/crates/engine/src/lisp/drawing.rs +++ b/crates/engine/src/lisp/drawing.rs @@ -158,6 +158,8 @@ pub fn from_json(v: &serde_json::Value) -> Value { const TO_JSON_BUDGET: u64 = 2_000_000; const TO_JSON_MAX_DEPTH: usize = 256; +/// Convert a LISP value to owned JSON within element and nesting limits. +/// Return `None` before a shared cons structure can expand beyond the remaining budget. fn to_json_bounded(v: &Value, depth: usize, budget: &mut u64) -> Option { if depth > TO_JSON_MAX_DEPTH || *budget == 0 { return None; @@ -185,6 +187,7 @@ pub fn getvar(ctx: &mut Ctx<'_>, a: Vec) -> Result { Ok(Flow::Value(crate::sysvars::get(ctx.s, &name).map(|v| from_json(&v)).unwrap_or_default())) } +/// Set a system variable after bounded JSON conversion, reporting invalid or oversized values as LISP errors. pub fn setvar(ctx: &mut Ctx<'_>, a: Vec) -> Result { arity(&a, 2, 2)?; let name = string(&a[0])?; diff --git a/crates/engine/src/lisp/mod.rs b/crates/engine/src/lisp/mod.rs index 14ac32c6..47dea46d 100644 --- a/crates/engine/src/lisp/mod.rs +++ b/crates/engine/src/lisp/mod.rs @@ -131,6 +131,7 @@ pub(crate) struct Limits { } impl Default for Limits { + /// Provide finite cons-cell, string-byte, and synchronous-callback limits for a LISP run. fn default() -> Self { Limits { cells: value::MAX_CELLS, string_bytes: value::STRING_BUDGET, callback_steps: 500_000_000 } } diff --git a/crates/engine/src/lisp/tests.rs b/crates/engine/src/lisp/tests.rs index 20669056..9104e252 100644 --- a/crates/engine/src/lisp/tests.rs +++ b/crates/engine/src/lisp/tests.rs @@ -221,6 +221,7 @@ fn runaway_routines_are_errors_not_crashes() { assert_eq!(r, Ok("3000000".into())); } +/// An infinite comparator stops at its callback cap, and a later finite comparator still succeeds. #[test] fn synchronous_callback_loop_is_an_error_not_a_hang() { // A comparator with an infinite loop used to run with u64::MAX fuel and hang the thread; it @@ -232,6 +233,7 @@ fn synchronous_callback_loop_is_an_error_not_a_hang() { assert_eq!(ok(&mut s, "(vl-sort '(3 1 2) '<)"), "(1 2 3)"); } +/// A zero-step callback limit prevents even the first callback side effect. #[test] fn a_zero_callback_budget_does_not_run_the_callback() { let mut s = session(); @@ -242,6 +244,7 @@ fn a_zero_callback_budget_does_not_run_the_callback() { assert_eq!(ok(&mut s, "callback-ran"), "nil"); } +/// A callback stops at a cap smaller than one scheduling slice without completing all its assignments. #[test] fn a_callback_must_fit_within_a_partial_slice() { let mut s = session(); @@ -252,6 +255,7 @@ fn a_callback_must_fit_within_a_partial_slice() { assert_eq!(ok(&mut s, "(and (> callback-count 0) (< callback-count 1000))"), "T"); } +/// Crossing a scheduling boundary grants only the additional configured step, rather than another slice. #[test] fn reaching_a_callback_slice_boundary_does_not_grant_another_slice() { let counts: Vec = [399_999, 400_000] @@ -270,6 +274,7 @@ fn reaching_a_callback_slice_boundary_does_not_grant_another_slice() { assert!(counts[1] >= counts[0] && counts[1] - counts[0] <= 1, "{counts:?}"); } +/// System-variable conversion rejects a shared list whose owned JSON expansion would exceed its budget. #[test] fn setvar_rejects_an_oversized_shared_structure() { // A list repeatedly sharing its own contents expands exponentially; setvar's bounded JSON diff --git a/crates/engine/src/lisp/value.rs b/crates/engine/src/lisp/value.rs index 99f066d7..09da1d27 100644 --- a/crates/engine/src/lisp/value.rs +++ b/crates/engine/src/lisp/value.rs @@ -353,6 +353,7 @@ pub fn equal(a: &Value, b: &Value, fuzz: f64) -> bool { /// Largest number of node comparisons one `equal` call may perform. const EQUAL_BUDGET: u64 = 50_000_000; +/// Compare nested values within depth and node-work limits, returning false when either limit is reached. fn equal_depth(a: &Value, b: &Value, fuzz: f64, depth: usize, budget: &mut u64) -> bool { if depth > MAX_DEPTH || *budget == 0 { return false; diff --git a/crates/fonts/build.rs b/crates/fonts/build.rs index 658f5b7b..6df86f7e 100644 --- a/crates/fonts/build.rs +++ b/crates/fonts/build.rs @@ -20,7 +20,9 @@ fn main() { Err(e) if std::env::var_os("CRAFT_FONTS_REQUIRED").is_some() => { println!("cargo::error=CRAFT_FONTS_DIR={}: {}", one_line(&dir.display().to_string()), one_line(&e)); } - Err(e) => println!("cargo::warning=building without craft-fonts: CRAFT_FONTS_DIR={}: {}", one_line(&dir.display().to_string()), one_line(&e)), + Err(e) => { + println!("cargo::warning=building without craft-fonts: CRAFT_FONTS_DIR={}: {}", one_line(&dir.display().to_string()), one_line(&e)) + } } } src.push_str("];\n"); diff --git a/crates/fonts/src/complex.rs b/crates/fonts/src/complex.rs index fdfef151..6193f3f8 100644 --- a/crates/fonts/src/complex.rs +++ b/crates/fonts/src/complex.rs @@ -60,6 +60,7 @@ pub(crate) fn shape(primary: Option<&Face>, s: &str, height: f64, width_factor: catch_unwind(AssertUnwindSafe(|| shape_chars(primary, &chars, height, width_factor, oblique))).ok() } +/// Lay out decoded directional runs, keeping advances and decoration spans for empty glyphs. fn shape_chars(primary: Option<&Face>, chars: &[(char, bool, bool)], height: f64, width_factor: f64, oblique: f64) -> Shaped { let h = if height.is_finite() && height > 0.0 { height } else { 1.0 }; let wf = if width_factor.is_finite() && width_factor.abs() > 1e-6 { width_factor } else { 1.0 }; diff --git a/crates/fonts/src/mtext.rs b/crates/fonts/src/mtext.rs index 916bdf33..0c6cb074 100644 --- a/crates/fonts/src/mtext.rs +++ b/crates/fonts/src/mtext.rs @@ -685,6 +685,7 @@ pub fn layout_mtext_with(contents: &str, p: &MTextParams) -> MTextLayout { mod tests { use super::*; + /// Incremental character accounting preserves the text of a multi-word line without introducing wrapping. #[test] fn line_char_offsets_track_incrementally() { // A multi-word line lays out correctly even though the per-line character count is now diff --git a/crates/fonts/src/ttf.rs b/crates/fonts/src/ttf.rs index aedcbb07..52998900 100644 --- a/crates/fonts/src/ttf.rs +++ b/crates/fonts/src/ttf.rs @@ -624,6 +624,7 @@ struct GlyphCache { points: usize, } +/// Return the process-wide outline cache whose retained glyph and point counts are bounded during insertion. fn glyph_cache() -> &'static RwLock { static C: OnceLock> = OnceLock::new(); C.get_or_init(|| RwLock::new(GlyphCache::default())) @@ -643,6 +644,7 @@ struct Pen { const MAX_OUTLINE_POINTS: usize = 65_536; impl Pen { + /// Append one flattened outline point, discarding all contours if the per-glyph point limit is exceeded. fn push(&mut self, point: Vec2) { if self.truncated { return; @@ -661,6 +663,7 @@ impl Pen { fn pt(x: f32, y: f32) -> Vec2 { Vec2::new(f64::from(x), f64::from(y)) } + /// Close and retain a nondegenerate contour only if its closing point also fits the outline budget. fn flush(&mut self) { if self.cur.len() > 2 { if let (Some(f), Some(l)) = (self.cur.first().copied(), self.cur.last().copied()) @@ -678,13 +681,16 @@ impl Pen { } impl OutlinePen for Pen { + /// Finish the preceding contour and begin a new one through the budgeted point writer. fn move_to(&mut self, x: f32, y: f32) { self.flush(); self.push(Self::pt(x, y)); } + /// Append a line endpoint through the budgeted point writer. fn line_to(&mut self, x: f32, y: f32) { self.push(Self::pt(x, y)); } + /// Flatten a quadratic segment into six samples, charging each retained point to the glyph budget. fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) { let a = self.cur.last().copied().unwrap_or_default(); let c = Self::pt(cx0, cy0); @@ -695,6 +701,7 @@ impl OutlinePen for Pen { self.push(a * (u * u) + c * (2.0 * u * t) + b * (t * t)); } } + /// Flatten a cubic segment into eight samples, charging each retained point to the glyph budget. fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) { let a = self.cur.last().copied().unwrap_or_default(); let c0 = Self::pt(cx0, cy0); @@ -742,6 +749,8 @@ pub fn glyph_contours(face: &Face, gid: u16) -> Arc<[Vec]> { glyph_by_id(face, u32::from(gid)).contours.clone() } +/// Load or build a glyph outline with bounded flattening, retaining its advance even when contours are discarded. +/// Cache insertion clears existing entries when the glyph-count or aggregate point allowance would be exceeded. fn outline(face: &Face, k: GlyphKey) -> Arc { let key = (face.bytes.as_ptr() as usize, face.bytes.len(), face.index, k); if let Some(g) = glyph_cache().read().unwrap_or_else(PoisonError::into_inner).glyphs.get(&key) { @@ -812,6 +821,8 @@ pub(crate) fn text_glyph_at(g: &GlyphOutline, c: char, origin: Vec2, scale: f64, }) } +/// Charge source outline points before copying and transforming a glyph. +/// Return `None` without spending fuel when it cannot fit; empty outlines remain valid at zero budget. pub(crate) fn place_glyph_budgeted(g: &GlyphOutline, x: f64, h: f64, wf: f64, shear: f64, points_left: &mut usize) -> Option>> { *points_left = points_left.checked_sub(g.points)?; Some(place_glyph(g, x, h, wf, shear)) @@ -926,6 +937,7 @@ mod family_tests { #[cfg(test)] mod geometry_budget_tests { use super::*; + /// Placement checks the source point cost before copying and accepts empty outlines without spending points. #[test] fn placement_checks_source_size_and_accepts_empty_glyphs() { let glyph = GlyphOutline { contours: vec![vec![Vec2::ZERO; 4]].into(), points: 4, cap: 1.0, ..Default::default() }; @@ -939,6 +951,7 @@ mod geometry_budget_tests { assert_eq!(budget, 0); assert!(place_glyph_budgeted(&glyph, 0.0, 1.0, 1.0, 0.0, &mut budget).is_none()); } + /// Exceeding the per-glyph flattening limit discards all contour geometry before it reaches the cache. #[test] fn flattened_outlines_are_bounded_before_caching() { let mut pen = Pen::default(); @@ -951,6 +964,7 @@ mod geometry_budget_tests { assert!(pen.contours.is_empty() && pen.cur.is_empty()); assert_eq!(pen.points, MAX_OUTLINE_POINTS); } + /// A mapped empty glyph retains text metadata referring to an existing empty outline slot. #[test] fn mapped_empty_glyph_keeps_text_and_a_valid_outline_slot() { // Metadata placement does not parse the face; an empty face suffices to check the index contract. @@ -963,6 +977,7 @@ mod geometry_budget_tests { assert!(shaped.glyphs[shaped.text[0].outline].is_empty()); } + /// An empty glyph with no text mapping leaves the shaped result empty. #[test] fn unmapped_empty_glyph_does_not_make_the_shape_nonempty() { let glyph = GlyphOutline { cap: 1.0, ..Default::default() }; @@ -973,6 +988,7 @@ mod geometry_budget_tests { assert!(shaped.text.is_empty()); } + /// Missing text metadata does not discard a glyph that has drawable contours. #[test] fn an_unmapped_glyph_with_contours_still_draws() { let mut shaped = Shaped::default(); diff --git a/crates/io/src/dxf_image.rs b/crates/io/src/dxf_image.rs index a4f57a5d..8f4685a2 100644 --- a/crates/io/src/dxf_image.rs +++ b/crates/io/src/dxf_image.rs @@ -156,6 +156,8 @@ pub(crate) fn resolve(secs: &[Section], d: &mut Drawing) { } } +/// Resolve image definitions under a shared copied-string budget. +/// When strings no longer fit, keep applying pixel-size and resolution metadata. fn fill(store: &mut EntityStore, by_image: &HashMap, budget: &mut usize) { let hs: Vec = store.iter().filter(|e| matches!(e.kind, EntityKind::Image(_)) && by_image.contains_key(&e.handle)).map(|e| e.handle).collect(); diff --git a/crates/io/src/dxf_read.rs b/crates/io/src/dxf_read.rs index e8839dc5..bfd1ec0a 100644 --- a/crates/io/src/dxf_read.rs +++ b/crates/io/src/dxf_read.rs @@ -248,6 +248,7 @@ fn entity(kind: &str, tags: &[Tag]) -> Option<(Common, EntityKind)> { entity_with_table_budget(kind, tags, &mut 4_000_000) } +/// Parse one entity while charging materialized table cells to the caller's shared import budget. fn entity_with_table_budget(kind: &str, tags: &[Tag], table_cells_left: &mut usize) -> Option<(Common, EntityKind)> { // The paper-space viewport our writer adds (dxf_write::paper_view) stays implicit. if kind == "VIEWPORT" && crate::dxf_ext::is_paper_view(tags) { @@ -1398,6 +1399,7 @@ fn style_font(tags: &[Tag], file: String) -> String { } } +/// Load symbol tables and resolve handle references, indexing layer names for case-insensitive updates. fn tables(tags: &[Tag], d: &mut Drawing, rx: &mut Rx) { let recs = records(tags); // STYLE handle → name, or the shape file of an unnamed shape style (linetype elements). @@ -1681,6 +1683,7 @@ pub fn read(bytes: &[u8]) -> Result { read_budgeted(bytes, 4_000_000) } +/// Import DXF sections while sharing one materialized-table-cell budget across blocks and entity sections. fn read_budgeted(bytes: &[u8], mut table_cells_left: usize) -> Result { let tags = cadcraft_dxf::parse(bytes).map_err(|e| IoError::Format(e.to_string()))?; let secs = sections(&tags); @@ -2345,9 +2348,11 @@ pub fn entity_from_groups(kind: &str, tags: &[Tag]) -> Option<(Common, EntityKin #[cfg(test)] mod table_budget_tests { use super::*; + /// Build synthetic table tags with a requested row and column count for allocation-budget checks. fn table_tags(rows: i64, cols: i64) -> Vec { vec![Tag::s(100, "AcDbTable"), Tag::i(91, rows), Tag::i(92, cols), Tag::i(171, 1), Tag::s(1, "cell")] } + /// A table that cannot fit allocates no cell grid, while a smaller table spends only its cell count. #[test] fn exhausted_budget_rejects_before_materializing_cells() { let mut left = 3; @@ -2358,6 +2363,7 @@ mod table_budget_tests { assert_eq!(table.cells[0].len(), 2); assert_eq!(left, 1); } + /// Tables in blocks and repeated entity sections consume one document-wide cell allowance. #[test] fn blocks_and_entity_sections_share_one_budget() { let table = "0\nACAD_TABLE\n100\nAcDbTable\n91\n1\n92\n2\n171\n1\n1\ncell\n"; diff --git a/crates/io/src/svg_read.rs b/crates/io/src/svg_read.rs index 8e555216..1d150fc8 100644 --- a/crates/io/src/svg_read.rs +++ b/crates/io/src/svg_read.rs @@ -172,6 +172,7 @@ impl LeftOut { } impl Parser { + /// Initialize a metric SVG drawing with one document-wide generated-vertex allowance. fn new() -> Self { Parser { drawing: Drawing::new_metric(), @@ -334,6 +335,7 @@ impl Parser { self.cur().apply_vec(v) } + /// Charge an entity against element and retained-vertex limits before adding it to model space. fn add(&mut self, kind: EntityKind) -> Result<()> { self.elements += 1; if self.elements > MAX_ELEMENTS { diff --git a/crates/io/tests/svg_arc_budget.rs b/crates/io/tests/svg_arc_budget.rs index b3b096e3..dbcc9715 100644 --- a/crates/io/tests/svg_arc_budget.rs +++ b/crates/io/tests/svg_arc_budget.rs @@ -17,6 +17,7 @@ fn arc_bomb(arcs: usize) -> Vec { svg.into_bytes() } +/// A small transformed-arc input remains accepted within the generated-geometry budget. #[test] fn a_modest_arc_svg_still_reads() { // A handful of arcs is well within budget and parses fine. @@ -24,6 +25,7 @@ fn a_modest_arc_svg_still_reads() { assert!(d.model.iter().count() >= 1, "expected some geometry"); } +/// Repeated flattened arcs fail with a geometry-budget error before retaining excessive vertices. #[test] fn an_arc_heavy_svg_is_rejected_by_the_vertex_budget() { // ~1000 arcs x up to 10_000 chords each would far exceed the 8M vertex budget. diff --git a/crates/mcp/src/backend.rs b/crates/mcp/src/backend.rs index c758bac9..b843ebbd 100644 --- a/crates/mcp/src/backend.rs +++ b/crates/mcp/src/backend.rs @@ -10,9 +10,11 @@ use serde_json::{Value, json}; /// Something that answers control-channel methods (`engine.execute`, `cmdline.input`, /// `drawing.inspect`, `ui.render`…). See `cadcraft_ui_egui::control` for the full list. pub trait Backend { + /// Invoke a control method and return its result or a descriptive error. fn call(&mut self, method: &str, params: Value) -> Result; /// True when a real UI is attached (`ui.screenshot` works). fn has_ui(&self) -> bool; + /// Describe the backend connection for client-facing status. fn describe(&self) -> String; } @@ -37,6 +39,7 @@ fn port_of(addr: &str) -> Option { const LAUNCH_WAIT: Duration = Duration::from_secs(30); impl Remote { + /// Connect to a running control server; its session token is read lazily when the first request is sent. pub fn connect(addr: &str) -> std::io::Result { let mut r = Self { addr: addr.to_string(), conn: None, next_id: 1, launch: false, token: None }; r.reconnect()?; @@ -182,6 +185,8 @@ fn is_read_only(method: &str) -> bool { } impl Backend for Remote { + /// Send an authenticated control request, refreshing the token for a safe retry. + /// A mutation that may have reached the server is never resent automatically. fn call(&mut self, method: &str, params: Value) -> Result { let id = self.next_id; self.next_id += 1; @@ -447,6 +452,7 @@ mod tests { server.join().unwrap(); } } + /// Retrying a read after a lost reply reloads the replacement server session token. #[test] fn safe_retry_rereads_a_rotated_token_after_a_lost_reply() { let listener = TcpListener::bind("127.0.0.1:0").unwrap(); @@ -473,6 +479,7 @@ mod tests { assert_eq!(remote.call("ping", json!({})).unwrap(), json!("pong")); server.join().unwrap(); } + /// A failed mutation write reports uncertain delivery and does not open a retry connection. #[test] fn failed_mutation_write_is_not_resent() { let listener = TcpListener::bind("127.0.0.1:0").unwrap(); diff --git a/crates/mcp/src/control_token.rs b/crates/mcp/src/control_token.rs index 84677ae4..084f8b45 100644 --- a/crates/mcp/src/control_token.rs +++ b/crates/mcp/src/control_token.rs @@ -23,6 +23,7 @@ pub fn token_path(port: u16) -> io::Result { Ok(private_dir()?.join(format!("cadcraft-control-{port}.token"))) } +/// Choose the Unix runtime directory, home cache, or temporary directory for an owner-only token file. #[cfg(unix)] fn private_dir() -> io::Result { if let Some(d) = std::env::var_os("XDG_RUNTIME_DIR").filter(|v| !v.is_empty()) { @@ -34,11 +35,13 @@ fn private_dir() -> io::Result { Ok(std::env::temp_dir()) } +/// Require the Windows per-user local app-data directory for session token storage. #[cfg(windows)] fn private_dir() -> io::Result { windows_private_dir(std::env::var_os("LOCALAPPDATA")) } +/// Validate that Windows supplied a nonempty local app-data path instead of using a shared fallback. #[cfg(any(windows, test))] fn windows_private_dir(local: Option) -> io::Result { local @@ -47,6 +50,7 @@ fn windows_private_dir(local: Option) -> io::Result .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "LOCALAPPDATA is required for a private control token")) } +/// Reject token storage on platforms without a supported private-directory convention. #[cfg(not(any(unix, windows)))] fn private_dir() -> io::Result { Err(io::Error::new(io::ErrorKind::Unsupported, "no private control token directory on this platform")) @@ -61,6 +65,7 @@ pub fn issue(port: u16) -> io::Result { Ok(token) } +/// Create missing parent directories, then write the session token through the platform privacy helper. fn issue_at(path: &std::path::Path, bytes: &[u8]) -> io::Result<()> { if let Some(parent) = path.parent() { std::fs::create_dir_all(parent)?; @@ -98,6 +103,7 @@ pub fn matches(expected: &str, given: &str) -> bool { diff == 0 } +/// Encode 256 bits of operating-system randomness as lowercase hexadecimal, propagating entropy failures. fn random_hex() -> io::Result { let mut bytes = [0u8; TOKEN_BYTES]; getrandom::fill(&mut bytes).map_err(|e| io::Error::other(format!("no secure randomness: {e}")))?; @@ -109,6 +115,7 @@ fn random_hex() -> io::Result { Ok(s) } +/// Replace a stale token with an exclusively created file using Unix mode `0600`, then flush its bytes. #[cfg(unix)] fn write_private(path: &std::path::Path, bytes: &[u8]) -> io::Result<()> { use std::io::Write; @@ -120,6 +127,7 @@ fn write_private(path: &std::path::Path, bytes: &[u8]) -> io::Result<()> { f.flush() } +/// Exclusively create and flush a replacement token within the per-user directory selected by the caller. #[cfg(not(unix))] fn write_private(path: &std::path::Path, bytes: &[u8]) -> io::Result<()> { use std::io::Write; @@ -134,6 +142,7 @@ fn write_private(path: &std::path::Path, bytes: &[u8]) -> io::Result<()> { mod tests { use super::*; + /// An issued token has the expected size, can be read back, and disappears after revocation. #[test] fn issue_then_read_round_trips() { let port = 50000 + (std::process::id() % 10000) as u16; @@ -144,6 +153,7 @@ mod tests { assert!(read(port).is_err()); } + /// Token comparison rejects different bytes and lengths while accepting identical tokens. #[test] fn matches_is_exact() { assert!(matches("abc", "abc")); @@ -152,6 +162,7 @@ mod tests { assert!(!matches("", "x")); } + /// A token issued on Unix grants no file permission bits to group or other users. #[cfg(unix)] #[test] fn token_file_is_owner_only() { @@ -162,6 +173,7 @@ mod tests { assert_eq!(mode & 0o077, 0, "token file is readable by group/other: {:o}", mode); revoke(port); } + /// Windows token storage rejects absent or empty app-data paths and preserves a supplied path. #[test] fn windows_requires_local_app_data() { assert!(windows_private_dir(None).is_err()); @@ -169,6 +181,7 @@ mod tests { assert_eq!(windows_private_dir(Some("private-app-data".into())).unwrap(), PathBuf::from("private-app-data")); } + /// Token issuance creates its missing directory hierarchy before writing the credential. #[test] fn issuance_creates_missing_parent() { let dir = std::env::temp_dir().join(format!("cadcraft-token-parent-{}", std::process::id())); diff --git a/crates/render/src/dim.rs b/crates/render/src/dim.rs index e79b7b4e..3a14aabc 100644 --- a/crates/render/src/dim.rs +++ b/crates/render/src/dim.rs @@ -413,6 +413,7 @@ fn tol_height_code(st: &DimStyle) -> String { format!("{}x", format_decimal(f / 0.7, 4, 8, ".")) } +/// Apply measurement placeholders or append a dimension suffix, bounding the resulting UTF-8 text. fn post(template: &str, meas: &str) -> String { if template.contains("<>") { bounded_replace(template, meas) @@ -424,6 +425,7 @@ fn post(template: &str, meas: &str) -> String { } } +/// Apply dimension text overrides: empty uses the base, whitespace suppresses text, and other text substitutes it. fn apply_override(base: &str, d: &Dimension) -> String { if d.text.is_empty() { base.to_string() @@ -455,6 +457,8 @@ fn bounded_replace(template: &str, value: &str) -> String { } // Returns false when truncation consumed the remaining UTF-8-safe space. +/// Append within the remaining dimension-text byte allowance without splitting a UTF-8 character. +/// Return false if any input text had to be truncated. fn append_dim_text(out: &mut String, text: &str) -> bool { let mut end = text.len().min(MAX_DIM_TEXT.saturating_sub(out.len())); while !text.is_char_boundary(end) { @@ -1363,12 +1367,14 @@ mod tests { #[cfg(test)] mod text_budget_tests { use super::*; + /// A dimension suffix without placeholders still obeys the total text limit. #[test] fn bounds_post_without_markers() { let text = post(&"x".repeat(MAX_DIM_TEXT * 2), "12"); assert_eq!(text.len(), MAX_DIM_TEXT); assert!(text.starts_with("12x")); } + /// Oversized substituted text retains its prefix and ends at a valid UTF-8 boundary. #[test] fn oversized_value_is_substituted_and_truncated_at_a_character_boundary() { let text = bounded_replace("prefix <> mm", &"é".repeat(MAX_DIM_TEXT)); diff --git a/crates/render/src/hatch.rs b/crates/render/src/hatch.rs index b297618a..dd83a71a 100644 --- a/crates/render/src/hatch.rs +++ b/crates/render/src/hatch.rs @@ -79,6 +79,8 @@ fn same_polygon(a: &[Vec2], b: &[Vec2]) -> bool { (0..n).any(|k| (0..n).all(|i| near(a[i], b[(i + k) % n]))) || (0..n).any(|k| (0..n).all(|i| near(a[i], b[(k + n - (i % n)) % n]))) } +/// Remove degenerate and duplicate boundary loops within a shared comparison budget. +/// Retain remaining loops without further comparisons once the budget is exhausted. fn dedup_polygons(loops: &mut Vec>) { // `same_polygon` is O(n²) in a polygon's vertex count (every cyclic alignment, both windings), // and each loop is compared against every unique one kept so far, so adversarial boundary @@ -463,6 +465,7 @@ impl Family<'_> { self.origin + p.rotate(self.ang) } + /// Intersect a pattern family with boundary loops, respecting view, output, cancellation, and shared dash-work limits. fn sweep(&self, loops: &[Vec], px: f64, view: Option<&Bounds2>, cancel: Option<&AtomicBool>, out: &mut Vec>, work: &mut u64) { let local: Vec> = loops.iter().map(|l| l.iter().map(|p| self.to_local(*p)).collect()).collect(); let bb = Bounds2::from_points(local.iter().flatten().copied()); @@ -606,6 +609,7 @@ mod tests { #[cfg(test)] mod dedup_budget_tests { use super::*; + /// Loops with different vertex counts leave enough comparison fuel to remove a later duplicate. #[test] fn unequal_loops_do_not_spend_the_comparison_budget() { // The old estimate exhausted the budget on this unequal pair before seeing the triangle. diff --git a/crates/render/src/linetype.rs b/crates/render/src/linetype.rs index 8a8cab34..0c999caa 100644 --- a/crates/render/src/linetype.rs +++ b/crates/render/src/linetype.rs @@ -118,6 +118,7 @@ pub fn ranges(len: f64, lt: &Linetype, scale: f64, min_pattern: f64) -> Option Vec> { } // No partial coverage may reach owner assignment: it would revisit unbounded merge rectangles. +/// Mark cells covered by merges under grid-size and marking-work limits. +/// Return partial coverage as an error so layout can use its outline fallback instead of assigning owners. fn coverage(t: &Table) -> Result>, Vec>> { let rows = t.row_heights.len().min(10_000); let cols = t.col_widths.len().min(10_000); @@ -395,6 +397,7 @@ mod budget_tests { use super::*; use cadcraft_doc::TableCell; + /// Excessive overlapping merges draw only the table outline after coverage work is exhausted. #[test] fn overlapping_merges_use_outline_fallback() { let t = Table { @@ -409,6 +412,7 @@ mod budget_tests { assert!(g.fills.is_empty() && g.texts.is_empty()); } + /// A split table clips merge spans using the rendered column count when source columns exceed the limit. #[test] fn part_clips_merges_using_displayed_column_count() { let t = Table { diff --git a/xtask/src/stats.rs b/xtask/src/stats.rs index a5b6e7df..c2c6de3d 100644 --- a/xtask/src/stats.rs +++ b/xtask/src/stats.rs @@ -14,11 +14,14 @@ struct Counts { /// never make the stats command run unbounded work. const MAX_ENTRIES: usize = 2_000_000; +/// Accumulate Rust source counts under one directory with a fresh traversal-entry budget. fn walk(dir: &Path, c: &mut Counts) -> Result<(), String> { let mut budget = MAX_ENTRIES; walk_inner(dir, c, &mut budget) } +/// Traverse entries without following symlinks, sharing one budget across descendant directories. +/// Skip unreadable entries and excluded directories; report budget exhaustion to the caller. fn walk_inner(dir: &Path, c: &mut Counts, budget: &mut usize) -> Result<(), String> { let Ok(rd) = std::fs::read_dir(dir) else { return Ok(()); @@ -84,6 +87,7 @@ fn exact_tests(pkg: &str) -> Option { Some(String::from_utf8_lossy(&out.stdout).lines().filter(|l| l.ends_with(": test")).count()) } +/// Print per-package source statistics, optionally querying exact test counts, and propagate traversal errors. pub fn run(_root: &Path, exact: bool) -> Result<(), String> { let meta = crate::metadata()?; let mut rows = Vec::new(); @@ -138,6 +142,7 @@ mod tests { assert_eq!(count_tests(src), (2, 1)); } + /// A directory alias does not cause its Rust source to be counted twice. #[cfg(unix)] #[test] fn walk_does_not_follow_directory_symlinks() { @@ -154,6 +159,7 @@ mod tests { assert_eq!(c.files, 1, "the aliased directory was followed and counted twice"); std::fs::remove_dir_all(&base).ok(); } + /// Traversal reports an exhausted entry budget and accepts a tree that exactly fits it. #[test] fn walk_reports_budget_exhaustion() { let base = std::env::temp_dir().join(format!("cadcraft-stats-budget-{}", std::process::id())); From bad1073fdf10a521949f6b542837b564719d0023 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Thu, 22 Oct 2026 20:06:04 -0400 Subject: [PATCH 05/19] Release 0.12.0 Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 32 ++++++++++++++++---------------- Cargo.toml | 2 +- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6ed70601..2c24364a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -547,7 +547,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadcraft" -version = "0.11.0" +version = "0.12.0" dependencies = [ "cadcraft-engine", "cadcraft-io", @@ -571,7 +571,7 @@ dependencies = [ [[package]] name = "cadcraft-cli" -version = "0.11.0" +version = "0.12.0" dependencies = [ "cadcraft-engine", "cadcraft-io", @@ -582,14 +582,14 @@ dependencies = [ [[package]] name = "cadcraft-color" -version = "0.11.0" +version = "0.12.0" dependencies = [ "serde", ] [[package]] name = "cadcraft-constraints" -version = "0.11.0" +version = "0.12.0" dependencies = [ "cadcraft-doc", "cadcraft-geom", @@ -599,7 +599,7 @@ dependencies = [ [[package]] name = "cadcraft-doc" -version = "0.11.0" +version = "0.12.0" dependencies = [ "cadcraft-color", "cadcraft-geom", @@ -610,14 +610,14 @@ dependencies = [ [[package]] name = "cadcraft-dwg" -version = "0.11.0" +version = "0.12.0" dependencies = [ "acadrust", ] [[package]] name = "cadcraft-dxf" -version = "0.11.0" +version = "0.12.0" dependencies = [ "encoding_rs", "log", @@ -626,7 +626,7 @@ dependencies = [ [[package]] name = "cadcraft-engine" -version = "0.11.0" +version = "0.12.0" dependencies = [ "cadcraft-color", "cadcraft-constraints", @@ -647,7 +647,7 @@ dependencies = [ [[package]] name = "cadcraft-fonts" -version = "0.11.0" +version = "0.12.0" dependencies = [ "cadcraft-geom", "harfrust", @@ -657,14 +657,14 @@ dependencies = [ [[package]] name = "cadcraft-geom" -version = "0.11.0" +version = "0.12.0" dependencies = [ "serde", ] [[package]] name = "cadcraft-io" -version = "0.11.0" +version = "0.12.0" dependencies = [ "acadrust", "cadcraft-color", @@ -684,7 +684,7 @@ dependencies = [ [[package]] name = "cadcraft-mcp" -version = "0.11.0" +version = "0.12.0" dependencies = [ "cadcraft-engine", "cadcraft-io", @@ -696,7 +696,7 @@ dependencies = [ [[package]] name = "cadcraft-render" -version = "0.11.0" +version = "0.12.0" dependencies = [ "cadcraft-color", "cadcraft-doc", @@ -710,7 +710,7 @@ dependencies = [ [[package]] name = "cadcraft-ui-egui" -version = "0.11.0" +version = "0.12.0" dependencies = [ "cadcraft-color", "cadcraft-doc", @@ -733,7 +733,7 @@ dependencies = [ [[package]] name = "cadcraft-web" -version = "0.11.0" +version = "0.12.0" dependencies = [ "cadcraft-engine", "cadcraft-io", @@ -4825,7 +4825,7 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" [[package]] name = "xtask" -version = "0.11.0" +version = "0.12.0" dependencies = [ "chrono", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index c87f38ff..7ac093d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ members = ["crates/*", "apps/*", "xtask"] # default-members [workspace.package] -version = "0.11.0" +version = "0.12.0" edition = "2024" license = "MIT OR Apache-2.0" rust-version = "1.90" From fa6bb0351bfa93716504a5b37aa9b539bd575d01 Mon Sep 17 00:00:00 2001 From: skylartaylor <26588250+skylartaylor@users.noreply.github.com> Date: Fri, 23 Oct 2026 02:14:11 +0200 Subject: [PATCH 06/19] docs: clarify security regression guarantees --- crates/constraints/src/model.rs | 2 +- crates/mcp/src/backend.rs | 2 +- crates/render/src/linetype.rs | 2 +- crates/render/src/table.rs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/constraints/src/model.rs b/crates/constraints/src/model.rs index b7fe1432..ba7e5854 100644 --- a/crates/constraints/src/model.rs +++ b/crates/constraints/src/model.rs @@ -623,7 +623,7 @@ mod budget_tests { use cadcraft_geom::Vec3; use std::sync::Arc; - /// Writing a model back preserves partial rational weights and the spline they describe. + /// Writing an unchanged model back leaves partial rational weights and the original spline geometry intact. #[test] fn write_back_keeps_partial_rational_weights() { let mut drawing = Drawing::default(); diff --git a/crates/mcp/src/backend.rs b/crates/mcp/src/backend.rs index b843ebbd..69383933 100644 --- a/crates/mcp/src/backend.rs +++ b/crates/mcp/src/backend.rs @@ -185,7 +185,7 @@ fn is_read_only(method: &str) -> bool { } impl Backend for Remote { - /// Send an authenticated control request, refreshing the token for a safe retry. + /// Send a control request with the token when available, refreshing it before a safe retry. /// A mutation that may have reached the server is never resent automatically. fn call(&mut self, method: &str, params: Value) -> Result { let id = self.next_id; diff --git a/crates/render/src/linetype.rs b/crates/render/src/linetype.rs index 0c999caa..1385a4d3 100644 --- a/crates/render/src/linetype.rs +++ b/crates/render/src/linetype.rs @@ -118,7 +118,7 @@ pub fn ranges(len: f64, lt: &Linetype, scale: f64, min_pattern: f64) -> Option Date: Thu, 22 Oct 2026 23:54:32 -0400 Subject: [PATCH 07/19] chore(deps): bump the cargo-dependencies group across 1 directory with 17 updates (#1957) Bumps the cargo-dependencies group with 17 updates in the / directory: | Package | From | To | | --- | --- | --- | | [serde_json](https://github.com/serde-rs/json) | `1.0.151` | `1.0.154` | | [tiny-skia](https://github.com/linebender/tiny-skia) | `0.11.4` | `0.12.0` | | [skrifa](https://github.com/googlefonts/fontations) | `0.47.0` | `0.48.0` | | [harfrust](https://github.com/harfbuzz/harfrust) | `0.12.0` | `0.14.0` | | [quick-xml](https://github.com/tafia/quick-xml) | `0.41.0` | `0.42.0` | | [egui](https://github.com/emilk/egui) | `0.36.2` | `0.37.0` | | [egui_kittest](https://github.com/emilk/egui) | `0.36.2` | `0.37.0` | | [eframe](https://github.com/emilk/egui) | `0.36.2` | `0.37.0` | | [egui-wgpu](https://github.com/emilk/egui) | `0.36.2` | `0.37.0` | | [arboard](https://github.com/1Password/arboard) | `3.6.1` | `3.7.0` | | [acadrust](https://github.com/hakanaktt/acadrust) | `0.6.4` | `0.6.5` | | [rstar](https://github.com/georust/rstar) | `0.12.2` | `0.13.0` | | [libc](https://github.com/rust-lang/libc) | `0.2.190` | `0.2.191` | | [miniz_oxide](https://github.com/Frommi/miniz_oxide) | `0.8.9` | `0.9.1` | | [getrandom](https://github.com/rust-random/getrandom) | `0.3.4` | `0.4.3` | | [muda](https://github.com/tauri-apps/muda) | `0.21.0` | `0.21.2` | | [winresource](https://github.com/BenjaminRi/winresource) | `0.1.31` | `0.1.34` | Updates `serde_json` from 1.0.151 to 1.0.154 - [Release notes](https://github.com/serde-rs/json/releases) - [Commits](https://github.com/serde-rs/json/compare/v1.0.151...v1.0.154) Updates `tiny-skia` from 0.11.4 to 0.12.0 - [Changelog](https://github.com/linebender/tiny-skia/blob/main/CHANGELOG.md) - [Commits](https://github.com/linebender/tiny-skia/compare/v0.11.4...v0.12.0) Updates `skrifa` from 0.47.0 to 0.48.0 - [Release notes](https://github.com/googlefonts/fontations/releases) - [Changelog](https://github.com/googlefonts/fontations/blob/main/docs/RELEASE.md) - [Commits](https://github.com/googlefonts/fontations/compare/skrifa-v0.47.0...skrifa-v0.48.0) Updates `harfrust` from 0.12.0 to 0.14.0 - [Release notes](https://github.com/harfbuzz/harfrust/releases) - [Changelog](https://github.com/harfbuzz/harfrust/blob/main/CHANGELOG.md) - [Commits](https://github.com/harfbuzz/harfrust/compare/0.12.0...0.14.0) Updates `quick-xml` from 0.41.0 to 0.42.0 - [Release notes](https://github.com/tafia/quick-xml/releases) - [Changelog](https://github.com/tafia/quick-xml/blob/master/Changelog.md) - [Commits](https://github.com/tafia/quick-xml/compare/v0.41.0...v0.42.0) Updates `egui` from 0.36.2 to 0.37.0 - [Release notes](https://github.com/emilk/egui/releases) - [Changelog](https://github.com/emilk/egui/blob/main/CHANGELOG.md) - [Commits](https://github.com/emilk/egui/compare/0.36.2...0.37.0) Updates `egui_kittest` from 0.36.2 to 0.37.0 - [Release notes](https://github.com/emilk/egui/releases) - [Changelog](https://github.com/emilk/egui/blob/main/CHANGELOG.md) - [Commits](https://github.com/emilk/egui/compare/0.36.2...0.37.0) Updates `eframe` from 0.36.2 to 0.37.0 - [Release notes](https://github.com/emilk/egui/releases) - [Changelog](https://github.com/emilk/egui/blob/main/CHANGELOG.md) - [Commits](https://github.com/emilk/egui/compare/0.36.2...0.37.0) Updates `egui-wgpu` from 0.36.2 to 0.37.0 - [Release notes](https://github.com/emilk/egui/releases) - [Changelog](https://github.com/emilk/egui/blob/main/CHANGELOG.md) - [Commits](https://github.com/emilk/egui/compare/0.36.2...0.37.0) Updates `arboard` from 3.6.1 to 3.7.0 - [Release notes](https://github.com/1Password/arboard/releases) - [Commits](https://github.com/1Password/arboard/compare/v3.6.1...v3.7.0) Updates `acadrust` from 0.6.4 to 0.6.5 - [Release notes](https://github.com/hakanaktt/acadrust/releases) - [Changelog](https://github.com/hakanaktt/acadrust/blob/main/CHANGELOG.md) - [Commits](https://github.com/hakanaktt/acadrust/compare/v0.6.4...v0.6.5) Updates `rstar` from 0.12.2 to 0.13.0 - [Commits](https://github.com/georust/rstar/compare/v0.12.2...v0.13.0) Updates `libc` from 0.2.190 to 0.2.191 - [Release notes](https://github.com/rust-lang/libc/releases) - [Changelog](https://github.com/rust-lang/libc/blob/0.2.191/CHANGELOG.md) - [Commits](https://github.com/rust-lang/libc/compare/0.2.190...0.2.191) Updates `miniz_oxide` from 0.8.9 to 0.9.1 - [Changelog](https://github.com/Frommi/miniz_oxide/blob/master/CHANGELOG.md) - [Commits](https://github.com/Frommi/miniz_oxide/commits) Updates `getrandom` from 0.3.4 to 0.4.3 - [Changelog](https://github.com/rust-random/getrandom/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-random/getrandom/compare/v0.3.4...v0.4.3) Updates `muda` from 0.21.0 to 0.21.2 - [Release notes](https://github.com/tauri-apps/muda/releases) - [Changelog](https://github.com/tauri-apps/muda/blob/dev/CHANGELOG.md) - [Commits](https://github.com/tauri-apps/muda/compare/muda-v0.21...muda-v0.21.2) Updates `winresource` from 0.1.31 to 0.1.34 - [Commits](https://github.com/BenjaminRi/winresource/commits) --- updated-dependencies: - dependency-name: acadrust dependency-version: 0.6.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: cargo-dependencies - dependency-name: arboard dependency-version: 3.7.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: cargo-dependencies - dependency-name: eframe dependency-version: 0.37.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: cargo-dependencies - dependency-name: egui dependency-version: 0.37.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: cargo-dependencies - dependency-name: egui-wgpu dependency-version: 0.37.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: cargo-dependencies - dependency-name: egui_kittest dependency-version: 0.37.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: cargo-dependencies - dependency-name: getrandom dependency-version: 0.4.3 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: cargo-dependencies - dependency-name: harfrust dependency-version: 0.14.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: cargo-dependencies - dependency-name: libc dependency-version: 0.2.191 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: cargo-dependencies - dependency-name: miniz_oxide dependency-version: 0.9.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: cargo-dependencies - dependency-name: muda dependency-version: 0.21.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: cargo-dependencies - dependency-name: quick-xml dependency-version: 0.42.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: cargo-dependencies - dependency-name: rstar dependency-version: 0.13.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: cargo-dependencies - dependency-name: serde_json dependency-version: 1.0.154 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: cargo-dependencies - dependency-name: skrifa dependency-version: 0.48.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: cargo-dependencies - dependency-name: tiny-skia dependency-version: 0.12.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: cargo-dependencies - dependency-name: winresource dependency-version: 0.1.34 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: cargo-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 690 +++++++++++++++++++++------------------ Cargo.toml | 16 +- crates/engine/Cargo.toml | 2 +- crates/io/Cargo.toml | 2 +- crates/mcp/Cargo.toml | 2 +- 5 files changed, 380 insertions(+), 332 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2c24364a..f1bf96a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,20 +4,20 @@ version = 4 [[package]] name = "acadrust" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94862189cab6e282ed0433a801a216cd1a712953813284a48fa81c67223e38f6" +checksum = "5eaa72520f70eb6f7f09b06f44e1ed0d8fad5fa743364f2c3715c30df962ec7c" dependencies = [ "anyhow", "bitflags 2.13.2", "byteorder", "encoding_rs", "flate2", - "foldhash", + "foldhash 0.2.0", "indexmap", "itoa", "memmap2", - "nom", + "nom 7.1.3", "oem_cp", "once_cell", "rayon", @@ -31,6 +31,15 @@ name = "accesskit" version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3b7f7f85a7e5f68090000ed7622545829afd484d210358702ae4cb97dd0c320" +dependencies = [ + "uuid", +] + +[[package]] +name = "accesskit" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad442f58ee04714aaa0ba0a2768c1ea1935b29507bb22ddece8cbbc76db02932" dependencies = [ "enumn", "serde", @@ -39,57 +48,51 @@ dependencies = [ [[package]] name = "accesskit_atspi_common" -version = "0.18.1" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e8c61bee90b42a772d39d06a740207dc71a4e780004ace1db8d99fb1baaa954" +checksum = "52c182f9c282ac9c5638d876d551d15e5f7d397ec263349a0c6a2b61595dd5e4" dependencies = [ - "accesskit", - "accesskit_consumer 0.36.0", + "accesskit 0.25.1", + "accesskit_consumer", "atspi-common", - "phf 0.13.1", + "phf 0.14.0", "serde", "zvariant", ] [[package]] name = "accesskit_consumer" -version = "0.35.0" +version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53cf47daed85312e763fbf85ceca136e0d7abc68e0a7e12abe11f48172bc3b10" +checksum = "8a45e2bb300bea6d5f33a82c3e051c3ddc6f41c209750f68a0acbe991a284344" dependencies = [ - "accesskit", - "hashbrown 0.16.1", + "accesskit 0.25.1", + "hashbrown 0.17.1", ] [[package]] -name = "accesskit_consumer" -version = "0.36.0" +name = "accesskit_ios" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e0d7e25d06f4dc21d1774d67146e9e80d6789216cbd4d1e88185b0095dba60" +checksum = "28e539d64b27fd4c14de8f78ada8f7c944c092f52f7e49fb63316507a57f6937" dependencies = [ - "accesskit", - "hashbrown 0.16.1", -] - -[[package]] -name = "accesskit_consumer" -version = "0.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d10a236f96f87d70732e44520046785431ef01d5bcd6b041317bfadd2f88245" -dependencies = [ - "accesskit", - "hashbrown 0.16.1", + "accesskit 0.25.1", + "accesskit_consumer", + "hashbrown 0.17.1", + "objc2 0.5.3", + "objc2-foundation 0.2.2", + "objc2-ui-kit 0.2.2", ] [[package]] name = "accesskit_macos" -version = "0.26.3" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce02dc63b43f0c9296af9ac946312a2dc8814427d7a64d2d600971dac55b6076" +checksum = "5701624bf6a11efb6d4c8e922bf44126ed96250e5faa9a7a0468c979635839a4" dependencies = [ - "accesskit", - "accesskit_consumer 0.38.0", - "hashbrown 0.16.1", + "accesskit 0.25.1", + "accesskit_consumer", + "hashbrown 0.17.1", "objc2 0.5.3", "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", @@ -97,11 +100,11 @@ dependencies = [ [[package]] name = "accesskit_unix" -version = "0.21.1" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b016ca8db0ea0ea2ceff29a9d6240391492d960716aa471967c00e8cc8cb197c" +checksum = "202f24df034a7476d07b7f74284de84f6d62aabd858dbe7ee9cad3b7ad6f8f9d" dependencies = [ - "accesskit", + "accesskit 0.25.1", "accesskit_atspi_common", "async-channel", "async-executor", @@ -115,13 +118,13 @@ dependencies = [ [[package]] name = "accesskit_windows" -version = "0.32.1" +version = "0.35.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff7009f1a532e917d66970a1e80c965140c6cfbbabbdde3d64e5431e6c78e21" +checksum = "ce63f35d6bdcf59f26b76b3379063f738e6412cef46999cc772d46aa3de35adb" dependencies = [ - "accesskit", - "accesskit_consumer 0.35.0", - "hashbrown 0.16.1", + "accesskit 0.25.1", + "accesskit_consumer", + "hashbrown 0.17.1", "static_assertions", "windows", "windows-core", @@ -129,11 +132,12 @@ dependencies = [ [[package]] name = "accesskit_winit" -version = "0.32.2" +version = "0.34.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fe9a94394896352cc4660ca2288bd4ef883d83238853c038b44070c8f134313" +checksum = "ee96dd150375c05912d8a473f98c2fb155fb3234fac88292d396548a2460fa77" dependencies = [ - "accesskit", + "accesskit 0.25.1", + "accesskit_ios", "accesskit_macos", "accesskit_unix", "accesskit_windows", @@ -209,9 +213,9 @@ checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "arboard" -version = "3.6.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +checksum = "aeb9553e62312c8756a5979254fc6d7da8bbeb5986a1e17037fab932f7037917" dependencies = [ "clipboard-win", "image", @@ -223,7 +227,8 @@ dependencies = [ "objc2-foundation 0.3.2", "parking_lot", "percent-encoding", - "windows-sys 0.60.2", + "windows-sys 0.61.2", + "wl-clipboard-rs", "x11rb", ] @@ -555,7 +560,7 @@ dependencies = [ "cadcraft-ui-egui", "chrono", "eframe", - "egui", + "egui 0.37.0", "hidapi", "log", "muda", @@ -650,8 +655,8 @@ name = "cadcraft-fonts" version = "0.12.0" dependencies = [ "cadcraft-geom", - "harfrust", - "skrifa 0.47.0", + "harfrust 0.14.0", + "skrifa 0.48.0", "unicode-bidi", ] @@ -674,8 +679,8 @@ dependencies = [ "cadcraft-fonts", "cadcraft-geom", "cadcraft-render", - "miniz_oxide 0.8.9", - "quick-xml", + "miniz_oxide 0.9.1", + "quick-xml 0.42.0", "serde", "serde_json", "thiserror 2.0.21", @@ -689,7 +694,7 @@ dependencies = [ "cadcraft-engine", "cadcraft-io", "cadcraft-render", - "getrandom 0.3.4", + "getrandom 0.4.3", "log", "serde_json", ] @@ -720,7 +725,7 @@ dependencies = [ "cadcraft-render", "chrono", "craft-ui", - "egui", + "egui 0.37.0", "egui-wgpu", "egui_kittest", "image", @@ -739,7 +744,7 @@ dependencies = [ "cadcraft-io", "cadcraft-ui-egui", "eframe", - "egui", + "egui 0.37.0", "log", "serde_json", "wasm-bindgen", @@ -761,42 +766,18 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "calloop" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43b32313e937c8721c9d17ee9bf60699a6402b4b7c2cb6ac1bee0ffcaa4b4e5e" -dependencies = [ - "bitflags 2.13.2", - "polling", - "rustix 1.1.5", - "tracing", -] - [[package]] name = "calloop-wayland-source" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" dependencies = [ - "calloop 0.13.0", + "calloop", "rustix 0.38.44", "wayland-backend", "wayland-client", ] -[[package]] -name = "calloop-wayland-source" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" -dependencies = [ - "calloop 0.14.5", - "rustix 1.1.5", - "wayland-backend", - "wayland-client", -] - [[package]] name = "cc" version = "1.6.0" @@ -953,7 +934,7 @@ name = "craft-ui" version = "0.1.0" source = "git+https://github.com/storytold/craft-libs?rev=5c874d51eeb3de70e735c8ce87a59a7dedd47675#5c874d51eeb3de70e735c8ce87a59a7dedd47675" dependencies = [ - "egui", + "egui 0.36.2", "serde", ] @@ -1089,22 +1070,31 @@ name = "ecolor" version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0656a7530b9e2841ee7a5f983965bdd9840c32f4012f759cfd44b912cced6b34" +dependencies = [ + "emath 0.36.2", +] + +[[package]] +name = "ecolor" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31bcbef0d4263a63eb32c4834e5d711613035ce1333468e107d928923e21e6e2" dependencies = [ "bytemuck", - "emath", + "emath 0.37.0", "serde", ] [[package]] name = "eframe" -version = "0.36.2" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55ad41fac6e3149abb4866e93df37dbd33baa95db525c3368c944584e95a5dc2" +checksum = "9c5642501a65ca657d05f00c643a5c14ff2346cb3818032bfe23d58e1f8e5f84" dependencies = [ "ahash", "bytemuck", "document-features", - "egui", + "egui 0.37.0", "egui-wgpu", "egui-winit", "egui_glow", @@ -1122,6 +1112,7 @@ dependencies = [ "profiling", "raw-window-handle", "ron", + "send_wrapper", "serde", "static_assertions", "wasm-bindgen", @@ -1138,11 +1129,31 @@ version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc938cc27cd911415e1e4d151bc56ff9df063d8db081374e87aeb805ea74b2ba" dependencies = [ - "accesskit", + "accesskit 0.24.1", "ahash", "bitflags 2.13.2", - "emath", - "epaint", + "emath 0.36.2", + "epaint 0.36.2", + "itertools", + "log", + "nohash-hasher", + "profiling", + "smallvec", + "unicode-segmentation", + "web-sys", +] + +[[package]] +name = "egui" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec9144f49b7a28b5f395d3147e325b4fe3e77ba686eac11d1bdf0a7da3f375f" +dependencies = [ + "accesskit 0.25.1", + "ahash", + "bitflags 2.13.2", + "emath 0.37.0", + "epaint 0.37.0", "itertools", "log", "nohash-hasher", @@ -1156,15 +1167,15 @@ dependencies = [ [[package]] name = "egui-wgpu" -version = "0.36.2" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cef1f6d1bba40b09a4d0d62cb822b81030922cfbf65c5373398c3a3caafc5bd" +checksum = "0309cbe1febed48af5b974e4bcc424b35e9664863a4d402697147b57a488c864" dependencies = [ "ahash", "bytemuck", "document-features", - "egui", - "epaint", + "egui 0.37.0", + "epaint 0.37.0", "log", "profiling", "thiserror 2.0.21", @@ -1176,35 +1187,37 @@ dependencies = [ [[package]] name = "egui-winit" -version = "0.36.2" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98466000559d66ba4db786a30448b01483946add4188e7f10f522a27b4fb89cc" +checksum = "e4e449f4c4a2a60c7ccb4ba7fa4078417bfe1f9ddc03b5e05ce75748e33a70de" dependencies = [ "accesskit_winit", "arboard", + "block2 0.6.2", "bytemuck", - "egui", + "egui 0.37.0", "log", "objc2 0.6.5", + "objc2-app-kit 0.3.2", "objc2-foundation 0.3.2", "objc2-ui-kit 0.3.2", "profiling", "raw-window-handle", "serde", - "smithay-clipboard", "web-time", "webbrowser", + "windows-sys 0.61.2", "winit", ] [[package]] name = "egui_glow" -version = "0.36.2" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "606ed4167daf341e5753d703285c4c1d562324b2c07aa8f405cd7d066fe58f3b" +checksum = "9275ca5cc35a6b7c94e0e8cedf22542f66cb0cf45ed8ab2657c78ff49fec7268" dependencies = [ "bytemuck", - "egui", + "egui 0.37.0", "glow", "log", "profiling", @@ -1213,13 +1226,13 @@ dependencies = [ [[package]] name = "egui_kittest" -version = "0.36.2" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb3f3effb2726fee23449293e67acbf6cc6ce35525b1722bd5743bb348114d5" +checksum = "0979c2e37860fcf13a2bea77508607f067a9a6a84ba9bd47b5e298582fb59b86" dependencies = [ "dify", "eframe", - "egui", + "egui 0.37.0", "egui-wgpu", "image", "kittest", @@ -1243,6 +1256,12 @@ name = "emath" version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52c8d4141cf0f60fb2f7aefaf352d3ffa5171ef0ab09731c64391801e95e48f1" + +[[package]] +name = "emath" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e73ce90f54750be8672c02be0743fe47530e6f251e066f0ee2869b59eb297af5" dependencies = [ "bytemuck", "serde", @@ -1305,14 +1324,37 @@ name = "epaint" version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0ec0308dc23130fc1a7d4623a8534c2ed069c6dc84e258ee4f3de99615aa1a3" +dependencies = [ + "ahash", + "ecolor 0.36.2", + "emath 0.36.2", + "font-types", + "harfrust 0.12.0", + "log", + "nohash-hasher", + "parking_lot", + "profiling", + "self_cell", + "skrifa 0.44.0", + "smallvec", + "unicode-general-category", + "unicode-segmentation", + "vello_cpu 0.1.0", +] + +[[package]] +name = "epaint" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97bac86ae3dde93fc3951e790951caea739ff39a6c89d909cdaf1d9278dbb1bd" dependencies = [ "ahash", "bytemuck", - "ecolor", - "emath", + "ecolor 0.37.0", + "emath 0.37.0", "epaint_default_fonts", "font-types", - "harfrust", + "harfrust 0.12.0", "log", "nohash-hasher", "parking_lot", @@ -1322,15 +1364,16 @@ dependencies = [ "skrifa 0.44.0", "smallvec", "unicode-general-category", + "unicode-properties", "unicode-segmentation", - "vello_cpu", + "vello_cpu 0.2.0", ] [[package]] name = "epaint_default_fonts" -version = "0.36.2" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773fa9c96dd0dbef887e39d0ed6177f141cce4d68e6041df77570aa3702dfa13" +checksum = "cfc1cc505827f4c23eedc297975a0fd038268d41293f389dd8ff3b574aefab19" [[package]] name = "equivalent" @@ -1416,6 +1459,12 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "flate2" version = "1.1.10" @@ -1433,6 +1482,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foldhash" version = "0.2.0" @@ -1441,9 +1496,9 @@ checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] name = "font-types" -version = "0.12.6" +version = "0.12.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c92d9081c7ab3bd83ef1eecdfad090f3d966e3ce74106779664114cfe1a6274" +checksum = "835ae9d781464a79026cde387f9cbc650c9a4909d7cf7e658b127e3c35d2f35d" dependencies = [ "bytemuck", "serde", @@ -1600,13 +1655,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed4a1bb24121291d27230c1b1b44e07d6a9b28cefdb32fe1581dfb84e14f940a" dependencies = [ "bytemuck", - "foldhash", + "foldhash 0.2.0", "hashbrown 0.17.1", "log", "peniko", "skrifa 0.44.0", "smallvec", - "vello_common", + "vello_common 0.1.0", +] + +[[package]] +name = "glifo" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282a26c1e23de04bdab3e34a21b6f877a479b96737792516b9b8b8f69b6661be" +dependencies = [ + "bytemuck", + "foldhash 0.2.0", + "hashbrown 0.17.1", + "log", + "peniko", + "skrifa 0.44.0", + "smallvec", + "vello_common 0.2.0", ] [[package]] @@ -1734,6 +1805,18 @@ dependencies = [ "smallvec", ] +[[package]] +name = "harfrust" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86aa776d6d32b072119e1f1650f4db843ff4382a4aaa6a2c9c4e35703cd377cb" +dependencies = [ + "bitflags 2.13.2", + "bytemuck", + "read-fonts 0.45.1", + "smallvec", +] + [[package]] name = "hash32" version = "0.3.1" @@ -1743,6 +1826,15 @@ dependencies = [ "byteorder", ] +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + [[package]] name = "hashbrown" version = "0.16.1" @@ -1751,7 +1843,7 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.2.0", ] [[package]] @@ -1760,7 +1852,7 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ - "foldhash", + "foldhash 0.2.0", ] [[package]] @@ -1914,7 +2006,7 @@ dependencies = [ "byteorder-lite", "moxcms", "num-traits", - "png 0.18.1", + "png", "tiff", "zune-core", "zune-jpeg", @@ -2072,12 +2164,12 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "kittest" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ceaa75eb0036a32b6b9833962eb18137449e9817e2e586006471925b727fd5" +checksum = "0bb81a721ba76aa095b7f45deb180504f786904e56ca4b8e141f66eb89a78b75" dependencies = [ - "accesskit", - "accesskit_consumer 0.35.0", + "accesskit 0.25.1", + "accesskit_consumer", ] [[package]] @@ -2100,9 +2192,9 @@ checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" [[package]] name = "libc" -version = "0.2.190" +version = "0.2.191" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" [[package]] name = "libloading" @@ -2271,9 +2363,9 @@ dependencies = [ [[package]] name = "muda" -version = "0.21.0" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6744ab645ebe79f9c428455c7cb9dfa4275fa3b50a25408e08f927da8dc618c" +checksum = "4340e89c178916e62879c5c36b7f699019369be413e496178a6983ce91b8bc03" dependencies = [ "crossbeam-channel", "dpi", @@ -2282,8 +2374,7 @@ dependencies = [ "objc2-app-kit 0.3.2", "objc2-core-foundation", "objc2-foundation 0.3.2", - "once_cell", - "png 0.18.1", + "png", "thiserror 2.0.21", "windows-sys 0.61.2", ] @@ -2390,6 +2481,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2787,6 +2887,16 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "os_pipe" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "parking" version = "2.2.1" @@ -2835,6 +2945,17 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap", +] + [[package]] name = "phf" version = "0.11.3" @@ -2846,33 +2967,33 @@ dependencies = [ [[package]] name = "phf" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +checksum = "010378780309880b08997fae13be7834dba947d36393bd372f2b1556deb2a2f6" dependencies = [ "phf_macros", - "phf_shared 0.13.1", + "phf_shared 0.14.0", "serde", ] [[package]] name = "phf_generator" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +checksum = "aeb62e0959d5a1bebc965f4d15d9e2b7cea002b6b0f5ba8cde6cc26738467100" dependencies = [ "fastrand", - "phf_shared 0.13.1", + "phf_shared 0.14.0", ] [[package]] name = "phf_macros" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +checksum = "5fa8d0ca26d424d27630da600c6624696e7dec8bf7b3b492b383c5dc49e5e085" dependencies = [ "phf_generator", - "phf_shared 0.13.1", + "phf_shared 0.14.0", "proc-macro2", "quote", "syn 2.0.119", @@ -2889,9 +3010,9 @@ dependencies = [ [[package]] name = "phf_shared" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +checksum = "c6fd9027e2d9319be6349febd1db4e8d02aa544921200c9b777720ac34a3aa89" dependencies = [ "siphasher", ] @@ -2945,19 +3066,6 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" -[[package]] -name = "png" -version = "0.17.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" -dependencies = [ - "bitflags 1.3.2", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide 0.8.9", -] - [[package]] name = "png" version = "0.18.1" @@ -3081,6 +3189,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "quick-xml" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.47" @@ -3159,13 +3276,15 @@ dependencies = [ [[package]] name = "read-fonts" -version = "0.44.0" +version = "0.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5a0cf4bb6cbce4c29756270d189a5e400ac05e91192161d83a0bcb6e68492e8" +checksum = "4edc8db0d972de88e53a085b1f01fb3fbdae7bb0cb7a0ce2b2f8285a412fe3ef" dependencies = [ "bytemuck", + "foldhash 0.2.0", "font-types", "once_cell", + "yoke", ] [[package]] @@ -3244,9 +3363,9 @@ dependencies = [ [[package]] name = "rstar" -version = "0.12.2" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "421400d13ccfd26dfa5858199c30a5d76f9c54e0dba7575273025b43c5175dbb" +checksum = "5912b862fa5ffb462607bfd1e35036c458c537921f508c8235a83d5f3987edfe" dependencies = [ "heapless", "num-traits", @@ -3345,6 +3464,12 @@ version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" + [[package]] name = "serde" version = "1.0.229" @@ -3377,9 +3502,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.151" +version = "1.0.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" dependencies = [ "itoa", "memchr", @@ -3464,12 +3589,12 @@ dependencies = [ [[package]] name = "skrifa" -version = "0.47.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "526e654b303ff4ad73a5a40b25de2b3ce3d039c5e3b6da5f89ae6e57004cb40f" +checksum = "a5213b4825bac3587046330052d61bfac7bd3bd0d203c758d7c2ef726fcea034" dependencies = [ "bytemuck", - "read-fonts 0.44.0", + "read-fonts 0.45.1", ] [[package]] @@ -3503,8 +3628,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" dependencies = [ "bitflags 2.13.2", - "calloop 0.13.0", - "calloop-wayland-source 0.3.0", + "calloop", + "calloop-wayland-source", "cursor-icon", "libc", "log", @@ -3521,44 +3646,6 @@ dependencies = [ "xkeysym", ] -[[package]] -name = "smithay-client-toolkit" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" -dependencies = [ - "bitflags 2.13.2", - "calloop 0.14.5", - "calloop-wayland-source 0.4.1", - "cursor-icon", - "libc", - "log", - "memmap2", - "rustix 1.1.5", - "thiserror 2.0.21", - "wayland-backend", - "wayland-client", - "wayland-csd-frame", - "wayland-cursor", - "wayland-protocols", - "wayland-protocols-experimental", - "wayland-protocols-misc", - "wayland-protocols-wlr", - "wayland-scanner", - "xkeysym", -] - -[[package]] -name = "smithay-clipboard" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226" -dependencies = [ - "libc", - "smithay-client-toolkit 0.20.0", - "wayland-backend", -] - [[package]] name = "smol_str" version = "0.2.2" @@ -3715,24 +3802,24 @@ dependencies = [ [[package]] name = "tiny-skia" -version = "0.11.4" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +checksum = "47ffee5eaaf5527f630fb0e356b90ebdec84d5d18d937c5e440350f88c5a91ea" dependencies = [ "arrayref", "arrayvec", "bytemuck", "cfg-if", "log", - "png 0.17.16", + "png", "tiny-skia-path", ] [[package]] name = "tiny-skia-path" -version = "0.11.4" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +checksum = "edca365c3faccca67d06593c5980fa6c57687de727a03131735bb85f01fdeeb9" dependencies = [ "arrayref", "bytemuck", @@ -3815,7 +3902,6 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ - "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -3841,6 +3927,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tree_magic_mini" +version = "3.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6" +dependencies = [ + "memchr", + "nom 8.0.0", + "petgraph", +] + [[package]] name = "type-map" version = "0.5.1" @@ -3885,6 +3982,12 @@ version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + [[package]] name = "unicode-segmentation" version = "1.13.3" @@ -3941,6 +4044,21 @@ dependencies = [ "thiserror 2.0.21", ] +[[package]] +name = "vello_common" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb2141a2bca6e6d598e471fd4d1d7eed8e020aad6a28187edda07f091a325dd" +dependencies = [ + "bytemuck", + "fearless_simd", + "guillotiere", + "log", + "peniko", + "smallvec", + "thiserror 2.0.21", +] + [[package]] name = "vello_cpu" version = "0.1.0" @@ -3948,9 +4066,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac7349e1f55f6b801c7c277958df4ea53e7f20f21e8014910ad888b2ecda93ea" dependencies = [ "bytemuck", - "glifo", + "glifo 0.2.0", "hashbrown 0.17.1", - "vello_common", + "vello_common 0.1.0", +] + +[[package]] +name = "vello_cpu" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace506ca414548966fcf3c283be94eb1cad8254de488791ee698bf438b0890a9" +dependencies = [ + "bytemuck", + "glifo 0.3.0", + "hashbrown 0.17.1", + "vello_common 0.2.0", ] [[package]] @@ -4094,32 +4224,6 @@ dependencies = [ "wayland-scanner", ] -[[package]] -name = "wayland-protocols-experimental" -version = "20250721.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" -dependencies = [ - "bitflags 2.13.2", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols-misc" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8" -dependencies = [ - "bitflags 2.13.2", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-scanner", -] - [[package]] name = "wayland-protocols-plasma" version = "0.3.12" @@ -4153,7 +4257,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" dependencies = [ "proc-macro2", - "quick-xml", + "quick-xml 0.41.0", "quote", ] @@ -4510,7 +4614,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -4519,16 +4623,7 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", + "windows-targets", ] [[package]] @@ -4546,31 +4641,14 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] @@ -4588,96 +4666,48 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "winit" version = "0.30.13" @@ -4690,7 +4720,7 @@ dependencies = [ "bitflags 2.13.2", "block2 0.5.1", "bytemuck", - "calloop 0.13.0", + "calloop", "cfg_aliases", "concurrent-queue", "core-foundation", @@ -4711,7 +4741,7 @@ dependencies = [ "raw-window-handle", "redox_syscall 0.4.1", "rustix 0.38.44", - "smithay-client-toolkit 0.19.2", + "smithay-client-toolkit", "smol_str", "tracing", "unicode-segmentation", @@ -4740,9 +4770,9 @@ dependencies = [ [[package]] name = "winresource" -version = "0.1.31" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0986a8b1d586b7d3e4fe3d9ea39fb451ae22869dcea4aa109d287a374d866087" +checksum = "a2bc6d36480b829272a5bdabf08e6dca8004761c1da7e35efef800540c66221b" dependencies = [ "toml", "version_check", @@ -4754,6 +4784,24 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +[[package]] +name = "wl-clipboard-rs" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d7888ccd4896447b2d14d3a9350a85df2aeb6f181e2e7a31349d104ac46cac1" +dependencies = [ + "libc", + "log", + "os_pipe", + "rustix 1.1.5", + "thiserror 2.0.21", + "tree_magic_mini", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", +] + [[package]] name = "writeable" version = "0.6.4" diff --git a/Cargo.toml b/Cargo.toml index 7ac093d0..958318b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,24 +37,24 @@ chrono = { version = "0.4.45", default-features = false, features = ["std"] } rayon = "1" # rendering / formats -tiny-skia = { version = "0.11", default-features = false, features = ["std", "simd", "png-format"] } +tiny-skia = { version = "0.12", default-features = false, features = ["std", "simd", "png-format"] } lyon_tessellation = "1.0.22" -skrifa = "0.47" +skrifa = "0.48" # Text shaping (OpenType GSUB/GPOS, HarfBuzz port on read-fonts; the version egui's epaint uses) # and the Unicode Bidirectional Algorithm for right-to-left drawing text. -harfrust = "0.12" +harfrust = "0.14" unicode-bidi = "0.3.18" image = { version = "0.25", default-features = false, features = ["png", "jpeg"] } encoding_rs = "0.8.35" # XML for the SVG reader (streaming parser, no entity expansion beyond the five predefined ones) -quick-xml = "0.41" +quick-xml = "0.42" # ui craft-ui = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "5c874d51eeb3de70e735c8ce87a59a7dedd47675", features = ["serde"] } -egui = "0.36" -egui_kittest = "0.36" -eframe = { version = "0.36", default-features = false } -egui-wgpu = "0.36" +egui = "0.37" +egui_kittest = "0.37" +eframe = { version = "0.37", default-features = false } +egui-wgpu = "0.37" rfd = "0.17" arboard = { version = "3.6", default-features = false } diff --git a/crates/engine/Cargo.toml b/crates/engine/Cargo.toml index 04be2919..8be55a2a 100644 --- a/crates/engine/Cargo.toml +++ b/crates/engine/Cargo.toml @@ -22,7 +22,7 @@ serde_json = { workspace = true } thiserror = { workspace = true } log = { workspace = true } # R-tree spatial index for picking, window selection and object snaps (MIT OR Apache-2.0). -rstar = { version = "0.12", default-features = false } +rstar = { version = "0.13", default-features = false } [target.'cfg(unix)'.dependencies] libc = "0.2" diff --git a/crates/io/Cargo.toml b/crates/io/Cargo.toml index 84adfec4..b056603c 100644 --- a/crates/io/Cargo.toml +++ b/crates/io/Cargo.toml @@ -19,7 +19,7 @@ cadcraft-fonts = { workspace = true } thiserror = { workspace = true } serde_json = { workspace = true } serde = { workspace = true } -miniz_oxide = "0.8" +miniz_oxide = "0.9" quick-xml = { workspace = true } [dev-dependencies] diff --git a/crates/mcp/Cargo.toml b/crates/mcp/Cargo.toml index c4452955..955a6b6d 100644 --- a/crates/mcp/Cargo.toml +++ b/crates/mcp/Cargo.toml @@ -15,4 +15,4 @@ cadcraft-io = { workspace = true } serde_json = { workspace = true } log = { workspace = true } # Secure randomness for the per-session control token (MIT OR Apache-2.0). -getrandom = "0.3" +getrandom = "0.4" From 611eb9f5a92ea70c3a19ea964d521232184f8649 Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Fri, 23 Oct 2026 08:44:31 +0200 Subject: [PATCH 08/19] fonts: port complex-text shaping to harfrust 0.14 --- crates/fonts/src/complex.rs | 47 ++++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/crates/fonts/src/complex.rs b/crates/fonts/src/complex.rs index 6193f3f8..1d76f813 100644 --- a/crates/fonts/src/complex.rs +++ b/crates/fonts/src/complex.rs @@ -8,6 +8,7 @@ use std::ops::Range; use std::panic::{AssertUnwindSafe, catch_unwind}; +use std::sync::Arc; use cadcraft_geom::Vec2; @@ -182,28 +183,31 @@ struct Glyph { /// Shape characters `r` of `cs` (one direction, one face) into glyphs in display order. fn shape_run(face: &Face, cs: &[char], r: Range, rtl: bool) -> Vec { let run = cs.get(r.clone()).unwrap_or_default(); - let Ok(font) = harfrust::FontRef::from_index(&face.bytes, face.index) else { return Vec::new() }; - let data = harfrust::ShaperData::new(&font); - let shaper = data.shaper(&font).build(); - let mut buf = harfrust::UnicodeBuffer::new(); + // Shares the face's bytes instead of copying them. + let bytes: Arc + Send + Sync> = face.bytes.clone(); + let Some(font) = harfrust::Font::new(bytes, face.index) else { return Vec::new() }; + let shaper = harfrust::ShaperFont::new(&font); + let mut buf = harfrust::Buffer::new(); let arabic = run.iter().any(|c| matches!(*c, '\u{0600}'..='\u{08FF}')); if arabic && !face.has_joining_features() { for (c, i) in bidi::presentation_forms(run, |p| face.has_glyph(p)) { - buf.add(c, u32::try_from(r.start + i).unwrap_or(u32::MAX)); + buf.push(u32::from(c), u32::try_from(r.start + i).unwrap_or(u32::MAX)); } } else { for (i, c) in run.iter().enumerate() { - buf.add(*c, u32::try_from(r.start + i).unwrap_or(u32::MAX)); + buf.push(u32::from(*c), u32::try_from(r.start + i).unwrap_or(u32::MAX)); } } buf.set_direction(if rtl { harfrust::Direction::RightToLeft } else { harfrust::Direction::LeftToRight }); buf.guess_segment_properties(); - let shaped = shaper.shape(buf, harfrust::ShapeOptions::new()); - let k = 1000.0 / f64::from(shaper.units_per_em().max(1)); - shaped - .glyph_infos() + // Without a plan, shaping fails only on an already shaped buffer or an unset direction. + if harfrust::shape(&shaper, &mut buf, harfrust::ShapeOptions::new()).is_err() { + return Vec::new(); + } + let k = 1000.0 / f64::from(font.units_per_em().max(1)); + buf.glyph_infos() .iter() - .zip(shaped.glyph_positions()) + .zip(buf.glyph_positions()) .map(|(gi, gp)| Glyph { gid: gi.glyph_id, cluster: usize::try_from(gi.cluster).unwrap_or(usize::MAX), @@ -291,6 +295,27 @@ mod tests { assert!((crate::line_width("قیمت 25 تومان", 1.0, 1.0) - sh.width).abs() < 1e-9); } + #[test] + fn devanagari_run_is_reordered_with_line_clusters_and_advances_per_1000_em() { + use skrifa::MetadataProvider; + use skrifa::instance::{LocationRef, Size}; + let Some(face) = ttf::fallback_for('\u{0915}') else { return }; + let Some(ka) = nominal(&face, '\u{0915}') else { return }; + // "कि" after two other characters of the line: the vowel sign i is drawn before the + // consonant (Indic reordering, which needs the script the shaper guesses from the text), + // both glyphs carry the cluster of the consonant's index in the line, not in the run. + let cs: Vec = "ab\u{0915}\u{093F}".chars().collect(); + let gs = shape_run(&face, &cs, 2..4, false); + let gids: Vec = gs.iter().map(|g| g.gid).collect(); + assert_eq!(gids.len(), 2, "{gids:?}"); + assert!(gids.first() != Some(&ka) && gids.last() == Some(&ka), "vowel sign before ka: {gids:?}"); + assert!(gs.iter().all(|g| g.cluster == 2), "{:?}", gs.iter().map(|g| g.cluster).collect::>()); + // Advances come out at 1000 units per em, whatever the font's own em. + let hmtx = face.font().and_then(|f| f.glyph_metrics(Size::new(1000.0), LocationRef::default()).advance_width(skrifa::GlyphId::new(ka))); + let adv = gs.last().map(|g| g.advance); + assert!(matches!((adv, hmtx), (Some(a), Some(h)) if (a - f64::from(h)).abs() < 1e-3), "{adv:?} vs {hmtx:?}"); + } + #[test] fn hostile_rtl_lines_never_panic() { for s in ["\u{202E}%%u\u{05D0}%%", "\u{2067}\u{0644}\u{0627}\u{2069}%%o", "\u{064E}\u{064E}\u{05D0}", "\u{05D0}\u{E000}\u{10FFFF}"] { From e61e328edb1e08a3f3f767dc9391e6abc716331a Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Fri, 23 Oct 2026 08:50:32 +0200 Subject: [PATCH 09/19] io: read SVG element and attribute names with quick-xml 0.42 --- crates/io/src/svg_read.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/io/src/svg_read.rs b/crates/io/src/svg_read.rs index 1d150fc8..4b2892c3 100644 --- a/crates/io/src/svg_read.rs +++ b/crates/io/src/svg_read.rs @@ -207,15 +207,15 @@ impl Parser { match reader.read_event().map_err(parse_err)? { Event::Start(e) => { let name = e.name(); - self.on_start(name.as_ref(), &e)?; + self.on_start(name.into_inner().as_bytes(), &e)?; } Event::Empty(e) => { let name = e.name(); - self.on_element(name.as_ref(), &e)?; + self.on_element(name.into_inner().as_bytes(), &e)?; } Event::End(e) => { let name = e.name(); - self.on_end(name.as_ref()); + self.on_end(name.into_inner().as_bytes()); } Event::Eof => break, _ => {} @@ -578,7 +578,7 @@ fn hidden(e: &BytesStart) -> bool { fn attr(e: &BytesStart, name: &[u8]) -> Option { for a in e.attributes().flatten() { - if a.key.as_ref() == name { + if a.key.into_inner().as_bytes() == name { return a.normalized_value(XmlVersion::Explicit1_0).ok().map(|v| v.into_owned()); } } From f344cf4460be9916958c97639d8a1d1833e03c03 Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Fri, 23 Oct 2026 08:50:32 +0200 Subject: [PATCH 10/19] engine: pass the query envelope by value to rstar 0.13 --- crates/engine/src/spatial.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/engine/src/spatial.rs b/crates/engine/src/spatial.rs index 98999b71..ca15bd4c 100644 --- a/crates/engine/src/spatial.rs +++ b/crates/engine/src/spatial.rs @@ -208,7 +208,7 @@ impl SpatialIndex { let pad = (b.width() + b.height()) * 1e-9 + (b.min.x.abs() + b.min.y.abs() + b.max.x.abs() + b.max.y.abs()) * 1e-12 + 1e-300; let env = AABB::from_corners([b.min.x - pad, b.min.y - pad], [b.max.x + pad, b.max.y + pad]); let mut out: Vec = - self.tree.locate_in_envelope_intersecting(&env).filter_map(|it| self.offsets.get(&it.data.0).map(|off| off + it.data.1)).collect(); + self.tree.locate_in_envelope_intersecting(env).filter_map(|it| self.offsets.get(&it.data.0).map(|off| off + it.data.1)).collect(); if with_infinite { out.extend_from_slice(&self.infinite); } From 5648a06ac438c54fff1392f17d57448e83e31817 Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Fri, 23 Oct 2026 09:14:51 +0200 Subject: [PATCH 11/19] Hold the egui crates at 0.36 until craft-ui moves to 0.37 (craft-libs#199) --- Cargo.lock | 489 ++++++++++++++++++++--------------------------------- Cargo.toml | 8 +- 2 files changed, 192 insertions(+), 305 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f1bf96a7..e5076e82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,11 +13,11 @@ dependencies = [ "byteorder", "encoding_rs", "flate2", - "foldhash 0.2.0", + "foldhash", "indexmap", "itoa", "memmap2", - "nom 7.1.3", + "nom", "oem_cp", "once_cell", "rayon", @@ -31,15 +31,6 @@ name = "accesskit" version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3b7f7f85a7e5f68090000ed7622545829afd484d210358702ae4cb97dd0c320" -dependencies = [ - "uuid", -] - -[[package]] -name = "accesskit" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad442f58ee04714aaa0ba0a2768c1ea1935b29507bb22ddece8cbbc76db02932" dependencies = [ "enumn", "serde", @@ -48,51 +39,57 @@ dependencies = [ [[package]] name = "accesskit_atspi_common" -version = "0.21.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52c182f9c282ac9c5638d876d551d15e5f7d397ec263349a0c6a2b61595dd5e4" +checksum = "1e8c61bee90b42a772d39d06a740207dc71a4e780004ace1db8d99fb1baaa954" dependencies = [ - "accesskit 0.25.1", - "accesskit_consumer", + "accesskit", + "accesskit_consumer 0.36.0", "atspi-common", - "phf 0.14.0", + "phf 0.13.1", "serde", "zvariant", ] [[package]] name = "accesskit_consumer" -version = "0.39.1" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a45e2bb300bea6d5f33a82c3e051c3ddc6f41c209750f68a0acbe991a284344" +checksum = "53cf47daed85312e763fbf85ceca136e0d7abc68e0a7e12abe11f48172bc3b10" dependencies = [ - "accesskit 0.25.1", - "hashbrown 0.17.1", + "accesskit", + "hashbrown 0.16.1", ] [[package]] -name = "accesskit_ios" -version = "0.2.1" +name = "accesskit_consumer" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28e539d64b27fd4c14de8f78ada8f7c944c092f52f7e49fb63316507a57f6937" +checksum = "25e0d7e25d06f4dc21d1774d67146e9e80d6789216cbd4d1e88185b0095dba60" dependencies = [ - "accesskit 0.25.1", - "accesskit_consumer", - "hashbrown 0.17.1", - "objc2 0.5.3", - "objc2-foundation 0.2.2", - "objc2-ui-kit 0.2.2", + "accesskit", + "hashbrown 0.16.1", +] + +[[package]] +name = "accesskit_consumer" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d10a236f96f87d70732e44520046785431ef01d5bcd6b041317bfadd2f88245" +dependencies = [ + "accesskit", + "hashbrown 0.16.1", ] [[package]] name = "accesskit_macos" -version = "0.27.1" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5701624bf6a11efb6d4c8e922bf44126ed96250e5faa9a7a0468c979635839a4" +checksum = "ce02dc63b43f0c9296af9ac946312a2dc8814427d7a64d2d600971dac55b6076" dependencies = [ - "accesskit 0.25.1", - "accesskit_consumer", - "hashbrown 0.17.1", + "accesskit", + "accesskit_consumer 0.38.0", + "hashbrown 0.16.1", "objc2 0.5.3", "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", @@ -100,11 +97,11 @@ dependencies = [ [[package]] name = "accesskit_unix" -version = "0.24.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "202f24df034a7476d07b7f74284de84f6d62aabd858dbe7ee9cad3b7ad6f8f9d" +checksum = "b016ca8db0ea0ea2ceff29a9d6240391492d960716aa471967c00e8cc8cb197c" dependencies = [ - "accesskit 0.25.1", + "accesskit", "accesskit_atspi_common", "async-channel", "async-executor", @@ -118,13 +115,13 @@ dependencies = [ [[package]] name = "accesskit_windows" -version = "0.35.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce63f35d6bdcf59f26b76b3379063f738e6412cef46999cc772d46aa3de35adb" +checksum = "eff7009f1a532e917d66970a1e80c965140c6cfbbabbdde3d64e5431e6c78e21" dependencies = [ - "accesskit 0.25.1", - "accesskit_consumer", - "hashbrown 0.17.1", + "accesskit", + "accesskit_consumer 0.35.0", + "hashbrown 0.16.1", "static_assertions", "windows", "windows-core", @@ -132,12 +129,11 @@ dependencies = [ [[package]] name = "accesskit_winit" -version = "0.34.1" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee96dd150375c05912d8a473f98c2fb155fb3234fac88292d396548a2460fa77" +checksum = "1fe9a94394896352cc4660ca2288bd4ef883d83238853c038b44070c8f134313" dependencies = [ - "accesskit 0.25.1", - "accesskit_ios", + "accesskit", "accesskit_macos", "accesskit_unix", "accesskit_windows", @@ -228,7 +224,6 @@ dependencies = [ "parking_lot", "percent-encoding", "windows-sys 0.61.2", - "wl-clipboard-rs", "x11rb", ] @@ -560,7 +555,7 @@ dependencies = [ "cadcraft-ui-egui", "chrono", "eframe", - "egui 0.37.0", + "egui", "hidapi", "log", "muda", @@ -725,7 +720,7 @@ dependencies = [ "cadcraft-render", "chrono", "craft-ui", - "egui 0.37.0", + "egui", "egui-wgpu", "egui_kittest", "image", @@ -744,7 +739,7 @@ dependencies = [ "cadcraft-io", "cadcraft-ui-egui", "eframe", - "egui 0.37.0", + "egui", "log", "serde_json", "wasm-bindgen", @@ -766,18 +761,42 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "calloop" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b32313e937c8721c9d17ee9bf60699a6402b4b7c2cb6ac1bee0ffcaa4b4e5e" +dependencies = [ + "bitflags 2.13.2", + "polling", + "rustix 1.1.5", + "tracing", +] + [[package]] name = "calloop-wayland-source" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" dependencies = [ - "calloop", + "calloop 0.13.0", "rustix 0.38.44", "wayland-backend", "wayland-client", ] +[[package]] +name = "calloop-wayland-source" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" +dependencies = [ + "calloop 0.14.5", + "rustix 1.1.5", + "wayland-backend", + "wayland-client", +] + [[package]] name = "cc" version = "1.6.0" @@ -934,7 +953,7 @@ name = "craft-ui" version = "0.1.0" source = "git+https://github.com/storytold/craft-libs?rev=5c874d51eeb3de70e735c8ce87a59a7dedd47675#5c874d51eeb3de70e735c8ce87a59a7dedd47675" dependencies = [ - "egui 0.36.2", + "egui", "serde", ] @@ -1070,31 +1089,22 @@ name = "ecolor" version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0656a7530b9e2841ee7a5f983965bdd9840c32f4012f759cfd44b912cced6b34" -dependencies = [ - "emath 0.36.2", -] - -[[package]] -name = "ecolor" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31bcbef0d4263a63eb32c4834e5d711613035ce1333468e107d928923e21e6e2" dependencies = [ "bytemuck", - "emath 0.37.0", + "emath", "serde", ] [[package]] name = "eframe" -version = "0.37.0" +version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c5642501a65ca657d05f00c643a5c14ff2346cb3818032bfe23d58e1f8e5f84" +checksum = "55ad41fac6e3149abb4866e93df37dbd33baa95db525c3368c944584e95a5dc2" dependencies = [ "ahash", "bytemuck", "document-features", - "egui 0.37.0", + "egui", "egui-wgpu", "egui-winit", "egui_glow", @@ -1112,7 +1122,6 @@ dependencies = [ "profiling", "raw-window-handle", "ron", - "send_wrapper", "serde", "static_assertions", "wasm-bindgen", @@ -1129,31 +1138,11 @@ version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc938cc27cd911415e1e4d151bc56ff9df063d8db081374e87aeb805ea74b2ba" dependencies = [ - "accesskit 0.24.1", + "accesskit", "ahash", "bitflags 2.13.2", - "emath 0.36.2", - "epaint 0.36.2", - "itertools", - "log", - "nohash-hasher", - "profiling", - "smallvec", - "unicode-segmentation", - "web-sys", -] - -[[package]] -name = "egui" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec9144f49b7a28b5f395d3147e325b4fe3e77ba686eac11d1bdf0a7da3f375f" -dependencies = [ - "accesskit 0.25.1", - "ahash", - "bitflags 2.13.2", - "emath 0.37.0", - "epaint 0.37.0", + "emath", + "epaint", "itertools", "log", "nohash-hasher", @@ -1167,15 +1156,15 @@ dependencies = [ [[package]] name = "egui-wgpu" -version = "0.37.0" +version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0309cbe1febed48af5b974e4bcc424b35e9664863a4d402697147b57a488c864" +checksum = "5cef1f6d1bba40b09a4d0d62cb822b81030922cfbf65c5373398c3a3caafc5bd" dependencies = [ "ahash", "bytemuck", "document-features", - "egui 0.37.0", - "epaint 0.37.0", + "egui", + "epaint", "log", "profiling", "thiserror 2.0.21", @@ -1187,37 +1176,35 @@ dependencies = [ [[package]] name = "egui-winit" -version = "0.37.0" +version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4e449f4c4a2a60c7ccb4ba7fa4078417bfe1f9ddc03b5e05ce75748e33a70de" +checksum = "98466000559d66ba4db786a30448b01483946add4188e7f10f522a27b4fb89cc" dependencies = [ "accesskit_winit", "arboard", - "block2 0.6.2", "bytemuck", - "egui 0.37.0", + "egui", "log", "objc2 0.6.5", - "objc2-app-kit 0.3.2", "objc2-foundation 0.3.2", "objc2-ui-kit 0.3.2", "profiling", "raw-window-handle", "serde", + "smithay-clipboard", "web-time", "webbrowser", - "windows-sys 0.61.2", "winit", ] [[package]] name = "egui_glow" -version = "0.37.0" +version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9275ca5cc35a6b7c94e0e8cedf22542f66cb0cf45ed8ab2657c78ff49fec7268" +checksum = "606ed4167daf341e5753d703285c4c1d562324b2c07aa8f405cd7d066fe58f3b" dependencies = [ "bytemuck", - "egui 0.37.0", + "egui", "glow", "log", "profiling", @@ -1226,13 +1213,13 @@ dependencies = [ [[package]] name = "egui_kittest" -version = "0.37.0" +version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0979c2e37860fcf13a2bea77508607f067a9a6a84ba9bd47b5e298582fb59b86" +checksum = "5eb3f3effb2726fee23449293e67acbf6cc6ce35525b1722bd5743bb348114d5" dependencies = [ "dify", "eframe", - "egui 0.37.0", + "egui", "egui-wgpu", "image", "kittest", @@ -1256,12 +1243,6 @@ name = "emath" version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52c8d4141cf0f60fb2f7aefaf352d3ffa5171ef0ab09731c64391801e95e48f1" - -[[package]] -name = "emath" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e73ce90f54750be8672c02be0743fe47530e6f251e066f0ee2869b59eb297af5" dependencies = [ "bytemuck", "serde", @@ -1324,34 +1305,11 @@ name = "epaint" version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0ec0308dc23130fc1a7d4623a8534c2ed069c6dc84e258ee4f3de99615aa1a3" -dependencies = [ - "ahash", - "ecolor 0.36.2", - "emath 0.36.2", - "font-types", - "harfrust 0.12.0", - "log", - "nohash-hasher", - "parking_lot", - "profiling", - "self_cell", - "skrifa 0.44.0", - "smallvec", - "unicode-general-category", - "unicode-segmentation", - "vello_cpu 0.1.0", -] - -[[package]] -name = "epaint" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97bac86ae3dde93fc3951e790951caea739ff39a6c89d909cdaf1d9278dbb1bd" dependencies = [ "ahash", "bytemuck", - "ecolor 0.37.0", - "emath 0.37.0", + "ecolor", + "emath", "epaint_default_fonts", "font-types", "harfrust 0.12.0", @@ -1364,16 +1322,15 @@ dependencies = [ "skrifa 0.44.0", "smallvec", "unicode-general-category", - "unicode-properties", "unicode-segmentation", - "vello_cpu 0.2.0", + "vello_cpu", ] [[package]] name = "epaint_default_fonts" -version = "0.37.0" +version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfc1cc505827f4c23eedc297975a0fd038268d41293f389dd8ff3b574aefab19" +checksum = "773fa9c96dd0dbef887e39d0ed6177f141cce4d68e6041df77570aa3702dfa13" [[package]] name = "equivalent" @@ -1459,12 +1416,6 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" -[[package]] -name = "fixedbitset" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" - [[package]] name = "flate2" version = "1.1.10" @@ -1482,12 +1433,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - [[package]] name = "foldhash" version = "0.2.0" @@ -1655,29 +1600,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed4a1bb24121291d27230c1b1b44e07d6a9b28cefdb32fe1581dfb84e14f940a" dependencies = [ "bytemuck", - "foldhash 0.2.0", + "foldhash", "hashbrown 0.17.1", "log", "peniko", "skrifa 0.44.0", "smallvec", - "vello_common 0.1.0", -] - -[[package]] -name = "glifo" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282a26c1e23de04bdab3e34a21b6f877a479b96737792516b9b8b8f69b6661be" -dependencies = [ - "bytemuck", - "foldhash 0.2.0", - "hashbrown 0.17.1", - "log", - "peniko", - "skrifa 0.44.0", - "smallvec", - "vello_common 0.2.0", + "vello_common", ] [[package]] @@ -1826,15 +1755,6 @@ dependencies = [ "byteorder", ] -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash 0.1.5", -] - [[package]] name = "hashbrown" version = "0.16.1" @@ -1843,7 +1763,7 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", - "foldhash 0.2.0", + "foldhash", ] [[package]] @@ -1852,7 +1772,7 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ - "foldhash 0.2.0", + "foldhash", ] [[package]] @@ -2164,12 +2084,12 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "kittest" -version = "0.5.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb81a721ba76aa095b7f45deb180504f786904e56ca4b8e141f66eb89a78b75" +checksum = "90ceaa75eb0036a32b6b9833962eb18137449e9817e2e586006471925b727fd5" dependencies = [ - "accesskit 0.25.1", - "accesskit_consumer", + "accesskit", + "accesskit_consumer 0.35.0", ] [[package]] @@ -2481,15 +2401,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "nom" -version = "8.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" -dependencies = [ - "memchr", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -2887,16 +2798,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "os_pipe" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - [[package]] name = "parking" version = "2.2.1" @@ -2945,17 +2846,6 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" -[[package]] -name = "petgraph" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" -dependencies = [ - "fixedbitset", - "hashbrown 0.15.5", - "indexmap", -] - [[package]] name = "phf" version = "0.11.3" @@ -2967,33 +2857,33 @@ dependencies = [ [[package]] name = "phf" -version = "0.14.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "010378780309880b08997fae13be7834dba947d36393bd372f2b1556deb2a2f6" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" dependencies = [ "phf_macros", - "phf_shared 0.14.0", + "phf_shared 0.13.1", "serde", ] [[package]] name = "phf_generator" -version = "0.14.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aeb62e0959d5a1bebc965f4d15d9e2b7cea002b6b0f5ba8cde6cc26738467100" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" dependencies = [ "fastrand", - "phf_shared 0.14.0", + "phf_shared 0.13.1", ] [[package]] name = "phf_macros" -version = "0.14.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fa8d0ca26d424d27630da600c6624696e7dec8bf7b3b492b383c5dc49e5e085" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" dependencies = [ "phf_generator", - "phf_shared 0.14.0", + "phf_shared 0.13.1", "proc-macro2", "quote", "syn 2.0.119", @@ -3010,9 +2900,9 @@ dependencies = [ [[package]] name = "phf_shared" -version = "0.14.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fd9027e2d9319be6349febd1db4e8d02aa544921200c9b777720ac34a3aa89" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" dependencies = [ "siphasher", ] @@ -3281,7 +3171,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4edc8db0d972de88e53a085b1f01fb3fbdae7bb0cb7a0ce2b2f8285a412fe3ef" dependencies = [ "bytemuck", - "foldhash 0.2.0", + "foldhash", "font-types", "once_cell", "yoke", @@ -3464,12 +3354,6 @@ version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" -[[package]] -name = "send_wrapper" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" - [[package]] name = "serde" version = "1.0.229" @@ -3628,8 +3512,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" dependencies = [ "bitflags 2.13.2", - "calloop", - "calloop-wayland-source", + "calloop 0.13.0", + "calloop-wayland-source 0.3.0", "cursor-icon", "libc", "log", @@ -3646,6 +3530,44 @@ dependencies = [ "xkeysym", ] +[[package]] +name = "smithay-client-toolkit" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" +dependencies = [ + "bitflags 2.13.2", + "calloop 0.14.5", + "calloop-wayland-source 0.4.1", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 1.1.5", + "thiserror 2.0.21", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-experimental", + "wayland-protocols-misc", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-clipboard" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226" +dependencies = [ + "libc", + "smithay-client-toolkit 0.20.0", + "wayland-backend", +] + [[package]] name = "smol_str" version = "0.2.2" @@ -3902,6 +3824,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -3927,17 +3850,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "tree_magic_mini" -version = "3.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6" -dependencies = [ - "memchr", - "nom 8.0.0", - "petgraph", -] - [[package]] name = "type-map" version = "0.5.1" @@ -3982,12 +3894,6 @@ version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" -[[package]] -name = "unicode-properties" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" - [[package]] name = "unicode-segmentation" version = "1.13.3" @@ -4044,21 +3950,6 @@ dependencies = [ "thiserror 2.0.21", ] -[[package]] -name = "vello_common" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb2141a2bca6e6d598e471fd4d1d7eed8e020aad6a28187edda07f091a325dd" -dependencies = [ - "bytemuck", - "fearless_simd", - "guillotiere", - "log", - "peniko", - "smallvec", - "thiserror 2.0.21", -] - [[package]] name = "vello_cpu" version = "0.1.0" @@ -4066,21 +3957,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac7349e1f55f6b801c7c277958df4ea53e7f20f21e8014910ad888b2ecda93ea" dependencies = [ "bytemuck", - "glifo 0.2.0", + "glifo", "hashbrown 0.17.1", - "vello_common 0.1.0", -] - -[[package]] -name = "vello_cpu" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace506ca414548966fcf3c283be94eb1cad8254de488791ee698bf438b0890a9" -dependencies = [ - "bytemuck", - "glifo 0.3.0", - "hashbrown 0.17.1", - "vello_common 0.2.0", + "vello_common", ] [[package]] @@ -4224,6 +4103,32 @@ dependencies = [ "wayland-scanner", ] +[[package]] +name = "wayland-protocols-experimental" +version = "20250721.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-misc" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + [[package]] name = "wayland-protocols-plasma" version = "0.3.12" @@ -4720,7 +4625,7 @@ dependencies = [ "bitflags 2.13.2", "block2 0.5.1", "bytemuck", - "calloop", + "calloop 0.13.0", "cfg_aliases", "concurrent-queue", "core-foundation", @@ -4741,7 +4646,7 @@ dependencies = [ "raw-window-handle", "redox_syscall 0.4.1", "rustix 0.38.44", - "smithay-client-toolkit", + "smithay-client-toolkit 0.19.2", "smol_str", "tracing", "unicode-segmentation", @@ -4784,24 +4689,6 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" -[[package]] -name = "wl-clipboard-rs" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d7888ccd4896447b2d14d3a9350a85df2aeb6f181e2e7a31349d104ac46cac1" -dependencies = [ - "libc", - "log", - "os_pipe", - "rustix 1.1.5", - "thiserror 2.0.21", - "tree_magic_mini", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-protocols-wlr", -] - [[package]] name = "writeable" version = "0.6.4" diff --git a/Cargo.toml b/Cargo.toml index 958318b3..a0aea657 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,10 +51,10 @@ quick-xml = "0.42" # ui craft-ui = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "5c874d51eeb3de70e735c8ce87a59a7dedd47675", features = ["serde"] } -egui = "0.37" -egui_kittest = "0.37" -eframe = { version = "0.37", default-features = false } -egui-wgpu = "0.37" +egui = "0.36" +egui_kittest = "0.36" +eframe = { version = "0.36", default-features = false } +egui-wgpu = "0.36" rfd = "0.17" arboard = { version = "3.6", default-features = false } From 9621dcbd9442606f640e489d664733c85a3e5679 Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Fri, 23 Oct 2026 09:30:10 +0200 Subject: [PATCH 12/19] dwg: drop the dimension bit-0 inversion that acadrust 0.6.5 now does itself --- crates/dwg/src/lib.rs | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/crates/dwg/src/lib.rs b/crates/dwg/src/lib.rs index 27322924..f8a48f2a 100644 --- a/crates/dwg/src/lib.rs +++ b/crates/dwg/src/lib.rs @@ -107,7 +107,6 @@ mod native { current_style_names(&mut doc); table_merges(&mut doc); table_style_rows(&mut doc, false); - dimension_flags(&mut doc, false); let orphans = drop_orphaned_dgn_components(&mut doc); if orphans > 0 { let (s, were) = if orphans == 1 { ("", "was") } else { ("s", "were") }; @@ -291,24 +290,14 @@ mod native { } } - /// The DWG dimension flags byte. Its bit 0 is the opposite of DXF group 70's bit 128 (set: the - /// text is at its default position) and its bit 1 is group 70's bit 32 (ODA DWG - /// specification, common dimension data), while acadrust 0.6.4 reads and writes bit 0 as - /// `text_user_positioned` itself and keeps no bit 32 from DXF. Without this, every dimension - /// of a DWG CADCraft writes is user-positioned for AutoCAD (#1094), and every dimension of an - /// AutoCAD DWG is read as user-positioned. `to_dwg` sets the fields up for the writer: - /// bit 32 as CADCraft's DXF writer always sets it, and bit 0 through the inverted field; - /// otherwise the field is read from bit 0 after a DWG is read. - fn dimension_flags(doc: &mut acadrust::CadDocument, to_dwg: bool) { + /// The DWG dimension flags byte. Its bit 1 is DXF group 70's bit 32 (ODA DWG specification, + /// common dimension data), which acadrust keeps no copy of from DXF: it is set here as + /// CADCraft's DXF writer always sets it (#1094). Bit 0, the opposite of group 70's bit 128, + /// acadrust 0.6.5 reads and writes itself. + fn dimension_flags(doc: &mut acadrust::CadDocument) { for e in doc.entities_mut() { let acadrust::entities::EntityType::Dimension(d) = e else { continue }; - let b = d.base_mut(); - if to_dwg { - b.dwg_flags_byte |= 0x02; - b.text_user_positioned = !b.text_user_positioned; - } else { - b.text_user_positioned = b.dwg_flags_byte & 0x01 == 0; - } + d.base_mut().dwg_flags_byte |= 0x02; } } @@ -549,7 +538,7 @@ mod native { } other_layout_owners(&mut doc); table_style_rows(&mut doc, true); - dimension_flags(&mut doc, true); + dimension_flags(&mut doc); drop_unwritable(&mut doc); current_dimstyle(&mut doc); current_text_style(&mut doc); From 51db2d52e417578159bdbd0c1820e2d7a905ba2a Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Fri, 23 Oct 2026 09:49:38 +0200 Subject: [PATCH 13/19] dependabot: leave the egui crates at 0.36 until craft-ui moves to 0.37 (craft-libs#199) --- .github/dependabot.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 2c4b8cb1..093325ba 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -14,6 +14,17 @@ updates: - "*" commit-message: prefix: "chore(deps)" + # craft-ui is still on egui 0.36, and two eguis in the graph don't build (#1990). Remove this + # once craft-ui moves to egui 0.37 (storytold/craft-libs#199). + ignore: + - dependency-name: "egui" + versions: [">=0.37"] + - dependency-name: "eframe" + versions: [">=0.37"] + - dependency-name: "egui-wgpu" + versions: [">=0.37"] + - dependency-name: "egui_kittest" + versions: [">=0.37"] # Maintain GitHub Actions workflows - package-ecosystem: "github-actions" From 251fddb658a606cd950573fe576bd9894f41c429 Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Fri, 23 Oct 2026 09:49:38 +0200 Subject: [PATCH 14/19] fonts: harfrust comment names both versions; the advance check skips fonts at 1000 per em --- Cargo.toml | 4 ++-- crates/fonts/src/complex.rs | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a0aea657..cabb8a06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,8 +40,8 @@ rayon = "1" tiny-skia = { version = "0.12", default-features = false, features = ["std", "simd", "png-format"] } lyon_tessellation = "1.0.22" skrifa = "0.48" -# Text shaping (OpenType GSUB/GPOS, HarfBuzz port on read-fonts; the version egui's epaint uses) -# and the Unicode Bidirectional Algorithm for right-to-left drawing text. +# Text shaping (OpenType GSUB/GPOS, HarfBuzz port on read-fonts; egui's epaint is still on 0.12, +# so the graph has both) and the Unicode Bidirectional Algorithm for right-to-left drawing text. harfrust = "0.14" unicode-bidi = "0.3.18" image = { version = "0.25", default-features = false, features = ["png", "jpeg"] } diff --git a/crates/fonts/src/complex.rs b/crates/fonts/src/complex.rs index 1d76f813..dafabdc7 100644 --- a/crates/fonts/src/complex.rs +++ b/crates/fonts/src/complex.rs @@ -310,8 +310,13 @@ mod tests { assert_eq!(gids.len(), 2, "{gids:?}"); assert!(gids.first() != Some(&ka) && gids.last() == Some(&ka), "vowel sign before ka: {gids:?}"); assert!(gs.iter().all(|g| g.cluster == 2), "{:?}", gs.iter().map(|g| g.cluster).collect::>()); - // Advances come out at 1000 units per em, whatever the font's own em. - let hmtx = face.font().and_then(|f| f.glyph_metrics(Size::new(1000.0), LocationRef::default()).advance_width(skrifa::GlyphId::new(ka))); + // Advances come out at 1000 units per em, whatever the font's own em. A font that is + // already at 1000 per em could not tell a missing scale apart, so it checks nothing here. + let Some(font) = face.font() else { return }; + if font.metrics(Size::unscaled(), LocationRef::default()).units_per_em == 1000 { + return; + } + let hmtx = font.glyph_metrics(Size::new(1000.0), LocationRef::default()).advance_width(skrifa::GlyphId::new(ka)); let adv = gs.last().map(|g| g.advance); assert!(matches!((adv, hmtx), (Some(a), Some(h)) if (a - f64::from(h)).abs() < 1e-3), "{adv:?} vs {hmtx:?}"); } From 8ad9a3ce158a983c5aa4146a5503ddca81bb54f5 Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Fri, 23 Oct 2026 14:54:19 +0200 Subject: [PATCH 15/19] dwg: require acadrust 0.6.5, the first version that inverts dimension bit 0 itself --- crates/dwg/Cargo.toml | 2 +- crates/io/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/dwg/Cargo.toml b/crates/dwg/Cargo.toml index 398df0a3..a409e5b9 100644 --- a/crates/dwg/Cargo.toml +++ b/crates/dwg/Cargo.toml @@ -10,4 +10,4 @@ workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies] # MPL-2.0: used as an unmodified dependency only (see plan/adr/0001 and NOTICE). -acadrust = "0.6.4" +acadrust = "0.6.5" diff --git a/crates/io/Cargo.toml b/crates/io/Cargo.toml index b056603c..395ec496 100644 --- a/crates/io/Cargo.toml +++ b/crates/io/Cargo.toml @@ -24,5 +24,5 @@ quick-xml = { workspace = true } [dev-dependencies] # Reads CADCraft's DWG output as written (tests only; the same version as cadcraft-dwg's). -acadrust = "0.6.4" +acadrust = "0.6.5" tiny-skia = { workspace = true } From 78decf80f79ead81870e4826c51b765779d90aef Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 23 Oct 2026 11:16:34 -0400 Subject: [PATCH 16/19] web: allow browser zoom in the viewport meta (SonarCloud Web:S7926) (#1974) user-scalable=no stops people with low vision from zooming the page. The canvas keeps its own pinch/pan handling: eframe already prevent_default()s touch and gesture events on the canvas, so dropping the flag only restores browser zoom (and accessibility zoom) outside of the app's gestures. --- apps/cadcraft-web/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/cadcraft-web/index.html b/apps/cadcraft-web/index.html index c8e2e960..6cd4ab97 100644 --- a/apps/cadcraft-web/index.html +++ b/apps/cadcraft-web/index.html @@ -2,7 +2,7 @@ - + CADCraft