Compare commits

..
Author SHA1 Message Date
Brandon ThomasandClaude Opus 5.5 bad1073fdf Release 0.12.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 20:06:04 -04:00
31 changed files with 19 additions and 141 deletions
Generated
+16 -16
View File
@@ -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",
+1 -1
View File
@@ -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"
-10
View File
@@ -48,8 +48,6 @@ 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<ControlRequest> {
match TcpListener::bind(("127.0.0.1", port)) {
Ok(l) => {
@@ -103,7 +101,6 @@ 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<ControlRequest>, ctx: egui::Context, limits: Limits, token: Arc<String>) {
let open = Arc::new(AtomicUsize::new(0));
for mut stream in accepted(listener.incoming(), || std::thread::sleep(ACCEPT_RETRY)) {
@@ -165,7 +162,6 @@ 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());
@@ -202,8 +198,6 @@ 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<ControlRequest>, 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;
@@ -391,7 +385,6 @@ 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());
@@ -414,7 +407,6 @@ 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();
@@ -431,7 +423,6 @@ 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).
@@ -446,7 +437,6 @@ 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();
-3
View File
@@ -89,8 +89,6 @@ struct CompDof {
objects: Vec<Option<usize>>,
}
/// 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);
@@ -215,7 +213,6 @@ 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();
-2
View File
@@ -328,8 +328,6 @@ 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],
-5
View File
@@ -117,8 +117,6 @@ pub fn rigid_points(d: &Drawing, ins: &Insert) -> Vec<Vec2> {
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<Vec2> {
let Some(block) = d.block(&ins.block) else { return Vec::new() };
let m = ins.transform(block.base.xy());
@@ -189,7 +187,6 @@ 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 {
@@ -623,7 +620,6 @@ mod budget_tests {
use cadcraft_geom::Vec3;
use std::sync::Arc;
/// 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();
@@ -639,7 +635,6 @@ 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();
-1
View File
@@ -116,7 +116,6 @@ 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 {
-1
View File
@@ -364,7 +364,6 @@ 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();
+1 -5
View File
@@ -529,11 +529,7 @@ 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()))
-3
View File
@@ -44,7 +44,6 @@ pub fn read_capped(path: &Path, max: usize) -> io::Result<Vec<u8>> {
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()));
@@ -58,7 +57,6 @@ 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() {
@@ -68,7 +66,6 @@ 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() {
-1
View File
@@ -337,7 +337,6 @@ 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<Value> {
if let Some(_path) = str_param(p, "path") {
#[cfg(not(target_arch = "wasm32"))]
-9
View File
@@ -698,14 +698,11 @@ 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<Vec<Vec<char>>, ()> {
let mut out = vec![Vec::new()];
let mut chars = pat.chars().peekable();
@@ -745,7 +742,6 @@ fn split_patterns(pat: &str, budget: &mut u64) -> Result<Vec<Vec<char>>, ()> {
Ok(out)
}
/// Match one wildcard alternative, including leading `~` negation, within the shared budget.
fn match_pattern(text: &[char], pat: &[char], budget: &mut u64) -> Result<bool, ()> {
if let Some(('~', rest)) = pat.split_first().map(|(c, r)| (*c, r))
&& !rest.is_empty()
@@ -761,7 +757,6 @@ fn glob(text: &[char], pat: &[char], depth: usize, budget: &mut u64) -> Result<b
glob_cached(text, pat, depth, budget, &mut std::collections::HashSet::new())
}
/// Match suffixes while memoizing failures; both matching work and retained failure entries are bounded.
fn glob_cached(
text: &[char],
pat: &[char],
@@ -783,7 +778,6 @@ fn glob_cached(
Ok(matched)
}
/// Evaluate wildcard tokens and backtracking branches under shared work and recursion limits.
fn glob_inner(
text: &[char],
pat: &[char],
@@ -1066,7 +1060,6 @@ fn findfile(a: &[Value]) -> 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<String> {
// Read through a byte-limited, regular-file-only reader so a path naming an endless device
@@ -1075,7 +1068,6 @@ pub fn read_text_file(path: &str) -> Option<String> {
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<String> {
None
@@ -1596,7 +1588,6 @@ 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());
-3
View File
@@ -158,8 +158,6 @@ 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<serde_json::Value> {
if depth > TO_JSON_MAX_DEPTH || *budget == 0 {
return None;
@@ -187,7 +185,6 @@ pub fn getvar(ctx: &mut Ctx<'_>, a: Vec<Value>) -> Result<Flow, LispError> {
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<Value>) -> Result<Flow, LispError> {
arity(&a, 2, 2)?;
let name = string(&a[0])?;
-1
View File
@@ -131,7 +131,6 @@ 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 }
}
-5
View File
@@ -221,7 +221,6 @@ 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
@@ -233,7 +232,6 @@ 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();
@@ -244,7 +242,6 @@ 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();
@@ -255,7 +252,6 @@ 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<u64> = [399_999, 400_000]
@@ -274,7 +270,6 @@ 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
-1
View File
@@ -353,7 +353,6 @@ 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;
+1 -3
View File
@@ -20,9 +20,7 @@ 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");
-1
View File
@@ -60,7 +60,6 @@ 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 };
-1
View File
@@ -685,7 +685,6 @@ 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
-16
View File
@@ -624,7 +624,6 @@ 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<GlyphCache> {
static C: OnceLock<RwLock<GlyphCache>> = OnceLock::new();
C.get_or_init(|| RwLock::new(GlyphCache::default()))
@@ -644,7 +643,6 @@ 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;
@@ -663,7 +661,6 @@ 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())
@@ -681,16 +678,13 @@ 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);
@@ -701,7 +695,6 @@ 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);
@@ -749,8 +742,6 @@ pub fn glyph_contours(face: &Face, gid: u16) -> Arc<[Vec<Vec2>]> {
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<GlyphOutline> {
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) {
@@ -821,8 +812,6 @@ 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<Vec<Vec<Vec2>>> {
*points_left = points_left.checked_sub(g.points)?;
Some(place_glyph(g, x, h, wf, shear))
@@ -937,7 +926,6 @@ 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() };
@@ -951,7 +939,6 @@ 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();
@@ -964,7 +951,6 @@ 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.
@@ -977,7 +963,6 @@ 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() };
@@ -988,7 +973,6 @@ 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();
-2
View File
@@ -156,8 +156,6 @@ 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<Handle, &Def>, budget: &mut usize) {
let hs: Vec<Handle> =
store.iter().filter(|e| matches!(e.kind, EntityKind::Image(_)) && by_image.contains_key(&e.handle)).map(|e| e.handle).collect();
-6
View File
@@ -248,7 +248,6 @@ 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) {
@@ -1399,7 +1398,6 @@ 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).
@@ -1683,7 +1681,6 @@ pub fn read(bytes: &[u8]) -> Result<Drawing> {
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<Drawing> {
let tags = cadcraft_dxf::parse(bytes).map_err(|e| IoError::Format(e.to_string()))?;
let secs = sections(&tags);
@@ -2348,11 +2345,9 @@ 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<Tag> {
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;
@@ -2363,7 +2358,6 @@ 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";
-2
View File
@@ -172,7 +172,6 @@ impl LeftOut {
}
impl Parser {
/// Initialize a metric SVG drawing with one document-wide generated-vertex allowance.
fn new() -> Self {
Parser {
drawing: Drawing::new_metric(),
@@ -335,7 +334,6 @@ 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 {
-2
View File
@@ -17,7 +17,6 @@ fn arc_bomb(arcs: usize) -> Vec<u8> {
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.
@@ -25,7 +24,6 @@ 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.
-7
View File
@@ -10,11 +10,9 @@ 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<Value, String>;
/// 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;
}
@@ -39,7 +37,6 @@ fn port_of(addr: &str) -> Option<u16> {
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<Self> {
let mut r = Self { addr: addr.to_string(), conn: None, next_id: 1, launch: false, token: None };
r.reconnect()?;
@@ -185,8 +182,6 @@ fn is_read_only(method: &str) -> bool {
}
impl Backend for Remote {
/// 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<Value, String> {
let id = self.next_id;
self.next_id += 1;
@@ -452,7 +447,6 @@ 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();
@@ -479,7 +473,6 @@ 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();
-13
View File
@@ -23,7 +23,6 @@ pub fn token_path(port: u16) -> io::Result<PathBuf> {
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<PathBuf> {
if let Some(d) = std::env::var_os("XDG_RUNTIME_DIR").filter(|v| !v.is_empty()) {
@@ -35,13 +34,11 @@ fn private_dir() -> io::Result<PathBuf> {
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<PathBuf> {
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<std::ffi::OsString>) -> io::Result<PathBuf> {
local
@@ -50,7 +47,6 @@ fn windows_private_dir(local: Option<std::ffi::OsString>) -> io::Result<PathBuf>
.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<PathBuf> {
Err(io::Error::new(io::ErrorKind::Unsupported, "no private control token directory on this platform"))
@@ -65,7 +61,6 @@ pub fn issue(port: u16) -> io::Result<String> {
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)?;
@@ -103,7 +98,6 @@ 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<String> {
let mut bytes = [0u8; TOKEN_BYTES];
getrandom::fill(&mut bytes).map_err(|e| io::Error::other(format!("no secure randomness: {e}")))?;
@@ -115,7 +109,6 @@ fn random_hex() -> io::Result<String> {
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;
@@ -127,7 +120,6 @@ 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;
@@ -142,7 +134,6 @@ 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;
@@ -153,7 +144,6 @@ 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"));
@@ -162,7 +152,6 @@ 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() {
@@ -173,7 +162,6 @@ 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());
@@ -181,7 +169,6 @@ 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()));
-6
View File
@@ -413,7 +413,6 @@ 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)
@@ -425,7 +424,6 @@ 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()
@@ -457,8 +455,6 @@ 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) {
@@ -1367,14 +1363,12 @@ 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));
-4
View File
@@ -79,8 +79,6 @@ 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<Vec<Vec2>>) {
// `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
@@ -465,7 +463,6 @@ 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<Vec2>], px: f64, view: Option<&Bounds2>, cancel: Option<&AtomicBool>, out: &mut Vec<Vec<Vec2>>, work: &mut u64) {
let local: Vec<Vec<Vec2>> = loops.iter().map(|l| l.iter().map(|p| self.to_local(*p)).collect()).collect();
let bb = Bounds2::from_points(local.iter().flatten().copied());
@@ -609,7 +606,6 @@ 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.
-1
View File
@@ -118,7 +118,6 @@ pub fn ranges(len: f64, lt: &Linetype, scale: f64, min_pattern: f64) -> Option<V
mod tests {
use super::*;
/// This gap-only pattern draws nothing, and `ranges` returns before cycling through its repeats.
#[test]
fn an_all_gap_pattern_draws_nothing_without_spinning() {
let lt = Linetype::simple("GAPS", "", &[-0.001, -0.002]);
-4
View File
@@ -57,8 +57,6 @@ pub fn table_covered(t: &Table) -> Vec<Vec<bool>> {
}
// 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<Vec<bool>>, Vec<Vec<bool>>> {
let rows = t.row_heights.len().min(10_000);
let cols = t.col_widths.len().min(10_000);
@@ -397,7 +395,6 @@ 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 {
@@ -412,7 +409,6 @@ mod budget_tests {
assert!(g.fills.is_empty() && g.texts.is_empty());
}
/// A split table within the displayed-cell budget clips a merge to contiguous selected rows.
#[test]
fn part_clips_merges_using_displayed_column_count() {
let t = Table {
-6
View File
@@ -14,14 +14,11 @@ 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(());
@@ -87,7 +84,6 @@ fn exact_tests(pkg: &str) -> Option<usize> {
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();
@@ -142,7 +138,6 @@ 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() {
@@ -159,7 +154,6 @@ 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()));