Compare commits

...
Author SHA1 Message Date
Dylan M. Taylor 1c00ae3492 security: write the release from the release job only (SonarCloud S8233)
The workflow-level contents: write granted every job the right to push
tags and releases; the reusable release workflow is the only job that
needs it. Workflow level drops to read.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
2026-10-23 17:16:13 -04:00
Dylan M. Taylor c1dd60ffd6 Merge pull request #2018 from dylanmtaylor/ci/sonar-main-not-cancelled
ci(sonar): stop cancelling main-branch runs so coverage is published
2026-10-23 16:58:56 -04:00
Dylan M. Taylor bec28d6b87 ci(sonar): don't cancel superseded runs on main, so coverage is published 2026-10-23 16:50:42 -04:00
aiongg d3e6111cf1 Xrefs and data links: resolve stored paths with craft-docpath's resolve_absolute (#1750)
* Xrefs and data links: use resolve_absolute for stored paths

* CI: check that git dependencies come from storytold

* CI: release only with git dependencies from storytold

* Parse Cargo.lock as TOML when checking git sources

* Use the shared git-sources check in CI

* Parse cargo config, legacy .cargo/config included, in the git-sources check
2026-10-23 16:42:01 -04:00
aiongg dd1f0531ac i18n: missing translations never fail; cargo xtask i18n reports them (#2013) 2026-10-23 15:40:12 -04:00
aiongg 37150774c6 CI: run the gate through craft-common-actions' shared gate.yml; FreeBSD nightly and on release (#1987)
ci.yml is new: it calls the shared workflow (cached apt packages, nextest, main-only cache saves, Cargo.lock check). FreeBSD and Windows ARM64 run nightly and on pushes to release instead of on pull requests. Depends on storytold/craft-common-actions#69.
2026-10-23 13:56:16 -04:00
aiongg 9cf115e808 CI: one test binary per crate, nextest, line tables only (#1989)
* Build: line tables only in dev, no debug info for dependencies

* Tests: one integration-test binary per crate

Each crate's tests/*.rs becomes a module of tests/it/main.rs. Files that
install process-wide state (the file io hooks, user aliases, SAVETIME and
SAVEFILEPATH, user linetype and pattern files, registered fonts) stay their
own test binaries.

* xtask ci: nextest, tests without examples, one wasm check call
2026-10-23 13:34:26 -04:00
aiongg 9d8f16c825 Merge pull request #1991 from marcoarnulfo/fix/harfrust-0.14
Build main again after the #1957 dependency bump
2026-10-23 13:00:16 -04:00
Dylan M. Taylor f043540d13 packaging: use [[ ]] tests, named locals and explicit returns in shell scripts (SonarCloud S7688/S7679/S7682) (#1972)
All flagged scripts run under bash (shebang or '# shellcheck shell=bash'), so
[[ ]] is safe. Positional parameters in helper functions are copied into named
locals, and helper functions end with a bare 'return', which keeps the exit
status of the last command (no behaviour change, notably for sha256 under
pipefail). packaging/linux/postinst.sh is POSIX sh and is left alone.
2026-10-23 11:16:40 -04:00
Dylan M. Taylor 78decf80f7 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.
2026-10-23 11:16:34 -04:00
Marco Arnulfo 8ad9a3ce15 dwg: require acadrust 0.6.5, the first version that inverts dimension bit 0 itself 2026-10-23 14:54:19 +02:00
Marco Arnulfo 251fddb658 fonts: harfrust comment names both versions; the advance check skips fonts at 1000 per em 2026-10-23 09:49:38 +02:00
Marco Arnulfo 51db2d52e4 dependabot: leave the egui crates at 0.36 until craft-ui moves to 0.37 (craft-libs#199) 2026-10-23 09:49:38 +02:00
Marco Arnulfo 9621dcbd94 dwg: drop the dimension bit-0 inversion that acadrust 0.6.5 now does itself 2026-10-23 09:30:10 +02:00
Marco Arnulfo 5648a06ac4 Hold the egui crates at 0.36 until craft-ui moves to 0.37 (craft-libs#199) 2026-10-23 09:14:51 +02:00
Marco Arnulfo f344cf4460 engine: pass the query envelope by value to rstar 0.13 2026-10-23 08:50:32 +02:00
Marco Arnulfo e61e328edb io: read SVG element and attribute names with quick-xml 0.42 2026-10-23 08:50:32 +02:00
Marco Arnulfo 611eb9f5a9 fonts: port complex-text shaping to harfrust 0.14 2026-10-23 08:44:31 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 1977a493c7 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] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-22 23:54:32 -04:00
skylar taylor-barrick a08fd7c1d1 Merge pull request #1963 from storytold/docs/security-review-docstrings-20261022
docs: document security limits and regression guarantees
2026-10-22 18:36:19 -06:00
skylartaylor fa6bb0351b docs: clarify security regression guarantees 2026-10-23 02:14:11 +02:00
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
skylartaylor 48b4165591 docs: document security limits and regression guarantees 2026-10-23 01:59:55 +02:00
Dylan M. Taylor 01bbdb24bc security: confine craft-fonts manifest paths and reject newline directives (CWE-22, CWE-74) (#1841)
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).
2026-10-22 18:42:51 -04:00
Dylan M. Taylor 15ba3dc5cc security: submit XATTACH picked path as literal text, not a command line (CWE-74) (#1843)
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.
2026-10-22 18:19:13 -04:00
Dylan M. Taylor 07bb604214 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).
2026-10-22 18:18:43 -04:00
331 changed files with 1081 additions and 1245 deletions
+11
View File
@@ -14,6 +14,17 @@ updates:
- "*" - "*"
commit-message: commit-message:
prefix: "chore(deps)" 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 # Maintain GitHub Actions workflows
- package-ecosystem: "github-actions" - package-ecosystem: "github-actions"
+30
View File
@@ -0,0 +1,30 @@
name: CI
# Quality gates for every push to main and every pull request (`cargo xtask ci`), through the
# suite's shared gate workflow in craft-common-actions.
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
permissions:
contents: read
# A newer push to a PR branch cancels its older run; every push to main runs to the end.
concurrency:
group: ci-${{ github.event_name == 'pull_request' && github.ref || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
gate:
# Tracks @main so suite-wide CI fixes reach every app without a PR in each repository.
uses: storytold/craft-common-actions/.github/workflows/gate.yml@main
with:
# Same craft-fonts commit as release.yml.
craft-fonts-ref: 3d9ab3a132ed0eff457e424226a435a901da30d3
timeout-minutes: 90
packages: >-
pkg-config libxkbcommon-dev libwayland-dev libx11-dev libxcb1-dev libxcursor-dev
libxrandr-dev libxi-dev libfontconfig1-dev libfreetype-dev mesa-vulkan-drivers
libgl1-mesa-dev
+3
View File
@@ -2,6 +2,9 @@
# (GitHub has no FreeBSD runners). GPU tests skip themselves when the VM has no adapter. # (GitHub has no FreeBSD runners). GPU tests skip themselves when the VM has no adapter.
name: FreeBSD name: FreeBSD
# The VM build is slow, so it runs nightly on main and on pushes to `release`, not on every pull
# request: a drifted dependency still shows up within a day, and always before a release ships.
# PRs that change the FreeBSD workflow or packaging itself still run it.
on: on:
push: push:
branches: [release] branches: [release]
+38
View File
@@ -21,3 +21,41 @@ jobs:
nix: nix:
# Intentionally tracked at @main (not SHA-pinned) so org-wide CI improvements, security fixes, and build optimizations propagate without PR churn across all craftapp repos. # Intentionally tracked at @main (not SHA-pinned) so org-wide CI improvements, security fixes, and build optimizations propagate without PR churn across all craftapp repos.
uses: storytold/craft-common-actions/.github/workflows/nix.yml@main uses: storytold/craft-common-actions/.github/workflows/nix.yml@main
git-sources:
name: Git dependencies come from storytold
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Git dependencies come from storytold
run: |
set -euo pipefail
test -f Cargo.lock || { echo "Cargo.lock is missing"; exit 1; }
# Source replacement, patches and path overrides in cargo config never show up
# in Cargo.lock. Parsed rather than grepped: TOML has many spellings of one key.
# Cargo still reads the legacy .cargo/config.
python3 - <<'PY'
import os, sys, tomllib
for path in (".cargo/config", ".cargo/config.toml"):
if not os.path.exists(path):
continue
config = tomllib.load(open(path, "rb"))
replaced = [name for name, source in config.get("source", {}).items()
if isinstance(source, dict) and "replace-with" in source]
if replaced or "patch" in config or "paths" in config:
print(f"{path} replaces, patches or overrides a source")
sys.exit(1)
PY
# Fails when Cargo.lock is out of date with the manifests.
cargo metadata --locked --format-version 1 > /dev/null
python3 - <<'PY'
import sys, tomllib
lock = tomllib.load(open("Cargo.lock", "rb"))
bad = sorted({p["source"] for p in lock.get("package", [])
if p.get("source", "").startswith("git+")
and not p["source"].startswith("git+https://github.com/storytold/")})
if bad:
print("Git dependencies outside github.com/storytold:", *bad, sep="\n")
sys.exit(1)
PY
+44 -1
View File
@@ -11,14 +11,57 @@ on:
default: "" default: ""
permissions: permissions:
contents: write contents: read
concurrency: concurrency:
group: release-${{ github.ref }} group: release-${{ github.ref }}
cancel-in-progress: false cancel-in-progress: false
jobs: jobs:
git-sources:
name: Git dependencies come from storytold
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Git dependencies come from storytold
run: |
set -euo pipefail
test -f Cargo.lock || { echo "Cargo.lock is missing"; exit 1; }
# Source replacement, patches and path overrides in cargo config never show up
# in Cargo.lock. Parsed rather than grepped: TOML has many spellings of one key.
# Cargo still reads the legacy .cargo/config.
python3 - <<'PY'
import os, sys, tomllib
for path in (".cargo/config", ".cargo/config.toml"):
if not os.path.exists(path):
continue
config = tomllib.load(open(path, "rb"))
replaced = [name for name, source in config.get("source", {}).items()
if isinstance(source, dict) and "replace-with" in source]
if replaced or "patch" in config or "paths" in config:
print(f"{path} replaces, patches or overrides a source")
sys.exit(1)
PY
# Fails when Cargo.lock is out of date with the manifests.
cargo metadata --locked --format-version 1 > /dev/null
python3 - <<'PY'
import sys, tomllib
lock = tomllib.load(open("Cargo.lock", "rb"))
bad = sorted({p["source"] for p in lock.get("package", [])
if p.get("source", "").startswith("git+")
and not p["source"].startswith("git+https://github.com/storytold/")})
if bad:
print("Git dependencies outside github.com/storytold:", *bad, sep="\n")
sys.exit(1)
PY
release: release:
needs: git-sources
permissions:
# The reusable release workflow tags and uploads the draft release.
contents: write
# Intentionally tracked at @main (not SHA-pinned) so org-wide CI improvements, security fixes, and build optimizations propagate without PR churn across all craftapp repos. # Intentionally tracked at @main (not SHA-pinned) so org-wide CI improvements, security fixes, and build optimizations propagate without PR churn across all craftapp repos.
uses: storytold/craft-common-actions/.github/workflows/release.yml@main uses: storytold/craft-common-actions/.github/workflows/release.yml@main
with: with:
+2 -1
View File
@@ -11,7 +11,8 @@ permissions:
concurrency: concurrency:
group: sonarcloud-${{ github.ref }} group: sonarcloud-${{ github.ref }}
cancel-in-progress: true # A push to main is the run that publishes coverage; superseded runs there would never finish.
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs: jobs:
sonarcloud: sonarcloud:
+4 -2
View File
@@ -3,15 +3,17 @@
# binaries are ARM64, run the CLI natively, and uninstall. # binaries are ARM64, run the CLI natively, and uninstall.
name: Windows ARM64 name: Windows ARM64
# Nightly on main and on pushes to `release` (dependency and runner-image drift shows up within a
# day, and always before a release ships), and on PRs that change the Windows packaging itself.
on: on:
schedule:
- cron: "3 5 * * *"
push: push:
branches: [release] branches: [release]
pull_request: pull_request:
paths: paths:
- .github/workflows/windows-arm64.yml - .github/workflows/windows-arm64.yml
- packaging/windows/** - packaging/windows/**
schedule:
- cron: "41 3 * * 1"
workflow_dispatch: workflow_dispatch:
concurrency: concurrency:
Generated
+85 -150
View File
@@ -4,9 +4,9 @@ version = 4
[[package]] [[package]]
name = "acadrust" name = "acadrust"
version = "0.6.4" version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94862189cab6e282ed0433a801a216cd1a712953813284a48fa81c67223e38f6" checksum = "5eaa72520f70eb6f7f09b06f44e1ed0d8fad5fa743364f2c3715c30df962ec7c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bitflags 2.13.2", "bitflags 2.13.2",
@@ -209,9 +209,9 @@ checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]] [[package]]
name = "arboard" name = "arboard"
version = "3.6.1" version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" checksum = "aeb9553e62312c8756a5979254fc6d7da8bbeb5986a1e17037fab932f7037917"
dependencies = [ dependencies = [
"clipboard-win", "clipboard-win",
"image", "image",
@@ -223,7 +223,7 @@ dependencies = [
"objc2-foundation 0.3.2", "objc2-foundation 0.3.2",
"parking_lot", "parking_lot",
"percent-encoding", "percent-encoding",
"windows-sys 0.60.2", "windows-sys 0.61.2",
"x11rb", "x11rb",
] ]
@@ -547,7 +547,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]] [[package]]
name = "cadcraft" name = "cadcraft"
version = "0.11.0" version = "0.12.0"
dependencies = [ dependencies = [
"cadcraft-engine", "cadcraft-engine",
"cadcraft-io", "cadcraft-io",
@@ -571,7 +571,7 @@ dependencies = [
[[package]] [[package]]
name = "cadcraft-cli" name = "cadcraft-cli"
version = "0.11.0" version = "0.12.0"
dependencies = [ dependencies = [
"cadcraft-engine", "cadcraft-engine",
"cadcraft-io", "cadcraft-io",
@@ -582,14 +582,14 @@ dependencies = [
[[package]] [[package]]
name = "cadcraft-color" name = "cadcraft-color"
version = "0.11.0" version = "0.12.0"
dependencies = [ dependencies = [
"serde", "serde",
] ]
[[package]] [[package]]
name = "cadcraft-constraints" name = "cadcraft-constraints"
version = "0.11.0" version = "0.12.0"
dependencies = [ dependencies = [
"cadcraft-doc", "cadcraft-doc",
"cadcraft-geom", "cadcraft-geom",
@@ -599,7 +599,7 @@ dependencies = [
[[package]] [[package]]
name = "cadcraft-doc" name = "cadcraft-doc"
version = "0.11.0" version = "0.12.0"
dependencies = [ dependencies = [
"cadcraft-color", "cadcraft-color",
"cadcraft-geom", "cadcraft-geom",
@@ -610,14 +610,14 @@ dependencies = [
[[package]] [[package]]
name = "cadcraft-dwg" name = "cadcraft-dwg"
version = "0.11.0" version = "0.12.0"
dependencies = [ dependencies = [
"acadrust", "acadrust",
] ]
[[package]] [[package]]
name = "cadcraft-dxf" name = "cadcraft-dxf"
version = "0.11.0" version = "0.12.0"
dependencies = [ dependencies = [
"encoding_rs", "encoding_rs",
"log", "log",
@@ -626,7 +626,7 @@ dependencies = [
[[package]] [[package]]
name = "cadcraft-engine" name = "cadcraft-engine"
version = "0.11.0" version = "0.12.0"
dependencies = [ dependencies = [
"cadcraft-color", "cadcraft-color",
"cadcraft-constraints", "cadcraft-constraints",
@@ -647,24 +647,24 @@ dependencies = [
[[package]] [[package]]
name = "cadcraft-fonts" name = "cadcraft-fonts"
version = "0.11.0" version = "0.12.0"
dependencies = [ dependencies = [
"cadcraft-geom", "cadcraft-geom",
"harfrust", "harfrust 0.14.0",
"skrifa 0.47.0", "skrifa 0.48.0",
"unicode-bidi", "unicode-bidi",
] ]
[[package]] [[package]]
name = "cadcraft-geom" name = "cadcraft-geom"
version = "0.11.0" version = "0.12.0"
dependencies = [ dependencies = [
"serde", "serde",
] ]
[[package]] [[package]]
name = "cadcraft-io" name = "cadcraft-io"
version = "0.11.0" version = "0.12.0"
dependencies = [ dependencies = [
"acadrust", "acadrust",
"cadcraft-color", "cadcraft-color",
@@ -674,8 +674,8 @@ dependencies = [
"cadcraft-fonts", "cadcraft-fonts",
"cadcraft-geom", "cadcraft-geom",
"cadcraft-render", "cadcraft-render",
"miniz_oxide 0.8.9", "miniz_oxide 0.9.1",
"quick-xml", "quick-xml 0.42.0",
"serde", "serde",
"serde_json", "serde_json",
"thiserror 2.0.21", "thiserror 2.0.21",
@@ -684,19 +684,19 @@ dependencies = [
[[package]] [[package]]
name = "cadcraft-mcp" name = "cadcraft-mcp"
version = "0.11.0" version = "0.12.0"
dependencies = [ dependencies = [
"cadcraft-engine", "cadcraft-engine",
"cadcraft-io", "cadcraft-io",
"cadcraft-render", "cadcraft-render",
"getrandom 0.3.4", "getrandom 0.4.3",
"log", "log",
"serde_json", "serde_json",
] ]
[[package]] [[package]]
name = "cadcraft-render" name = "cadcraft-render"
version = "0.11.0" version = "0.12.0"
dependencies = [ dependencies = [
"cadcraft-color", "cadcraft-color",
"cadcraft-doc", "cadcraft-doc",
@@ -710,7 +710,7 @@ dependencies = [
[[package]] [[package]]
name = "cadcraft-ui-egui" name = "cadcraft-ui-egui"
version = "0.11.0" version = "0.12.0"
dependencies = [ dependencies = [
"cadcraft-color", "cadcraft-color",
"cadcraft-doc", "cadcraft-doc",
@@ -733,7 +733,7 @@ dependencies = [
[[package]] [[package]]
name = "cadcraft-web" name = "cadcraft-web"
version = "0.11.0" version = "0.12.0"
dependencies = [ dependencies = [
"cadcraft-engine", "cadcraft-engine",
"cadcraft-io", "cadcraft-io",
@@ -1312,7 +1312,7 @@ dependencies = [
"emath", "emath",
"epaint_default_fonts", "epaint_default_fonts",
"font-types", "font-types",
"harfrust", "harfrust 0.12.0",
"log", "log",
"nohash-hasher", "nohash-hasher",
"parking_lot", "parking_lot",
@@ -1441,9 +1441,9 @@ checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]] [[package]]
name = "font-types" name = "font-types"
version = "0.12.6" version = "0.12.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c92d9081c7ab3bd83ef1eecdfad090f3d966e3ce74106779664114cfe1a6274" checksum = "835ae9d781464a79026cde387f9cbc650c9a4909d7cf7e658b127e3c35d2f35d"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"serde", "serde",
@@ -1734,6 +1734,18 @@ dependencies = [
"smallvec", "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]] [[package]]
name = "hash32" name = "hash32"
version = "0.3.1" version = "0.3.1"
@@ -1914,7 +1926,7 @@ dependencies = [
"byteorder-lite", "byteorder-lite",
"moxcms", "moxcms",
"num-traits", "num-traits",
"png 0.18.1", "png",
"tiff", "tiff",
"zune-core", "zune-core",
"zune-jpeg", "zune-jpeg",
@@ -2100,9 +2112,9 @@ checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.190" version = "0.2.191"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
[[package]] [[package]]
name = "libloading" name = "libloading"
@@ -2271,9 +2283,9 @@ dependencies = [
[[package]] [[package]]
name = "muda" name = "muda"
version = "0.21.0" version = "0.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6744ab645ebe79f9c428455c7cb9dfa4275fa3b50a25408e08f927da8dc618c" checksum = "4340e89c178916e62879c5c36b7f699019369be413e496178a6983ce91b8bc03"
dependencies = [ dependencies = [
"crossbeam-channel", "crossbeam-channel",
"dpi", "dpi",
@@ -2282,8 +2294,7 @@ dependencies = [
"objc2-app-kit 0.3.2", "objc2-app-kit 0.3.2",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-foundation 0.3.2", "objc2-foundation 0.3.2",
"once_cell", "png",
"png 0.18.1",
"thiserror 2.0.21", "thiserror 2.0.21",
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
@@ -2945,19 +2956,6 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" 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]] [[package]]
name = "png" name = "png"
version = "0.18.1" version = "0.18.1"
@@ -3081,6 +3079,15 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "quick-xml"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.47" version = "1.0.47"
@@ -3159,13 +3166,15 @@ dependencies = [
[[package]] [[package]]
name = "read-fonts" name = "read-fonts"
version = "0.44.0" version = "0.45.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5a0cf4bb6cbce4c29756270d189a5e400ac05e91192161d83a0bcb6e68492e8" checksum = "4edc8db0d972de88e53a085b1f01fb3fbdae7bb0cb7a0ce2b2f8285a412fe3ef"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"foldhash",
"font-types", "font-types",
"once_cell", "once_cell",
"yoke",
] ]
[[package]] [[package]]
@@ -3244,9 +3253,9 @@ dependencies = [
[[package]] [[package]]
name = "rstar" name = "rstar"
version = "0.12.2" version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "421400d13ccfd26dfa5858199c30a5d76f9c54e0dba7575273025b43c5175dbb" checksum = "5912b862fa5ffb462607bfd1e35036c458c537921f508c8235a83d5f3987edfe"
dependencies = [ dependencies = [
"heapless", "heapless",
"num-traits", "num-traits",
@@ -3377,9 +3386,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.151" version = "1.0.154"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",
@@ -3464,12 +3473,12 @@ dependencies = [
[[package]] [[package]]
name = "skrifa" name = "skrifa"
version = "0.47.0" version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "526e654b303ff4ad73a5a40b25de2b3ce3d039c5e3b6da5f89ae6e57004cb40f" checksum = "a5213b4825bac3587046330052d61bfac7bd3bd0d203c758d7c2ef726fcea034"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"read-fonts 0.44.0", "read-fonts 0.45.1",
] ]
[[package]] [[package]]
@@ -3715,24 +3724,24 @@ dependencies = [
[[package]] [[package]]
name = "tiny-skia" name = "tiny-skia"
version = "0.11.4" version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" checksum = "47ffee5eaaf5527f630fb0e356b90ebdec84d5d18d937c5e440350f88c5a91ea"
dependencies = [ dependencies = [
"arrayref", "arrayref",
"arrayvec", "arrayvec",
"bytemuck", "bytemuck",
"cfg-if", "cfg-if",
"log", "log",
"png 0.17.16", "png",
"tiny-skia-path", "tiny-skia-path",
] ]
[[package]] [[package]]
name = "tiny-skia-path" name = "tiny-skia-path"
version = "0.11.4" version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" checksum = "edca365c3faccca67d06593c5980fa6c57687de727a03131735bb85f01fdeeb9"
dependencies = [ dependencies = [
"arrayref", "arrayref",
"bytemuck", "bytemuck",
@@ -4153,7 +4162,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quick-xml", "quick-xml 0.41.0",
"quote", "quote",
] ]
@@ -4510,7 +4519,7 @@ version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [ dependencies = [
"windows-targets 0.52.6", "windows-targets",
] ]
[[package]] [[package]]
@@ -4519,16 +4528,7 @@ version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [ dependencies = [
"windows-targets 0.52.6", "windows-targets",
]
[[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",
] ]
[[package]] [[package]]
@@ -4546,31 +4546,14 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [ dependencies = [
"windows_aarch64_gnullvm 0.52.6", "windows_aarch64_gnullvm",
"windows_aarch64_msvc 0.52.6", "windows_aarch64_msvc",
"windows_i686_gnu 0.52.6", "windows_i686_gnu",
"windows_i686_gnullvm 0.52.6", "windows_i686_gnullvm",
"windows_i686_msvc 0.52.6", "windows_i686_msvc",
"windows_x86_64_gnu 0.52.6", "windows_x86_64_gnu",
"windows_x86_64_gnullvm 0.52.6", "windows_x86_64_gnullvm",
"windows_x86_64_msvc 0.52.6", "windows_x86_64_msvc",
]
[[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",
] ]
[[package]] [[package]]
@@ -4588,96 +4571,48 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.52.6" version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.52.6" version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
[[package]] [[package]]
name = "windows_i686_gnullvm" name = "windows_i686_gnullvm"
version = "0.52.6" version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.52.6" version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.52.6" version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 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]] [[package]]
name = "windows_x86_64_gnullvm" name = "windows_x86_64_gnullvm"
version = "0.52.6" version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 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]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.52.6" version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 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]] [[package]]
name = "winit" name = "winit"
version = "0.30.13" version = "0.30.13"
@@ -4740,9 +4675,9 @@ dependencies = [
[[package]] [[package]]
name = "winresource" name = "winresource"
version = "0.1.31" version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0986a8b1d586b7d3e4fe3d9ea39fb451ae22869dcea4aa109d287a374d866087" checksum = "a2bc6d36480b829272a5bdabf08e6dca8004761c1da7e35efef800540c66221b"
dependencies = [ dependencies = [
"toml", "toml",
"version_check", "version_check",
@@ -4825,7 +4760,7 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
[[package]] [[package]]
name = "xtask" name = "xtask"
version = "0.11.0" version = "0.12.0"
dependencies = [ dependencies = [
"chrono", "chrono",
"serde_json", "serde_json",
+9 -7
View File
@@ -4,7 +4,7 @@ members = ["crates/*", "apps/*", "xtask"]
# default-members # default-members
[workspace.package] [workspace.package]
version = "0.11.0" version = "0.12.0"
edition = "2024" edition = "2024"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
rust-version = "1.90" rust-version = "1.90"
@@ -37,17 +37,17 @@ chrono = { version = "0.4.45", default-features = false, features = ["std"] }
rayon = "1" rayon = "1"
# rendering / formats # 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" 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) # Text shaping (OpenType GSUB/GPOS, HarfBuzz port on read-fonts; egui's epaint is still on 0.12,
# and the Unicode Bidirectional Algorithm for right-to-left drawing text. # so the graph has both) and the Unicode Bidirectional Algorithm for right-to-left drawing text.
harfrust = "0.12" harfrust = "0.14"
unicode-bidi = "0.3.18" unicode-bidi = "0.3.18"
image = { version = "0.25", default-features = false, features = ["png", "jpeg"] } image = { version = "0.25", default-features = false, features = ["png", "jpeg"] }
encoding_rs = "0.8.35" encoding_rs = "0.8.35"
# XML for the SVG reader (streaming parser, no entity expansion beyond the five predefined ones) # XML for the SVG reader (streaming parser, no entity expansion beyond the five predefined ones)
quick-xml = "0.41" quick-xml = "0.42"
# ui # ui
craft-ui = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "5c874d51eeb3de70e735c8ce87a59a7dedd47675", features = ["serde"] } craft-ui = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "5c874d51eeb3de70e735c8ce87a59a7dedd47675", features = ["serde"] }
@@ -69,9 +69,11 @@ type_complexity = "allow"
[profile.dev.package."*"] [profile.dev.package."*"]
opt-level = 2 opt-level = 2
debug = false
[profile.dev] [profile.dev]
opt-level = 1 opt-level = 1
debug = "line-tables-only"
[profile.release] [profile.release]
opt-level = 3 opt-level = 3
+6
View File
@@ -0,0 +1,6 @@
mod cli;
mod drafting_spacing;
mod help;
mod mcp_connect;
mod skipped_records;
mod undrawn_entities;
+1 -1
View File
@@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>CADCraft</title> <title>CADCraft</title>
<link data-trunk rel="rust" data-bin="cadcraft-web" data-wasm-opt="z" data-weak-refs /> <link data-trunk rel="rust" data-bin="cadcraft-web" data-wasm-opt="z" data-weak-refs />
<style> <style>
+10
View File
@@ -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<ControlRequest> { pub fn start(port: u16, ctx: egui::Context) -> Receiver<ControlRequest> {
match TcpListener::bind(("127.0.0.1", port)) { match TcpListener::bind(("127.0.0.1", port)) {
Ok(l) => { 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<ControlRequest>, ctx: egui::Context, limits: Limits, token: Arc<String>) { fn accept_loop(listener: TcpListener, tx: SyncSender<ControlRequest>, ctx: egui::Context, limits: Limits, token: Arc<String>) {
let open = Arc::new(AtomicUsize::new(0)); let open = Arc::new(AtomicUsize::new(0));
for mut stream in accepted(listener.incoming(), || std::thread::sleep(ACCEPT_RETRY)) { for mut stream in accepted(listener.incoming(), || std::thread::sleep(ACCEPT_RETRY)) {
@@ -162,6 +165,7 @@ enum Line {
Reject(String), Reject(String),
} }
/// Parse a UTF-8 JSON request and its optional token, distinguishing blank lines from rejected input.
fn parse_line(bytes: &[u8]) -> Line { fn parse_line(bytes: &[u8]) -> Line {
let Ok(text) = std::str::from_utf8(bytes) else { let Ok(text) = std::str::from_utf8(bytes) else {
return Line::Reject("not a control request (invalid UTF-8); closing the connection".into()); 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/")) || 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) { 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() { if stream.set_read_timeout(Some(limits.idle_timeout)).is_err() || stream.set_write_timeout(Some(limits.write_timeout)).is_err() {
return; return;
@@ -385,6 +391,7 @@ mod tests {
assert!(t.elapsed() < Duration::from_secs(4)); 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] #[test]
fn a_full_queue_answers_busy() { fn a_full_queue_answers_busy() {
let (addr, rx) = serve_with(short()); let (addr, rx) = serve_with(short());
@@ -407,6 +414,7 @@ mod tests {
drop(rx); drop(rx);
} }
/// Replies on one authenticated connection preserve the ID of each request.
#[test] #[test]
fn requests_are_answered_with_their_ids() { fn requests_are_answered_with_their_ids() {
let addr = serve_echo(); let addr = serve_echo();
@@ -423,6 +431,7 @@ mod tests {
} }
} }
/// Missing and incorrect session tokens are rejected before dispatch.
#[test] #[test]
fn a_request_without_the_token_is_unauthorized() { fn a_request_without_the_token_is_unauthorized() {
// A local peer that does not hold the session token cannot drive the app (CWE-306). // 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:?}"); assert!(wrong[0]["error"].as_str().unwrap().contains("unauthorized"), "{wrong:?}");
} }
/// A request carrying the session token reaches the control dispatcher.
#[test] #[test]
fn a_request_with_the_token_is_dispatched() { fn a_request_with_the_token_is_dispatched() {
let addr = serve_echo(); let addr = serve_echo();
+3
View File
@@ -89,6 +89,8 @@ struct CompDof {
objects: Vec<Option<usize>>, 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 { fn analyse(comp: &Comp) -> CompDof {
let free = comp.free(None); let free = comp.free(None);
let rows = comp.rows(None); let rows = comp.rows(None);
@@ -213,6 +215,7 @@ mod budget_tests {
use cadcraft_doc::{Common, ConstraintKind, EntityKind, GeomRef, Space}; use cadcraft_doc::{Common, ConstraintKind, EntityKind, GeomRef, Space};
use cadcraft_geom::{Spline, Vec2}; use cadcraft_geom::{Spline, Vec2};
/// An expensive fit-spline endpoint residual produces unknown freedom before numerical differentiation.
#[test] #[test]
fn endpoint_analysis_rejects_cubic_fit_work_before_differentiation() { fn endpoint_analysis_rejects_cubic_fit_work_before_differentiation() {
let mut d = Drawing::default(); let mut d = Drawing::default();
+2
View File
@@ -328,6 +328,8 @@ pub(crate) fn build(d: &Drawing, cons: &[&Constraint], opts: &SolveOptions) -> R
build_budgeted(d, cons, opts, &mut members_left) 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( pub(crate) fn build_budgeted(
d: &Drawing, d: &Drawing,
cons: &[&Constraint], cons: &[&Constraint],
+5
View File
@@ -117,6 +117,8 @@ pub fn rigid_points(d: &Drawing, ins: &Insert) -> Vec<Vec2> {
rigid_points_budgeted(d, ins, &mut members_left) 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> { 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 Some(block) = d.block(&ins.block) else { return Vec::new() };
let m = ins.transform(block.base.xy()); let m = ins.transform(block.base.xy());
@@ -187,6 +189,7 @@ impl Model {
Self::build_budgeted(d, handles, &mut members_left) 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 { pub(crate) fn build_budgeted(d: &Drawing, handles: &[Handle], members_left: &mut usize) -> Model {
let mut m = Model::default(); let mut m = Model::default();
for &h in handles { for &h in handles {
@@ -620,6 +623,7 @@ mod budget_tests {
use cadcraft_geom::Vec3; use cadcraft_geom::Vec3;
use std::sync::Arc; use std::sync::Arc;
/// Writing an unchanged model back leaves partial rational weights and the original spline geometry intact.
#[test] #[test]
fn write_back_keeps_partial_rational_weights() { fn write_back_keeps_partial_rational_weights() {
let mut drawing = Drawing::default(); 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] #[test]
fn rigid_sampling_budget_is_shared_between_models() { fn rigid_sampling_budget_is_shared_between_models() {
let mut d = Drawing::default(); let mut d = Drawing::default();
+1
View File
@@ -116,6 +116,7 @@ fn r_at(c: &Constraint, i: usize) -> Option<&GeomRef> {
} }
impl Built { 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 { 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)); let cost = |i: usize| self.refs.get(i).map_or(0.0, |r| m.spline_reconstruct_cost(r.handle));
match self.form { match self.form {
+1
View File
@@ -364,6 +364,7 @@ fn hostile_new_geometry_never_panics() {
let _ = dof(&d, &[Handle(0), Handle(u64::MAX)]); 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] #[test]
fn vertex_constraints_on_large_fit_spline_do_not_charge_reconstruction() { fn vertex_constraints_on_large_fit_spline_do_not_charge_reconstruction() {
let mut d = Drawing::default(); let mut d = Drawing::default();
+2
View File
@@ -0,0 +1,2 @@
mod lin_pat_parse;
mod sub_segment_alias;
+1 -1
View File
@@ -10,4 +10,4 @@ workspace = true
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
# MPL-2.0: used as an unmodified dependency only (see plan/adr/0001 and NOTICE). # MPL-2.0: used as an unmodified dependency only (see plan/adr/0001 and NOTICE).
acadrust = "0.6.4" acadrust = "0.6.5"
+46 -18
View File
@@ -4,6 +4,25 @@
//! converted to DXF bytes, which CADCraft's own DXF reader maps to its document model; saving //! 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 //! 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). //! 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)] #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
@@ -88,7 +107,6 @@ mod native {
current_style_names(&mut doc); current_style_names(&mut doc);
table_merges(&mut doc); table_merges(&mut doc);
table_style_rows(&mut doc, false); table_style_rows(&mut doc, false);
dimension_flags(&mut doc, false);
let orphans = drop_orphaned_dgn_components(&mut doc); let orphans = drop_orphaned_dgn_components(&mut doc);
if orphans > 0 { if orphans > 0 {
let (s, were) = if orphans == 1 { ("", "was") } else { ("s", "were") }; let (s, were) = if orphans == 1 { ("", "was") } else { ("s", "were") };
@@ -272,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 /// The DWG dimension flags byte. Its bit 1 is DXF group 70's bit 32 (ODA DWG specification,
/// text is at its default position) and its bit 1 is group 70's bit 32 (ODA DWG /// common dimension data), which acadrust keeps no copy of from DXF: it is set here as
/// specification, common dimension data), while acadrust 0.6.4 reads and writes bit 0 as /// CADCraft's DXF writer always sets it (#1094). Bit 0, the opposite of group 70's bit 128,
/// `text_user_positioned` itself and keeps no bit 32 from DXF. Without this, every dimension /// acadrust 0.6.5 reads and writes itself.
/// of a DWG CADCraft writes is user-positioned for AutoCAD (#1094), and every dimension of an fn dimension_flags(doc: &mut acadrust::CadDocument) {
/// 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) {
for e in doc.entities_mut() { for e in doc.entities_mut() {
let acadrust::entities::EntityType::Dimension(d) = e else { continue }; let acadrust::entities::EntityType::Dimension(d) = e else { continue };
let b = d.base_mut(); d.base_mut().dwg_flags_byte |= 0x02;
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;
}
} }
} }
@@ -505,6 +513,17 @@ mod native {
} }
fn convert(dxf: &[u8], version: Option<acadrust::DxfVersion>) -> Result<Vec<u8>, String> { fn convert(dxf: &[u8], version: Option<acadrust::DxfVersion>) -> Result<Vec<u8>, 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 data = dxf.to_vec();
let mut doc = std::panic::catch_unwind(move || acadrust::DxfReader::from_reader(Cursor::new(data)).and_then(|r| r.read())) 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())? .map_err(|_| "the DXF→DWG conversion failed".to_string())?
@@ -519,7 +538,7 @@ mod native {
} }
other_layout_owners(&mut doc); other_layout_owners(&mut doc);
table_style_rows(&mut doc, true); table_style_rows(&mut doc, true);
dimension_flags(&mut doc, true); dimension_flags(&mut doc);
drop_unwritable(&mut doc); drop_unwritable(&mut doc);
current_dimstyle(&mut doc); current_dimstyle(&mut doc);
current_text_style(&mut doc); current_text_style(&mut doc);
@@ -618,6 +637,15 @@ mod tests {
assert!(dwg_to_dxf(b"").is_err()); 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. /// A DXF with one DIMENSION per (text, group 70, subclass, points) row.
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
fn dims_dxf(dims: &[(&str, i64, &str, &[(i32, [f64; 2])])]) -> String { fn dims_dxf(dims: &[(&str, i64, &str, &[(i32, [f64; 2])])]) -> String {
+1 -1
View File
@@ -22,7 +22,7 @@ serde_json = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
log = { workspace = true } log = { workspace = true }
# R-tree spatial index for picking, window selection and object snaps (MIT OR Apache-2.0). # 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] [target.'cfg(unix)'.dependencies]
libc = "0.2" libc = "0.2"
+3
View File
@@ -44,6 +44,7 @@ pub fn read_capped(path: &Path, max: usize) -> io::Result<Vec<u8>> {
mod tests { mod tests {
use super::*; use super::*;
/// Regular-file reads accept the byte boundary and reject any larger payload.
#[test] #[test]
fn reads_within_limit_and_rejects_oversize() { fn reads_within_limit_and_rejects_oversize() {
let dir = std::env::temp_dir().join(format!("cadcraft-cr-{}", std::process::id())); 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(); std::fs::remove_dir_all(&dir).ok();
} }
/// An endless device is rejected before its contents can be buffered.
#[cfg(unix)] #[cfg(unix)]
#[test] #[test]
fn rejects_non_regular_file() { fn rejects_non_regular_file() {
@@ -66,6 +68,7 @@ mod tests {
assert!(read_capped(p, 8 << 20).is_err()); assert!(read_capped(p, 8 << 20).is_err());
} }
} }
/// Opening a FIFO without a writer returns an error without waiting for a peer.
#[cfg(unix)] #[cfg(unix)]
#[test] #[test]
fn rejects_fifo_without_a_writer() { fn rejects_fifo_without_a_writer() {
+1
View File
@@ -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<Value> { fn run_open(s: &mut Session, p: &Value) -> Result<Value> {
if let Some(_path) = str_param(p, "path") { if let Some(_path) = str_param(p, "path") {
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
+1 -2
View File
@@ -18,8 +18,7 @@ pub use craft_docpath::Refused;
/// on other platforms. /// on other platforms.
pub fn resolve(stored: &str, document: Option<&Path>) -> Result<PathBuf, Refused> { pub fn resolve(stored: &str, document: Option<&Path>) -> Result<PathBuf, Refused> {
let document = document.filter(|d| !d.as_os_str().is_empty()).map(|d| std::path::absolute(d).unwrap_or_else(|_| d.to_path_buf())); let document = document.filter(|d| !d.as_os_str().is_empty()).map(|d| std::path::absolute(d).unwrap_or_else(|_| d.to_path_buf()));
let path = craft_docpath::resolve(stored.trim(), document.as_deref())?; craft_docpath::resolve_absolute(stored.trim(), document.as_deref())
if path.is_absolute() { Ok(path) } else { Err(Refused::Invalid) }
} }
#[cfg(test)] #[cfg(test)]
+9
View File
@@ -698,11 +698,14 @@ fn wcmatch(a: &[Value]) -> R {
/// cover), but finite so truly exponential `*`-heavy patterns are rejected rather than hanging. /// cover), but finite so truly exponential `*`-heavy patterns are rejected rather than hanging.
const WCMATCH_BUDGET: u64 = 200_000_000; 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<(), ()> { fn charge_match(budget: &mut u64) -> Result<(), ()> {
*budget = budget.checked_sub(1).ok_or(())?; *budget = budget.checked_sub(1).ok_or(())?;
Ok(()) 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>>, ()> { fn split_patterns(pat: &str, budget: &mut u64) -> Result<Vec<Vec<char>>, ()> {
let mut out = vec![Vec::new()]; let mut out = vec![Vec::new()];
let mut chars = pat.chars().peekable(); let mut chars = pat.chars().peekable();
@@ -742,6 +745,7 @@ fn split_patterns(pat: &str, budget: &mut u64) -> Result<Vec<Vec<char>>, ()> {
Ok(out) 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, ()> { fn match_pattern(text: &[char], pat: &[char], budget: &mut u64) -> Result<bool, ()> {
if let Some(('~', rest)) = pat.split_first().map(|(c, r)| (*c, r)) if let Some(('~', rest)) = pat.split_first().map(|(c, r)| (*c, r))
&& !rest.is_empty() && !rest.is_empty()
@@ -757,6 +761,7 @@ fn glob(text: &[char], pat: &[char], depth: usize, budget: &mut u64) -> Result<b
glob_cached(text, pat, depth, budget, &mut std::collections::HashSet::new()) 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( fn glob_cached(
text: &[char], text: &[char],
pat: &[char], pat: &[char],
@@ -778,6 +783,7 @@ fn glob_cached(
Ok(matched) Ok(matched)
} }
/// Evaluate wildcard tokens and backtracking branches under shared work and recursion limits.
fn glob_inner( fn glob_inner(
text: &[char], text: &[char],
pat: &[char], pat: &[char],
@@ -1060,6 +1066,7 @@ fn findfile(a: &[Value]) -> R {
Ok(if file_exists(&p) { Value::Str(p) } else { Value::Nil }) 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"))] #[cfg(not(target_arch = "wasm32"))]
pub fn read_text_file(path: &str) -> Option<String> { 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 // 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<String> {
let bytes = crate::capped_read::read_capped(std::path::Path::new(path), MAX).ok()?; let bytes = crate::capped_read::read_capped(std::path::Path::new(path), MAX).ok()?;
Some(String::from_utf8_lossy(&bytes).into_owned()) Some(String::from_utf8_lossy(&bytes).into_owned())
} }
/// Return `None` on the web, where native filesystem paths are unavailable.
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
pub fn read_text_file(_path: &str) -> Option<String> { pub fn read_text_file(_path: &str) -> Option<String> {
None None
@@ -1588,6 +1596,7 @@ pub fn wild_match(text: &str, pattern: &str) -> bool {
mod wildcard_budget_tests { mod wildcard_budget_tests {
use super::*; use super::*;
/// Pattern splitting, literal matching, and bracket scans all stop when their shared fuel runs out.
#[test] #[test]
fn all_native_scans_consume_fuel() { fn all_native_scans_consume_fuel() {
assert!(split_patterns("abcdefgh", &mut 4).is_err()); assert!(split_patterns("abcdefgh", &mut 4).is_err());
+3
View File
@@ -158,6 +158,8 @@ pub fn from_json(v: &serde_json::Value) -> Value {
const TO_JSON_BUDGET: u64 = 2_000_000; const TO_JSON_BUDGET: u64 = 2_000_000;
const TO_JSON_MAX_DEPTH: usize = 256; 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> { fn to_json_bounded(v: &Value, depth: usize, budget: &mut u64) -> Option<serde_json::Value> {
if depth > TO_JSON_MAX_DEPTH || *budget == 0 { if depth > TO_JSON_MAX_DEPTH || *budget == 0 {
return None; return None;
@@ -185,6 +187,7 @@ 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())) 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> { pub fn setvar(ctx: &mut Ctx<'_>, a: Vec<Value>) -> Result<Flow, LispError> {
arity(&a, 2, 2)?; arity(&a, 2, 2)?;
let name = string(&a[0])?; let name = string(&a[0])?;
+1
View File
@@ -131,6 +131,7 @@ pub(crate) struct Limits {
} }
impl Default for Limits { impl Default for Limits {
/// Provide finite cons-cell, string-byte, and synchronous-callback limits for a LISP run.
fn default() -> Self { fn default() -> Self {
Limits { cells: value::MAX_CELLS, string_bytes: value::STRING_BUDGET, callback_steps: 500_000_000 } Limits { cells: value::MAX_CELLS, string_bytes: value::STRING_BUDGET, callback_steps: 500_000_000 }
} }
+5
View File
@@ -221,6 +221,7 @@ fn runaway_routines_are_errors_not_crashes() {
assert_eq!(r, Ok("3000000".into())); assert_eq!(r, Ok("3000000".into()));
} }
/// An infinite comparator stops at its callback cap, and a later finite comparator still succeeds.
#[test] #[test]
fn synchronous_callback_loop_is_an_error_not_a_hang() { 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 // 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)"); 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] #[test]
fn a_zero_callback_budget_does_not_run_the_callback() { fn a_zero_callback_budget_does_not_run_the_callback() {
let mut s = session(); 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"); 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] #[test]
fn a_callback_must_fit_within_a_partial_slice() { fn a_callback_must_fit_within_a_partial_slice() {
let mut s = session(); 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"); 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] #[test]
fn reaching_a_callback_slice_boundary_does_not_grant_another_slice() { fn reaching_a_callback_slice_boundary_does_not_grant_another_slice() {
let counts: Vec<u64> = [399_999, 400_000] let counts: Vec<u64> = [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:?}"); 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] #[test]
fn setvar_rejects_an_oversized_shared_structure() { fn setvar_rejects_an_oversized_shared_structure() {
// A list repeatedly sharing its own contents expands exponentially; setvar's bounded JSON // A list repeatedly sharing its own contents expands exponentially; setvar's bounded JSON
+1
View File
@@ -353,6 +353,7 @@ pub fn equal(a: &Value, b: &Value, fuzz: f64) -> bool {
/// Largest number of node comparisons one `equal` call may perform. /// Largest number of node comparisons one `equal` call may perform.
const EQUAL_BUDGET: u64 = 50_000_000; 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 { fn equal_depth(a: &Value, b: &Value, fuzz: f64, depth: usize, budget: &mut u64) -> bool {
if depth > MAX_DEPTH || *budget == 0 { if depth > MAX_DEPTH || *budget == 0 {
return false; return false;
+1 -1
View File
@@ -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 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 env = AABB::from_corners([b.min.x - pad, b.min.y - pad], [b.max.x + pad, b.max.y + pad]);
let mut out: Vec<u32> = let mut out: Vec<u32> =
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 { if with_infinite {
out.extend_from_slice(&self.infinite); out.extend_from_slice(&self.infinite);
} }
@@ -29,7 +29,7 @@ fn session() -> (Session, Handle) {
/// As [`session`], with the fixture's text changed by `edit` first. /// As [`session`], with the fixture's text changed by `edit` first.
fn session_with(edit: impl Fn(String) -> String) -> (Session, Handle) { fn session_with(edit: impl Fn(String) -> String) -> (Session, Handle) {
let grip = "0 BLOCKLINEARGRIP\n5 68\n330 61\n100 AcDbEvalExpr\n90 9\n98 33\n99 232\n100 AcDbBlockElement\n300 End Grip\n98 33\n99 232\n1071 0\n"; let grip = "0 BLOCKLINEARGRIP\n5 68\n330 61\n100 AcDbEvalExpr\n90 9\n98 33\n99 232\n100 AcDbBlockElement\n300 End Grip\n98 33\n99 232\n1071 0\n";
let src = include_str!("../../io/tests/data/dynblock_door.txt") let src = include_str!("../../../io/tests/data/dynblock_door.txt")
.replace("\r\n", "\n") .replace("\r\n", "\n")
.replace("170 4\n91 0\n91 0\n", "170 4\n91 8\n91 9\n") .replace("170 4\n91 0\n91 0\n", "170 4\n91 8\n91 9\n")
.replace("1030 0\n93 0\n170 0\n171 0\n100 AcDbBlockVisibilityParameter", "1030 0\n93 10\n170 0\n171 0\n100 AcDbBlockVisibilityParameter") .replace("1030 0\n93 0\n170 0\n171 0\n100 AcDbBlockVisibilityParameter", "1030 0\n93 10\n170 0\n171 0\n100 AcDbBlockVisibilityParameter")
@@ -2,7 +2,6 @@
//! leave exactly the expected entities, layers and printed text. The corpus lives in its own //! leave exactly the expected entities, layers and printed text. The corpus lives in its own
//! repository; point `CADCRAFT_LISP_CORPUS` at its `cases` directory (or the repository) to run //! repository; point `CADCRAFT_LISP_CORPUS` at its `cases` directory (or the repository) to run
//! it. Without it the test only says so. //! it. Without it the test only says so.
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};

Some files were not shown because too many files have changed in this diff Show More