mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-24 10:56:51 -06:00
Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0e83aefa6 | ||
|
|
9d8f16c825 | ||
|
|
f043540d13 | ||
|
|
78decf80f7 | ||
|
|
8ad9a3ce15 | ||
|
|
251fddb658 | ||
|
|
51db2d52e4 | ||
|
|
9621dcbd94 | ||
|
|
5648a06ac4 | ||
|
|
f344cf4460 | ||
|
|
e61e328edb | ||
|
|
611eb9f5a9 | ||
|
|
1977a493c7 | ||
|
|
a08fd7c1d1 | ||
|
|
fa6bb0351b | ||
|
|
48b4165591 |
@@ -14,6 +14,17 @@ updates:
|
||||
- "*"
|
||||
commit-message:
|
||||
prefix: "chore(deps)"
|
||||
# craft-ui is still on egui 0.36, and two eguis in the graph don't build (#1990). Remove this
|
||||
# once craft-ui moves to egui 0.37 (storytold/craft-libs#199).
|
||||
ignore:
|
||||
- dependency-name: "egui"
|
||||
versions: [">=0.37"]
|
||||
- dependency-name: "eframe"
|
||||
versions: [">=0.37"]
|
||||
- dependency-name: "egui-wgpu"
|
||||
versions: [">=0.37"]
|
||||
- dependency-name: "egui_kittest"
|
||||
versions: [">=0.37"]
|
||||
|
||||
# Maintain GitHub Actions workflows
|
||||
- package-ecosystem: "github-actions"
|
||||
|
||||
Generated
+71
-136
@@ -4,9 +4,9 @@ version = 4
|
||||
|
||||
[[package]]
|
||||
name = "acadrust"
|
||||
version = "0.6.4"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94862189cab6e282ed0433a801a216cd1a712953813284a48fa81c67223e38f6"
|
||||
checksum = "5eaa72520f70eb6f7f09b06f44e1ed0d8fad5fa743364f2c3715c30df962ec7c"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.13.2",
|
||||
@@ -209,9 +209,9 @@ checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
||||
|
||||
[[package]]
|
||||
name = "arboard"
|
||||
version = "3.6.1"
|
||||
version = "3.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf"
|
||||
checksum = "aeb9553e62312c8756a5979254fc6d7da8bbeb5986a1e17037fab932f7037917"
|
||||
dependencies = [
|
||||
"clipboard-win",
|
||||
"image",
|
||||
@@ -223,7 +223,7 @@ dependencies = [
|
||||
"objc2-foundation 0.3.2",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
"x11rb",
|
||||
]
|
||||
|
||||
@@ -650,8 +650,8 @@ name = "cadcraft-fonts"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"cadcraft-geom",
|
||||
"harfrust",
|
||||
"skrifa 0.47.0",
|
||||
"harfrust 0.14.0",
|
||||
"skrifa 0.48.0",
|
||||
"unicode-bidi",
|
||||
]
|
||||
|
||||
@@ -674,8 +674,8 @@ dependencies = [
|
||||
"cadcraft-fonts",
|
||||
"cadcraft-geom",
|
||||
"cadcraft-render",
|
||||
"miniz_oxide 0.8.9",
|
||||
"quick-xml",
|
||||
"miniz_oxide 0.9.1",
|
||||
"quick-xml 0.42.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.21",
|
||||
@@ -689,7 +689,7 @@ dependencies = [
|
||||
"cadcraft-engine",
|
||||
"cadcraft-io",
|
||||
"cadcraft-render",
|
||||
"getrandom 0.3.4",
|
||||
"getrandom 0.4.3",
|
||||
"log",
|
||||
"serde_json",
|
||||
]
|
||||
@@ -1312,7 +1312,7 @@ dependencies = [
|
||||
"emath",
|
||||
"epaint_default_fonts",
|
||||
"font-types",
|
||||
"harfrust",
|
||||
"harfrust 0.12.0",
|
||||
"log",
|
||||
"nohash-hasher",
|
||||
"parking_lot",
|
||||
@@ -1441,9 +1441,9 @@ checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "font-types"
|
||||
version = "0.12.6"
|
||||
version = "0.12.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c92d9081c7ab3bd83ef1eecdfad090f3d966e3ce74106779664114cfe1a6274"
|
||||
checksum = "835ae9d781464a79026cde387f9cbc650c9a4909d7cf7e658b127e3c35d2f35d"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"serde",
|
||||
@@ -1734,6 +1734,18 @@ dependencies = [
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "harfrust"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86aa776d6d32b072119e1f1650f4db843ff4382a4aaa6a2c9c4e35703cd377cb"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"bytemuck",
|
||||
"read-fonts 0.45.1",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hash32"
|
||||
version = "0.3.1"
|
||||
@@ -1914,7 +1926,7 @@ dependencies = [
|
||||
"byteorder-lite",
|
||||
"moxcms",
|
||||
"num-traits",
|
||||
"png 0.18.1",
|
||||
"png",
|
||||
"tiff",
|
||||
"zune-core",
|
||||
"zune-jpeg",
|
||||
@@ -2100,9 +2112,9 @@ checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.190"
|
||||
version = "0.2.191"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
|
||||
checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
@@ -2200,9 +2212,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lyon_tessellation"
|
||||
version = "1.0.22"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43b8dcf906637ecef61b3c0740c7a4e7f27caeb31257cfac0cc579ce15be6005"
|
||||
checksum = "fd4872650e70dc907dd4f7ebc449d7d657a0d2658e423b2e724970870bbbf75b"
|
||||
dependencies = [
|
||||
"float_next_after",
|
||||
"lyon_path",
|
||||
@@ -2271,9 +2283,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "muda"
|
||||
version = "0.21.0"
|
||||
version = "0.21.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6744ab645ebe79f9c428455c7cb9dfa4275fa3b50a25408e08f927da8dc618c"
|
||||
checksum = "4340e89c178916e62879c5c36b7f699019369be413e496178a6983ce91b8bc03"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"dpi",
|
||||
@@ -2282,8 +2294,7 @@ dependencies = [
|
||||
"objc2-app-kit 0.3.2",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation 0.3.2",
|
||||
"once_cell",
|
||||
"png 0.18.1",
|
||||
"png",
|
||||
"thiserror 2.0.21",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
@@ -2945,19 +2956,6 @@ version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.17.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"crc32fast",
|
||||
"fdeflate",
|
||||
"flate2",
|
||||
"miniz_oxide 0.8.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.18.1"
|
||||
@@ -3081,6 +3079,15 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
@@ -3159,13 +3166,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "read-fonts"
|
||||
version = "0.44.0"
|
||||
version = "0.45.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d5a0cf4bb6cbce4c29756270d189a5e400ac05e91192161d83a0bcb6e68492e8"
|
||||
checksum = "4edc8db0d972de88e53a085b1f01fb3fbdae7bb0cb7a0ce2b2f8285a412fe3ef"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"foldhash",
|
||||
"font-types",
|
||||
"once_cell",
|
||||
"yoke",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3244,9 +3253,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rstar"
|
||||
version = "0.12.2"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "421400d13ccfd26dfa5858199c30a5d76f9c54e0dba7575273025b43c5175dbb"
|
||||
checksum = "5912b862fa5ffb462607bfd1e35036c458c537921f508c8235a83d5f3987edfe"
|
||||
dependencies = [
|
||||
"heapless",
|
||||
"num-traits",
|
||||
@@ -3377,9 +3386,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.151"
|
||||
version = "1.0.154"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||
checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
@@ -3464,12 +3473,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "skrifa"
|
||||
version = "0.47.0"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "526e654b303ff4ad73a5a40b25de2b3ce3d039c5e3b6da5f89ae6e57004cb40f"
|
||||
checksum = "a5213b4825bac3587046330052d61bfac7bd3bd0d203c758d7c2ef726fcea034"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"read-fonts 0.44.0",
|
||||
"read-fonts 0.45.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3715,24 +3724,24 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tiny-skia"
|
||||
version = "0.11.4"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab"
|
||||
checksum = "47ffee5eaaf5527f630fb0e356b90ebdec84d5d18d937c5e440350f88c5a91ea"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"arrayvec",
|
||||
"bytemuck",
|
||||
"cfg-if",
|
||||
"log",
|
||||
"png 0.17.16",
|
||||
"png",
|
||||
"tiny-skia-path",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-skia-path"
|
||||
version = "0.11.4"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93"
|
||||
checksum = "edca365c3faccca67d06593c5980fa6c57687de727a03131735bb85f01fdeeb9"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"bytemuck",
|
||||
@@ -4153,7 +4162,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quick-xml",
|
||||
"quick-xml 0.41.0",
|
||||
"quote",
|
||||
]
|
||||
|
||||
@@ -4510,7 +4519,7 @@ version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4519,16 +4528,7 @@ version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||
dependencies = [
|
||||
"windows-targets 0.53.5",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4546,31 +4546,14 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.6",
|
||||
"windows_aarch64_msvc 0.52.6",
|
||||
"windows_i686_gnu 0.52.6",
|
||||
"windows_i686_gnullvm 0.52.6",
|
||||
"windows_i686_msvc 0.52.6",
|
||||
"windows_x86_64_gnu 0.52.6",
|
||||
"windows_x86_64_gnullvm 0.52.6",
|
||||
"windows_x86_64_msvc 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.53.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows_aarch64_gnullvm 0.53.1",
|
||||
"windows_aarch64_msvc 0.53.1",
|
||||
"windows_i686_gnu 0.53.1",
|
||||
"windows_i686_gnullvm 0.53.1",
|
||||
"windows_i686_msvc 0.53.1",
|
||||
"windows_x86_64_gnu 0.53.1",
|
||||
"windows_x86_64_gnullvm 0.53.1",
|
||||
"windows_x86_64_msvc 0.53.1",
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4588,96 +4571,48 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||
|
||||
[[package]]
|
||||
name = "winit"
|
||||
version = "0.30.13"
|
||||
@@ -4740,9 +4675,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "winresource"
|
||||
version = "0.1.31"
|
||||
version = "0.1.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0986a8b1d586b7d3e4fe3d9ea39fb451ae22869dcea4aa109d287a374d866087"
|
||||
checksum = "a2bc6d36480b829272a5bdabf08e6dca8004761c1da7e35efef800540c66221b"
|
||||
dependencies = [
|
||||
"toml",
|
||||
"version_check",
|
||||
|
||||
+6
-6
@@ -37,17 +37,17 @@ chrono = { version = "0.4.45", default-features = false, features = ["std"] }
|
||||
rayon = "1"
|
||||
|
||||
# rendering / formats
|
||||
tiny-skia = { version = "0.11", default-features = false, features = ["std", "simd", "png-format"] }
|
||||
tiny-skia = { version = "0.12", default-features = false, features = ["std", "simd", "png-format"] }
|
||||
lyon_tessellation = "1.0.22"
|
||||
skrifa = "0.47"
|
||||
# Text shaping (OpenType GSUB/GPOS, HarfBuzz port on read-fonts; the version egui's epaint uses)
|
||||
# and the Unicode Bidirectional Algorithm for right-to-left drawing text.
|
||||
harfrust = "0.12"
|
||||
skrifa = "0.48"
|
||||
# Text shaping (OpenType GSUB/GPOS, HarfBuzz port on read-fonts; egui's epaint is still on 0.12,
|
||||
# so the graph has both) and the Unicode Bidirectional Algorithm for right-to-left drawing text.
|
||||
harfrust = "0.14"
|
||||
unicode-bidi = "0.3.18"
|
||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg"] }
|
||||
encoding_rs = "0.8.35"
|
||||
# XML for the SVG reader (streaming parser, no entity expansion beyond the five predefined ones)
|
||||
quick-xml = "0.41"
|
||||
quick-xml = "0.42"
|
||||
|
||||
# ui
|
||||
craft-ui = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "5c874d51eeb3de70e735c8ce87a59a7dedd47675", features = ["serde"] }
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<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>
|
||||
<link data-trunk rel="rust" data-bin="cadcraft-web" data-wasm-opt="z" data-weak-refs />
|
||||
<style>
|
||||
|
||||
@@ -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> {
|
||||
match TcpListener::bind(("127.0.0.1", port)) {
|
||||
Ok(l) => {
|
||||
@@ -101,6 +103,7 @@ impl Drop for Slot {
|
||||
}
|
||||
}
|
||||
|
||||
/// Accept clients within the connection limit and give each worker the same session token.
|
||||
fn accept_loop(listener: TcpListener, tx: SyncSender<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)) {
|
||||
@@ -162,6 +165,7 @@ enum Line {
|
||||
Reject(String),
|
||||
}
|
||||
|
||||
/// Parse a UTF-8 JSON request and its optional token, distinguishing blank lines from rejected input.
|
||||
fn parse_line(bytes: &[u8]) -> Line {
|
||||
let Ok(text) = std::str::from_utf8(bytes) else {
|
||||
return Line::Reject("not a control request (invalid UTF-8); closing the connection".into());
|
||||
@@ -198,6 +202,8 @@ fn is_http_request_line(text: &str) -> bool {
|
||||
|| text.rsplit_once(' ').is_some_and(|(_, version)| version.starts_with("HTTP/"))
|
||||
}
|
||||
|
||||
/// Serve bounded JSON lines, authenticating each request before enqueueing it for the UI.
|
||||
/// Invalid credentials close the connection; queue and reply limits report whether a request ran.
|
||||
fn serve(stream: TcpStream, tx: &SyncSender<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;
|
||||
@@ -385,6 +391,7 @@ mod tests {
|
||||
assert!(t.elapsed() < Duration::from_secs(4));
|
||||
}
|
||||
|
||||
/// Authenticated requests rejected by a full queue retain their IDs and report that they did not run.
|
||||
#[test]
|
||||
fn a_full_queue_answers_busy() {
|
||||
let (addr, rx) = serve_with(short());
|
||||
@@ -407,6 +414,7 @@ mod tests {
|
||||
drop(rx);
|
||||
}
|
||||
|
||||
/// Replies on one authenticated connection preserve the ID of each request.
|
||||
#[test]
|
||||
fn requests_are_answered_with_their_ids() {
|
||||
let addr = serve_echo();
|
||||
@@ -423,6 +431,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Missing and incorrect session tokens are rejected before dispatch.
|
||||
#[test]
|
||||
fn a_request_without_the_token_is_unauthorized() {
|
||||
// A local peer that does not hold the session token cannot drive the app (CWE-306).
|
||||
@@ -437,6 +446,7 @@ mod tests {
|
||||
assert!(wrong[0]["error"].as_str().unwrap().contains("unauthorized"), "{wrong:?}");
|
||||
}
|
||||
|
||||
/// A request carrying the session token reaches the control dispatcher.
|
||||
#[test]
|
||||
fn a_request_with_the_token_is_dispatched() {
|
||||
let addr = serve_echo();
|
||||
|
||||
@@ -89,6 +89,8 @@ 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);
|
||||
@@ -213,6 +215,7 @@ mod budget_tests {
|
||||
use cadcraft_doc::{Common, ConstraintKind, EntityKind, GeomRef, Space};
|
||||
use cadcraft_geom::{Spline, Vec2};
|
||||
|
||||
/// An expensive fit-spline endpoint residual produces unknown freedom before numerical differentiation.
|
||||
#[test]
|
||||
fn endpoint_analysis_rejects_cubic_fit_work_before_differentiation() {
|
||||
let mut d = Drawing::default();
|
||||
|
||||
@@ -328,6 +328,8 @@ pub(crate) fn build(d: &Drawing, cons: &[&Constraint], opts: &SolveOptions) -> R
|
||||
build_budgeted(d, cons, opts, &mut members_left)
|
||||
}
|
||||
|
||||
/// Build constraint components with one caller-owned rigid-member sampling budget shared by every model.
|
||||
/// Return constraints that cannot be interpreted as skipped entries.
|
||||
pub(crate) fn build_budgeted(
|
||||
d: &Drawing,
|
||||
cons: &[&Constraint],
|
||||
|
||||
@@ -117,6 +117,8 @@ 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());
|
||||
@@ -187,6 +189,7 @@ impl Model {
|
||||
Self::build_budgeted(d, handles, &mut members_left)
|
||||
}
|
||||
|
||||
/// Build solver parameters for supported entities while sharing rigid-member sampling work with other models.
|
||||
pub(crate) fn build_budgeted(d: &Drawing, handles: &[Handle], members_left: &mut usize) -> Model {
|
||||
let mut m = Model::default();
|
||||
for &h in handles {
|
||||
@@ -620,6 +623,7 @@ mod budget_tests {
|
||||
use cadcraft_geom::Vec3;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Writing 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();
|
||||
@@ -635,6 +639,7 @@ mod budget_tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// A second model receives no sampled block points after the first model spends the shared member budget.
|
||||
#[test]
|
||||
fn rigid_sampling_budget_is_shared_between_models() {
|
||||
let mut d = Drawing::default();
|
||||
|
||||
@@ -116,6 +116,7 @@ fn r_at(c: &Constraint, i: usize) -> Option<&GeomRef> {
|
||||
}
|
||||
|
||||
impl Built {
|
||||
/// Estimate spline reconstruction work for endpoint and join residuals; other residual forms add no such cost.
|
||||
pub(crate) fn spline_reconstruct_cost(&self, m: &Model) -> f64 {
|
||||
let cost = |i: usize| self.refs.get(i).map_or(0.0, |r| m.spline_reconstruct_cost(r.handle));
|
||||
match self.form {
|
||||
|
||||
@@ -364,6 +364,7 @@ fn hostile_new_geometry_never_panics() {
|
||||
let _ = dof(&d, &[Handle(0), Handle(u64::MAX)]);
|
||||
}
|
||||
|
||||
/// Fit-point vertex constraints retain a known freedom count because they do not reconstruct the spline.
|
||||
#[test]
|
||||
fn vertex_constraints_on_large_fit_spline_do_not_charge_reconstruction() {
|
||||
let mut d = Drawing::default();
|
||||
|
||||
@@ -10,4 +10,4 @@ workspace = true
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
# MPL-2.0: used as an unmodified dependency only (see plan/adr/0001 and NOTICE).
|
||||
acadrust = "0.6.4"
|
||||
acadrust = "0.6.5"
|
||||
|
||||
+12
-19
@@ -107,7 +107,6 @@ mod native {
|
||||
current_style_names(&mut doc);
|
||||
table_merges(&mut doc);
|
||||
table_style_rows(&mut doc, false);
|
||||
dimension_flags(&mut doc, false);
|
||||
let orphans = drop_orphaned_dgn_components(&mut doc);
|
||||
if orphans > 0 {
|
||||
let (s, were) = if orphans == 1 { ("", "was") } else { ("s", "were") };
|
||||
@@ -291,24 +290,14 @@ mod native {
|
||||
}
|
||||
}
|
||||
|
||||
/// The DWG dimension flags byte. Its bit 0 is the opposite of DXF group 70's bit 128 (set: the
|
||||
/// text is at its default position) and its bit 1 is group 70's bit 32 (ODA DWG
|
||||
/// specification, common dimension data), while acadrust 0.6.4 reads and writes bit 0 as
|
||||
/// `text_user_positioned` itself and keeps no bit 32 from DXF. Without this, every dimension
|
||||
/// of a DWG CADCraft writes is user-positioned for AutoCAD (#1094), and every dimension of an
|
||||
/// AutoCAD DWG is read as user-positioned. `to_dwg` sets the fields up for the writer:
|
||||
/// bit 32 as CADCraft's DXF writer always sets it, and bit 0 through the inverted field;
|
||||
/// otherwise the field is read from bit 0 after a DWG is read.
|
||||
fn dimension_flags(doc: &mut acadrust::CadDocument, to_dwg: bool) {
|
||||
/// The DWG dimension flags byte. Its bit 1 is DXF group 70's bit 32 (ODA DWG specification,
|
||||
/// common dimension data), which acadrust keeps no copy of from DXF: it is set here as
|
||||
/// CADCraft's DXF writer always sets it (#1094). Bit 0, the opposite of group 70's bit 128,
|
||||
/// acadrust 0.6.5 reads and writes itself.
|
||||
fn dimension_flags(doc: &mut acadrust::CadDocument) {
|
||||
for e in doc.entities_mut() {
|
||||
let acadrust::entities::EntityType::Dimension(d) = e else { continue };
|
||||
let b = d.base_mut();
|
||||
if to_dwg {
|
||||
b.dwg_flags_byte |= 0x02;
|
||||
b.text_user_positioned = !b.text_user_positioned;
|
||||
} else {
|
||||
b.text_user_positioned = b.dwg_flags_byte & 0x01 == 0;
|
||||
}
|
||||
d.base_mut().dwg_flags_byte |= 0x02;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -529,7 +518,11 @@ mod native {
|
||||
// interrupted once it starts (CWE-789). This bounds the large-input variant; the decoder
|
||||
// runs inside `catch_unwind` so a decode failure is reported rather than crashing.
|
||||
if dxf.len() > Limits::DEFAULT.max_dxf_bytes {
|
||||
return Err(format!("DXF: the input is {}, larger than the {} CADCraft converts to DWG", mb(dxf.len() as u64), mb(Limits::DEFAULT.max_dxf_bytes as u64)));
|
||||
return Err(format!(
|
||||
"DXF: the input is {}, larger than the {} CADCraft converts to DWG",
|
||||
mb(dxf.len() as u64),
|
||||
mb(Limits::DEFAULT.max_dxf_bytes as u64)
|
||||
));
|
||||
}
|
||||
let data = dxf.to_vec();
|
||||
let mut doc = std::panic::catch_unwind(move || acadrust::DxfReader::from_reader(Cursor::new(data)).and_then(|r| r.read()))
|
||||
@@ -545,7 +538,7 @@ mod native {
|
||||
}
|
||||
other_layout_owners(&mut doc);
|
||||
table_style_rows(&mut doc, true);
|
||||
dimension_flags(&mut doc, true);
|
||||
dimension_flags(&mut doc);
|
||||
drop_unwritable(&mut doc);
|
||||
current_dimstyle(&mut doc);
|
||||
current_text_style(&mut doc);
|
||||
|
||||
@@ -22,7 +22,7 @@ serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
log = { workspace = true }
|
||||
# R-tree spatial index for picking, window selection and object snaps (MIT OR Apache-2.0).
|
||||
rstar = { version = "0.12", default-features = false }
|
||||
rstar = { version = "0.13", default-features = false }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2"
|
||||
|
||||
@@ -44,6 +44,7 @@ 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()));
|
||||
@@ -57,6 +58,7 @@ mod tests {
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
}
|
||||
|
||||
/// An endless device is rejected before its contents can be buffered.
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn rejects_non_regular_file() {
|
||||
@@ -66,6 +68,7 @@ mod tests {
|
||||
assert!(read_capped(p, 8 << 20).is_err());
|
||||
}
|
||||
}
|
||||
/// Opening a FIFO without a writer returns an error without waiting for a peer.
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn rejects_fifo_without_a_writer() {
|
||||
|
||||
@@ -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> {
|
||||
if let Some(_path) = str_param(p, "path") {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
|
||||
@@ -698,11 +698,14 @@ fn wcmatch(a: &[Value]) -> R {
|
||||
/// cover), but finite so truly exponential `*`-heavy patterns are rejected rather than hanging.
|
||||
const WCMATCH_BUDGET: u64 = 200_000_000;
|
||||
|
||||
/// Spend one wildcard work unit, returning an error when the shared budget is exhausted.
|
||||
fn charge_match(budget: &mut u64) -> Result<(), ()> {
|
||||
*budget = budget.checked_sub(1).ok_or(())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Split comma-separated wildcard alternatives while preserving escapes and bracket sets.
|
||||
/// Every consumed character spends from the same budget used for matching.
|
||||
fn split_patterns(pat: &str, budget: &mut u64) -> Result<Vec<Vec<char>>, ()> {
|
||||
let mut out = vec![Vec::new()];
|
||||
let mut chars = pat.chars().peekable();
|
||||
@@ -742,6 +745,7 @@ fn split_patterns(pat: &str, budget: &mut u64) -> Result<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()
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
/// Match suffixes while memoizing failures; both matching work and retained failure entries are bounded.
|
||||
fn glob_cached(
|
||||
text: &[char],
|
||||
pat: &[char],
|
||||
@@ -778,6 +783,7 @@ fn glob_cached(
|
||||
Ok(matched)
|
||||
}
|
||||
|
||||
/// Evaluate wildcard tokens and backtracking branches under shared work and recursion limits.
|
||||
fn glob_inner(
|
||||
text: &[char],
|
||||
pat: &[char],
|
||||
@@ -1060,6 +1066,7 @@ 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
|
||||
@@ -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()?;
|
||||
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
|
||||
@@ -1588,6 +1596,7 @@ pub fn wild_match(text: &str, pattern: &str) -> bool {
|
||||
mod wildcard_budget_tests {
|
||||
use super::*;
|
||||
|
||||
/// Pattern splitting, literal matching, and bracket scans all stop when their shared fuel runs out.
|
||||
#[test]
|
||||
fn all_native_scans_consume_fuel() {
|
||||
assert!(split_patterns("abcdefgh", &mut 4).is_err());
|
||||
|
||||
@@ -158,6 +158,8 @@ pub fn from_json(v: &serde_json::Value) -> Value {
|
||||
const TO_JSON_BUDGET: u64 = 2_000_000;
|
||||
const TO_JSON_MAX_DEPTH: usize = 256;
|
||||
|
||||
/// Convert a LISP value to owned JSON within element and nesting limits.
|
||||
/// Return `None` before a shared cons structure can expand beyond the remaining budget.
|
||||
fn to_json_bounded(v: &Value, depth: usize, budget: &mut u64) -> Option<serde_json::Value> {
|
||||
if depth > TO_JSON_MAX_DEPTH || *budget == 0 {
|
||||
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()))
|
||||
}
|
||||
|
||||
/// 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])?;
|
||||
|
||||
@@ -131,6 +131,7 @@ pub(crate) struct Limits {
|
||||
}
|
||||
|
||||
impl Default for Limits {
|
||||
/// Provide finite cons-cell, string-byte, and synchronous-callback limits for a LISP run.
|
||||
fn default() -> Self {
|
||||
Limits { cells: value::MAX_CELLS, string_bytes: value::STRING_BUDGET, callback_steps: 500_000_000 }
|
||||
}
|
||||
|
||||
@@ -221,6 +221,7 @@ fn runaway_routines_are_errors_not_crashes() {
|
||||
assert_eq!(r, Ok("3000000".into()));
|
||||
}
|
||||
|
||||
/// An infinite comparator stops at its callback cap, and a later finite comparator still succeeds.
|
||||
#[test]
|
||||
fn synchronous_callback_loop_is_an_error_not_a_hang() {
|
||||
// A comparator with an infinite loop used to run with u64::MAX fuel and hang the thread; it
|
||||
@@ -232,6 +233,7 @@ fn synchronous_callback_loop_is_an_error_not_a_hang() {
|
||||
assert_eq!(ok(&mut s, "(vl-sort '(3 1 2) '<)"), "(1 2 3)");
|
||||
}
|
||||
|
||||
/// A zero-step callback limit prevents even the first callback side effect.
|
||||
#[test]
|
||||
fn a_zero_callback_budget_does_not_run_the_callback() {
|
||||
let mut s = session();
|
||||
@@ -242,6 +244,7 @@ fn a_zero_callback_budget_does_not_run_the_callback() {
|
||||
assert_eq!(ok(&mut s, "callback-ran"), "nil");
|
||||
}
|
||||
|
||||
/// A callback stops at a cap smaller than one scheduling slice without completing all its assignments.
|
||||
#[test]
|
||||
fn a_callback_must_fit_within_a_partial_slice() {
|
||||
let mut s = session();
|
||||
@@ -252,6 +255,7 @@ fn a_callback_must_fit_within_a_partial_slice() {
|
||||
assert_eq!(ok(&mut s, "(and (> callback-count 0) (< callback-count 1000))"), "T");
|
||||
}
|
||||
|
||||
/// Crossing a scheduling boundary grants only the additional configured step, rather than another slice.
|
||||
#[test]
|
||||
fn reaching_a_callback_slice_boundary_does_not_grant_another_slice() {
|
||||
let counts: Vec<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:?}");
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
||||
@@ -353,6 +353,7 @@ pub fn equal(a: &Value, b: &Value, fuzz: f64) -> bool {
|
||||
/// Largest number of node comparisons one `equal` call may perform.
|
||||
const EQUAL_BUDGET: u64 = 50_000_000;
|
||||
|
||||
/// Compare nested values within depth and node-work limits, returning false when either limit is reached.
|
||||
fn equal_depth(a: &Value, b: &Value, fuzz: f64, depth: usize, budget: &mut u64) -> bool {
|
||||
if depth > MAX_DEPTH || *budget == 0 {
|
||||
return false;
|
||||
|
||||
@@ -208,7 +208,7 @@ impl SpatialIndex {
|
||||
let pad = (b.width() + b.height()) * 1e-9 + (b.min.x.abs() + b.min.y.abs() + b.max.x.abs() + b.max.y.abs()) * 1e-12 + 1e-300;
|
||||
let env = AABB::from_corners([b.min.x - pad, b.min.y - pad], [b.max.x + pad, b.max.y + pad]);
|
||||
let mut out: Vec<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 {
|
||||
out.extend_from_slice(&self.infinite);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,9 @@ fn main() {
|
||||
Err(e) if std::env::var_os("CRAFT_FONTS_REQUIRED").is_some() => {
|
||||
println!("cargo::error=CRAFT_FONTS_DIR={}: {}", one_line(&dir.display().to_string()), one_line(&e));
|
||||
}
|
||||
Err(e) => println!("cargo::warning=building without craft-fonts: CRAFT_FONTS_DIR={}: {}", one_line(&dir.display().to_string()), one_line(&e)),
|
||||
Err(e) => {
|
||||
println!("cargo::warning=building without craft-fonts: CRAFT_FONTS_DIR={}: {}", one_line(&dir.display().to_string()), one_line(&e))
|
||||
}
|
||||
}
|
||||
}
|
||||
src.push_str("];\n");
|
||||
|
||||
+42
-11
@@ -8,6 +8,7 @@
|
||||
|
||||
use std::ops::Range;
|
||||
use std::panic::{AssertUnwindSafe, catch_unwind};
|
||||
use std::sync::Arc;
|
||||
|
||||
use cadcraft_geom::Vec2;
|
||||
|
||||
@@ -60,6 +61,7 @@ pub(crate) fn shape(primary: Option<&Face>, s: &str, height: f64, width_factor:
|
||||
catch_unwind(AssertUnwindSafe(|| shape_chars(primary, &chars, height, width_factor, oblique))).ok()
|
||||
}
|
||||
|
||||
/// Lay out decoded directional runs, keeping advances and decoration spans for empty glyphs.
|
||||
fn shape_chars(primary: Option<&Face>, chars: &[(char, bool, bool)], height: f64, width_factor: f64, oblique: f64) -> Shaped {
|
||||
let h = if height.is_finite() && height > 0.0 { height } else { 1.0 };
|
||||
let wf = if width_factor.is_finite() && width_factor.abs() > 1e-6 { width_factor } else { 1.0 };
|
||||
@@ -181,28 +183,31 @@ struct Glyph {
|
||||
/// Shape characters `r` of `cs` (one direction, one face) into glyphs in display order.
|
||||
fn shape_run(face: &Face, cs: &[char], r: Range<usize>, rtl: bool) -> Vec<Glyph> {
|
||||
let run = cs.get(r.clone()).unwrap_or_default();
|
||||
let Ok(font) = harfrust::FontRef::from_index(&face.bytes, face.index) else { return Vec::new() };
|
||||
let data = harfrust::ShaperData::new(&font);
|
||||
let shaper = data.shaper(&font).build();
|
||||
let mut buf = harfrust::UnicodeBuffer::new();
|
||||
// Shares the face's bytes instead of copying them.
|
||||
let bytes: Arc<dyn AsRef<[u8]> + Send + Sync> = face.bytes.clone();
|
||||
let Some(font) = harfrust::Font::new(bytes, face.index) else { return Vec::new() };
|
||||
let shaper = harfrust::ShaperFont::new(&font);
|
||||
let mut buf = harfrust::Buffer::new();
|
||||
let arabic = run.iter().any(|c| matches!(*c, '\u{0600}'..='\u{08FF}'));
|
||||
if arabic && !face.has_joining_features() {
|
||||
for (c, i) in bidi::presentation_forms(run, |p| face.has_glyph(p)) {
|
||||
buf.add(c, u32::try_from(r.start + i).unwrap_or(u32::MAX));
|
||||
buf.push(u32::from(c), u32::try_from(r.start + i).unwrap_or(u32::MAX));
|
||||
}
|
||||
} else {
|
||||
for (i, c) in run.iter().enumerate() {
|
||||
buf.add(*c, u32::try_from(r.start + i).unwrap_or(u32::MAX));
|
||||
buf.push(u32::from(*c), u32::try_from(r.start + i).unwrap_or(u32::MAX));
|
||||
}
|
||||
}
|
||||
buf.set_direction(if rtl { harfrust::Direction::RightToLeft } else { harfrust::Direction::LeftToRight });
|
||||
buf.guess_segment_properties();
|
||||
let shaped = shaper.shape(buf, harfrust::ShapeOptions::new());
|
||||
let k = 1000.0 / f64::from(shaper.units_per_em().max(1));
|
||||
shaped
|
||||
.glyph_infos()
|
||||
// Without a plan, shaping fails only on an already shaped buffer or an unset direction.
|
||||
if harfrust::shape(&shaper, &mut buf, harfrust::ShapeOptions::new()).is_err() {
|
||||
return Vec::new();
|
||||
}
|
||||
let k = 1000.0 / f64::from(font.units_per_em().max(1));
|
||||
buf.glyph_infos()
|
||||
.iter()
|
||||
.zip(shaped.glyph_positions())
|
||||
.zip(buf.glyph_positions())
|
||||
.map(|(gi, gp)| Glyph {
|
||||
gid: gi.glyph_id,
|
||||
cluster: usize::try_from(gi.cluster).unwrap_or(usize::MAX),
|
||||
@@ -290,6 +295,32 @@ mod tests {
|
||||
assert!((crate::line_width("قیمت 25 تومان", 1.0, 1.0) - sh.width).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn devanagari_run_is_reordered_with_line_clusters_and_advances_per_1000_em() {
|
||||
use skrifa::MetadataProvider;
|
||||
use skrifa::instance::{LocationRef, Size};
|
||||
let Some(face) = ttf::fallback_for('\u{0915}') else { return };
|
||||
let Some(ka) = nominal(&face, '\u{0915}') else { return };
|
||||
// "कि" after two other characters of the line: the vowel sign i is drawn before the
|
||||
// consonant (Indic reordering, which needs the script the shaper guesses from the text),
|
||||
// both glyphs carry the cluster of the consonant's index in the line, not in the run.
|
||||
let cs: Vec<char> = "ab\u{0915}\u{093F}".chars().collect();
|
||||
let gs = shape_run(&face, &cs, 2..4, false);
|
||||
let gids: Vec<u32> = gs.iter().map(|g| g.gid).collect();
|
||||
assert_eq!(gids.len(), 2, "{gids:?}");
|
||||
assert!(gids.first() != Some(&ka) && gids.last() == Some(&ka), "vowel sign before ka: {gids:?}");
|
||||
assert!(gs.iter().all(|g| g.cluster == 2), "{:?}", gs.iter().map(|g| g.cluster).collect::<Vec<_>>());
|
||||
// Advances come out at 1000 units per em, whatever the font's own em. A font that is
|
||||
// already at 1000 per em could not tell a missing scale apart, so it checks nothing here.
|
||||
let Some(font) = face.font() else { return };
|
||||
if font.metrics(Size::unscaled(), LocationRef::default()).units_per_em == 1000 {
|
||||
return;
|
||||
}
|
||||
let hmtx = font.glyph_metrics(Size::new(1000.0), LocationRef::default()).advance_width(skrifa::GlyphId::new(ka));
|
||||
let adv = gs.last().map(|g| g.advance);
|
||||
assert!(matches!((adv, hmtx), (Some(a), Some(h)) if (a - f64::from(h)).abs() < 1e-3), "{adv:?} vs {hmtx:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_rtl_lines_never_panic() {
|
||||
for s in ["\u{202E}%%u\u{05D0}%%", "\u{2067}\u{0644}\u{0627}\u{2069}%%o", "\u{064E}\u{064E}\u{05D0}", "\u{05D0}\u{E000}\u{10FFFF}"] {
|
||||
|
||||
@@ -685,6 +685,7 @@ pub fn layout_mtext_with(contents: &str, p: &MTextParams) -> MTextLayout {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Incremental character accounting preserves the text of a multi-word line without introducing wrapping.
|
||||
#[test]
|
||||
fn line_char_offsets_track_incrementally() {
|
||||
// A multi-word line lays out correctly even though the per-line character count is now
|
||||
|
||||
@@ -624,6 +624,7 @@ struct GlyphCache {
|
||||
points: usize,
|
||||
}
|
||||
|
||||
/// Return the process-wide outline cache whose retained glyph and point counts are bounded during insertion.
|
||||
fn glyph_cache() -> &'static RwLock<GlyphCache> {
|
||||
static C: OnceLock<RwLock<GlyphCache>> = OnceLock::new();
|
||||
C.get_or_init(|| RwLock::new(GlyphCache::default()))
|
||||
@@ -643,6 +644,7 @@ struct Pen {
|
||||
const MAX_OUTLINE_POINTS: usize = 65_536;
|
||||
|
||||
impl Pen {
|
||||
/// Append one flattened outline point, discarding all contours if the per-glyph point limit is exceeded.
|
||||
fn push(&mut self, point: Vec2) {
|
||||
if self.truncated {
|
||||
return;
|
||||
@@ -661,6 +663,7 @@ impl Pen {
|
||||
fn pt(x: f32, y: f32) -> Vec2 {
|
||||
Vec2::new(f64::from(x), f64::from(y))
|
||||
}
|
||||
/// Close and retain a nondegenerate contour only if its closing point also fits the outline budget.
|
||||
fn flush(&mut self) {
|
||||
if self.cur.len() > 2 {
|
||||
if let (Some(f), Some(l)) = (self.cur.first().copied(), self.cur.last().copied())
|
||||
@@ -678,13 +681,16 @@ impl Pen {
|
||||
}
|
||||
|
||||
impl OutlinePen for Pen {
|
||||
/// Finish the preceding contour and begin a new one through the budgeted point writer.
|
||||
fn move_to(&mut self, x: f32, y: f32) {
|
||||
self.flush();
|
||||
self.push(Self::pt(x, y));
|
||||
}
|
||||
/// Append a line endpoint through the budgeted point writer.
|
||||
fn line_to(&mut self, x: f32, y: f32) {
|
||||
self.push(Self::pt(x, y));
|
||||
}
|
||||
/// Flatten a quadratic segment into six samples, charging each retained point to the glyph budget.
|
||||
fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
|
||||
let a = self.cur.last().copied().unwrap_or_default();
|
||||
let c = Self::pt(cx0, cy0);
|
||||
@@ -695,6 +701,7 @@ impl OutlinePen for Pen {
|
||||
self.push(a * (u * u) + c * (2.0 * u * t) + b * (t * t));
|
||||
}
|
||||
}
|
||||
/// Flatten a cubic segment into eight samples, charging each retained point to the glyph budget.
|
||||
fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
|
||||
let a = self.cur.last().copied().unwrap_or_default();
|
||||
let c0 = Self::pt(cx0, cy0);
|
||||
@@ -742,6 +749,8 @@ pub fn glyph_contours(face: &Face, gid: u16) -> Arc<[Vec<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) {
|
||||
@@ -812,6 +821,8 @@ pub(crate) fn text_glyph_at(g: &GlyphOutline, c: char, origin: Vec2, scale: f64,
|
||||
})
|
||||
}
|
||||
|
||||
/// Charge source outline points before copying and transforming a glyph.
|
||||
/// Return `None` without spending fuel when it cannot fit; empty outlines remain valid at zero budget.
|
||||
pub(crate) fn place_glyph_budgeted(g: &GlyphOutline, x: f64, h: f64, wf: f64, shear: f64, points_left: &mut usize) -> Option<Vec<Vec<Vec2>>> {
|
||||
*points_left = points_left.checked_sub(g.points)?;
|
||||
Some(place_glyph(g, x, h, wf, shear))
|
||||
@@ -926,6 +937,7 @@ mod family_tests {
|
||||
#[cfg(test)]
|
||||
mod geometry_budget_tests {
|
||||
use super::*;
|
||||
/// Placement checks the source point cost before copying and accepts empty outlines without spending points.
|
||||
#[test]
|
||||
fn placement_checks_source_size_and_accepts_empty_glyphs() {
|
||||
let glyph = GlyphOutline { contours: vec![vec![Vec2::ZERO; 4]].into(), points: 4, cap: 1.0, ..Default::default() };
|
||||
@@ -939,6 +951,7 @@ mod geometry_budget_tests {
|
||||
assert_eq!(budget, 0);
|
||||
assert!(place_glyph_budgeted(&glyph, 0.0, 1.0, 1.0, 0.0, &mut budget).is_none());
|
||||
}
|
||||
/// Exceeding the per-glyph flattening limit discards all contour geometry before it reaches the cache.
|
||||
#[test]
|
||||
fn flattened_outlines_are_bounded_before_caching() {
|
||||
let mut pen = Pen::default();
|
||||
@@ -951,6 +964,7 @@ mod geometry_budget_tests {
|
||||
assert!(pen.contours.is_empty() && pen.cur.is_empty());
|
||||
assert_eq!(pen.points, MAX_OUTLINE_POINTS);
|
||||
}
|
||||
/// A mapped empty glyph retains text metadata referring to an existing empty outline slot.
|
||||
#[test]
|
||||
fn mapped_empty_glyph_keeps_text_and_a_valid_outline_slot() {
|
||||
// Metadata placement does not parse the face; an empty face suffices to check the index contract.
|
||||
@@ -963,6 +977,7 @@ mod geometry_budget_tests {
|
||||
assert!(shaped.glyphs[shaped.text[0].outline].is_empty());
|
||||
}
|
||||
|
||||
/// An empty glyph with no text mapping leaves the shaped result empty.
|
||||
#[test]
|
||||
fn unmapped_empty_glyph_does_not_make_the_shape_nonempty() {
|
||||
let glyph = GlyphOutline { cap: 1.0, ..Default::default() };
|
||||
@@ -973,6 +988,7 @@ mod geometry_budget_tests {
|
||||
assert!(shaped.text.is_empty());
|
||||
}
|
||||
|
||||
/// Missing text metadata does not discard a glyph that has drawable contours.
|
||||
#[test]
|
||||
fn an_unmapped_glyph_with_contours_still_draws() {
|
||||
let mut shaped = Shaped::default();
|
||||
|
||||
@@ -19,10 +19,10 @@ cadcraft-fonts = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
miniz_oxide = "0.8"
|
||||
miniz_oxide = "0.9"
|
||||
quick-xml = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
# Reads CADCraft's DWG output as written (tests only; the same version as cadcraft-dwg's).
|
||||
acadrust = "0.6.4"
|
||||
acadrust = "0.6.5"
|
||||
tiny-skia = { workspace = true }
|
||||
|
||||
@@ -156,6 +156,8 @@ pub(crate) fn resolve(secs: &[Section], d: &mut Drawing) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve image definitions under a shared copied-string budget.
|
||||
/// When strings no longer fit, keep applying pixel-size and resolution metadata.
|
||||
fn fill(store: &mut EntityStore, by_image: &HashMap<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();
|
||||
|
||||
@@ -248,6 +248,7 @@ fn entity(kind: &str, tags: &[Tag]) -> Option<(Common, EntityKind)> {
|
||||
entity_with_table_budget(kind, tags, &mut 4_000_000)
|
||||
}
|
||||
|
||||
/// Parse one entity while charging materialized table cells to the caller's shared import budget.
|
||||
fn entity_with_table_budget(kind: &str, tags: &[Tag], table_cells_left: &mut usize) -> Option<(Common, EntityKind)> {
|
||||
// The paper-space viewport our writer adds (dxf_write::paper_view) stays implicit.
|
||||
if kind == "VIEWPORT" && crate::dxf_ext::is_paper_view(tags) {
|
||||
@@ -1398,6 +1399,7 @@ fn style_font(tags: &[Tag], file: String) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Load symbol tables and resolve handle references, indexing layer names for case-insensitive updates.
|
||||
fn tables(tags: &[Tag], d: &mut Drawing, rx: &mut Rx) {
|
||||
let recs = records(tags);
|
||||
// STYLE handle → name, or the shape file of an unnamed shape style (linetype elements).
|
||||
@@ -1681,6 +1683,7 @@ pub fn read(bytes: &[u8]) -> Result<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);
|
||||
@@ -2345,9 +2348,11 @@ pub fn entity_from_groups(kind: &str, tags: &[Tag]) -> Option<(Common, EntityKin
|
||||
#[cfg(test)]
|
||||
mod table_budget_tests {
|
||||
use super::*;
|
||||
/// Build synthetic table tags with a requested row and column count for allocation-budget checks.
|
||||
fn table_tags(rows: i64, cols: i64) -> Vec<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;
|
||||
@@ -2358,6 +2363,7 @@ mod table_budget_tests {
|
||||
assert_eq!(table.cells[0].len(), 2);
|
||||
assert_eq!(left, 1);
|
||||
}
|
||||
/// Tables in blocks and repeated entity sections consume one document-wide cell allowance.
|
||||
#[test]
|
||||
fn blocks_and_entity_sections_share_one_budget() {
|
||||
let table = "0\nACAD_TABLE\n100\nAcDbTable\n91\n1\n92\n2\n171\n1\n1\ncell\n";
|
||||
|
||||
@@ -172,6 +172,7 @@ impl LeftOut {
|
||||
}
|
||||
|
||||
impl Parser {
|
||||
/// Initialize a metric SVG drawing with one document-wide generated-vertex allowance.
|
||||
fn new() -> Self {
|
||||
Parser {
|
||||
drawing: Drawing::new_metric(),
|
||||
@@ -206,15 +207,15 @@ impl Parser {
|
||||
match reader.read_event().map_err(parse_err)? {
|
||||
Event::Start(e) => {
|
||||
let name = e.name();
|
||||
self.on_start(name.as_ref(), &e)?;
|
||||
self.on_start(name.into_inner().as_bytes(), &e)?;
|
||||
}
|
||||
Event::Empty(e) => {
|
||||
let name = e.name();
|
||||
self.on_element(name.as_ref(), &e)?;
|
||||
self.on_element(name.into_inner().as_bytes(), &e)?;
|
||||
}
|
||||
Event::End(e) => {
|
||||
let name = e.name();
|
||||
self.on_end(name.as_ref());
|
||||
self.on_end(name.into_inner().as_bytes());
|
||||
}
|
||||
Event::Eof => break,
|
||||
_ => {}
|
||||
@@ -334,6 +335,7 @@ impl Parser {
|
||||
self.cur().apply_vec(v)
|
||||
}
|
||||
|
||||
/// Charge an entity against element and retained-vertex limits before adding it to model space.
|
||||
fn add(&mut self, kind: EntityKind) -> Result<()> {
|
||||
self.elements += 1;
|
||||
if self.elements > MAX_ELEMENTS {
|
||||
@@ -576,7 +578,7 @@ fn hidden(e: &BytesStart) -> bool {
|
||||
|
||||
fn attr(e: &BytesStart, name: &[u8]) -> Option<String> {
|
||||
for a in e.attributes().flatten() {
|
||||
if a.key.as_ref() == name {
|
||||
if a.key.into_inner().as_bytes() == name {
|
||||
return a.normalized_value(XmlVersion::Explicit1_0).ok().map(|v| v.into_owned());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ 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.
|
||||
@@ -24,6 +25,7 @@ fn a_modest_arc_svg_still_reads() {
|
||||
assert!(d.model.iter().count() >= 1, "expected some geometry");
|
||||
}
|
||||
|
||||
/// Repeated flattened arcs fail with a geometry-budget error before retaining excessive vertices.
|
||||
#[test]
|
||||
fn an_arc_heavy_svg_is_rejected_by_the_vertex_budget() {
|
||||
// ~1000 arcs x up to 10_000 chords each would far exceed the 8M vertex budget.
|
||||
|
||||
@@ -15,4 +15,4 @@ cadcraft-io = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
log = { workspace = true }
|
||||
# Secure randomness for the per-session control token (MIT OR Apache-2.0).
|
||||
getrandom = "0.3"
|
||||
getrandom = "0.4"
|
||||
|
||||
@@ -10,9 +10,11 @@ use serde_json::{Value, json};
|
||||
/// Something that answers control-channel methods (`engine.execute`, `cmdline.input`,
|
||||
/// `drawing.inspect`, `ui.render`…). See `cadcraft_ui_egui::control` for the full list.
|
||||
pub trait Backend {
|
||||
/// Invoke a control method and return its result or a descriptive error.
|
||||
fn call(&mut self, method: &str, params: Value) -> Result<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;
|
||||
}
|
||||
|
||||
@@ -37,6 +39,7 @@ 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()?;
|
||||
@@ -182,6 +185,8 @@ 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;
|
||||
@@ -447,6 +452,7 @@ mod tests {
|
||||
server.join().unwrap();
|
||||
}
|
||||
}
|
||||
/// Retrying a read after a lost reply reloads the replacement server session token.
|
||||
#[test]
|
||||
fn safe_retry_rereads_a_rotated_token_after_a_lost_reply() {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
@@ -473,6 +479,7 @@ mod tests {
|
||||
assert_eq!(remote.call("ping", json!({})).unwrap(), json!("pong"));
|
||||
server.join().unwrap();
|
||||
}
|
||||
/// A failed mutation write reports uncertain delivery and does not open a retry connection.
|
||||
#[test]
|
||||
fn failed_mutation_write_is_not_resent() {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
|
||||
@@ -23,6 +23,7 @@ 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()) {
|
||||
@@ -34,11 +35,13 @@ 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
|
||||
@@ -47,6 +50,7 @@ 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"))
|
||||
@@ -61,6 +65,7 @@ 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)?;
|
||||
@@ -98,6 +103,7 @@ pub fn matches(expected: &str, given: &str) -> bool {
|
||||
diff == 0
|
||||
}
|
||||
|
||||
/// Encode 256 bits of operating-system randomness as lowercase hexadecimal, propagating entropy failures.
|
||||
fn random_hex() -> io::Result<String> {
|
||||
let mut bytes = [0u8; TOKEN_BYTES];
|
||||
getrandom::fill(&mut bytes).map_err(|e| io::Error::other(format!("no secure randomness: {e}")))?;
|
||||
@@ -109,6 +115,7 @@ fn random_hex() -> io::Result<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;
|
||||
@@ -120,6 +127,7 @@ fn write_private(path: &std::path::Path, bytes: &[u8]) -> io::Result<()> {
|
||||
f.flush()
|
||||
}
|
||||
|
||||
/// Exclusively create and flush a replacement token within the per-user directory selected by the caller.
|
||||
#[cfg(not(unix))]
|
||||
fn write_private(path: &std::path::Path, bytes: &[u8]) -> io::Result<()> {
|
||||
use std::io::Write;
|
||||
@@ -134,6 +142,7 @@ fn write_private(path: &std::path::Path, bytes: &[u8]) -> io::Result<()> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// An issued token has the expected size, can be read back, and disappears after revocation.
|
||||
#[test]
|
||||
fn issue_then_read_round_trips() {
|
||||
let port = 50000 + (std::process::id() % 10000) as u16;
|
||||
@@ -144,6 +153,7 @@ mod tests {
|
||||
assert!(read(port).is_err());
|
||||
}
|
||||
|
||||
/// Token comparison rejects different bytes and lengths while accepting identical tokens.
|
||||
#[test]
|
||||
fn matches_is_exact() {
|
||||
assert!(matches("abc", "abc"));
|
||||
@@ -152,6 +162,7 @@ mod tests {
|
||||
assert!(!matches("", "x"));
|
||||
}
|
||||
|
||||
/// A token issued on Unix grants no file permission bits to group or other users.
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn token_file_is_owner_only() {
|
||||
@@ -162,6 +173,7 @@ mod tests {
|
||||
assert_eq!(mode & 0o077, 0, "token file is readable by group/other: {:o}", mode);
|
||||
revoke(port);
|
||||
}
|
||||
/// Windows token storage rejects absent or empty app-data paths and preserves a supplied path.
|
||||
#[test]
|
||||
fn windows_requires_local_app_data() {
|
||||
assert!(windows_private_dir(None).is_err());
|
||||
@@ -169,6 +181,7 @@ mod tests {
|
||||
assert_eq!(windows_private_dir(Some("private-app-data".into())).unwrap(), PathBuf::from("private-app-data"));
|
||||
}
|
||||
|
||||
/// Token issuance creates its missing directory hierarchy before writing the credential.
|
||||
#[test]
|
||||
fn issuance_creates_missing_parent() {
|
||||
let dir = std::env::temp_dir().join(format!("cadcraft-token-parent-{}", std::process::id()));
|
||||
|
||||
@@ -413,6 +413,7 @@ fn tol_height_code(st: &DimStyle) -> String {
|
||||
format!("{}x", format_decimal(f / 0.7, 4, 8, "."))
|
||||
}
|
||||
|
||||
/// Apply measurement placeholders or append a dimension suffix, bounding the resulting UTF-8 text.
|
||||
fn post(template: &str, meas: &str) -> String {
|
||||
if template.contains("<>") {
|
||||
bounded_replace(template, meas)
|
||||
@@ -424,6 +425,7 @@ fn post(template: &str, meas: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply dimension text overrides: empty uses the base, whitespace suppresses text, and other text substitutes it.
|
||||
fn apply_override(base: &str, d: &Dimension) -> String {
|
||||
if d.text.is_empty() {
|
||||
base.to_string()
|
||||
@@ -455,6 +457,8 @@ fn bounded_replace(template: &str, value: &str) -> String {
|
||||
}
|
||||
|
||||
// Returns false when truncation consumed the remaining UTF-8-safe space.
|
||||
/// Append within the remaining dimension-text byte allowance without splitting a UTF-8 character.
|
||||
/// Return false if any input text had to be truncated.
|
||||
fn append_dim_text(out: &mut String, text: &str) -> bool {
|
||||
let mut end = text.len().min(MAX_DIM_TEXT.saturating_sub(out.len()));
|
||||
while !text.is_char_boundary(end) {
|
||||
@@ -1363,12 +1367,14 @@ mod tests {
|
||||
#[cfg(test)]
|
||||
mod text_budget_tests {
|
||||
use super::*;
|
||||
/// A dimension suffix without placeholders still obeys the total text limit.
|
||||
#[test]
|
||||
fn bounds_post_without_markers() {
|
||||
let text = post(&"x".repeat(MAX_DIM_TEXT * 2), "12");
|
||||
assert_eq!(text.len(), MAX_DIM_TEXT);
|
||||
assert!(text.starts_with("12x"));
|
||||
}
|
||||
/// Oversized substituted text retains its prefix and ends at a valid UTF-8 boundary.
|
||||
#[test]
|
||||
fn oversized_value_is_substituted_and_truncated_at_a_character_boundary() {
|
||||
let text = bounded_replace("prefix <> mm", &"é".repeat(MAX_DIM_TEXT));
|
||||
|
||||
@@ -79,6 +79,8 @@ fn same_polygon(a: &[Vec2], b: &[Vec2]) -> bool {
|
||||
(0..n).any(|k| (0..n).all(|i| near(a[i], b[(i + k) % n]))) || (0..n).any(|k| (0..n).all(|i| near(a[i], b[(k + n - (i % n)) % n])))
|
||||
}
|
||||
|
||||
/// Remove degenerate and duplicate boundary loops within a shared comparison budget.
|
||||
/// Retain remaining loops without further comparisons once the budget is exhausted.
|
||||
fn dedup_polygons(loops: &mut Vec<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
|
||||
@@ -463,6 +465,7 @@ impl Family<'_> {
|
||||
self.origin + p.rotate(self.ang)
|
||||
}
|
||||
|
||||
/// Intersect a pattern family with boundary loops, respecting view, output, cancellation, and shared dash-work limits.
|
||||
fn sweep(&self, loops: &[Vec<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());
|
||||
@@ -606,6 +609,7 @@ mod tests {
|
||||
#[cfg(test)]
|
||||
mod dedup_budget_tests {
|
||||
use super::*;
|
||||
/// Loops with different vertex counts leave enough comparison fuel to remove a later duplicate.
|
||||
#[test]
|
||||
fn unequal_loops_do_not_spend_the_comparison_budget() {
|
||||
// The old estimate exhausted the budget on this unequal pair before seeing the triangle.
|
||||
|
||||
@@ -118,6 +118,7 @@ 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]);
|
||||
|
||||
@@ -57,6 +57,8 @@ 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);
|
||||
@@ -395,6 +397,7 @@ mod budget_tests {
|
||||
use super::*;
|
||||
use cadcraft_doc::TableCell;
|
||||
|
||||
/// Excessive overlapping merges draw only the table outline after coverage work is exhausted.
|
||||
#[test]
|
||||
fn overlapping_merges_use_outline_fallback() {
|
||||
let t = Table {
|
||||
@@ -409,6 +412,7 @@ mod budget_tests {
|
||||
assert!(g.fills.is_empty() && g.texts.is_empty());
|
||||
}
|
||||
|
||||
/// A split table within the displayed-cell budget clips a merge to contiguous selected rows.
|
||||
#[test]
|
||||
fn part_clips_merges_using_displayed_column_count() {
|
||||
let t = Table {
|
||||
|
||||
+12
-7
@@ -19,10 +19,11 @@ workspace_version() {
|
||||
/^\[/ { in_pkg = ($0 == "[workspace.package]") ; next }
|
||||
in_pkg && $1 == "version" { gsub(/[" ]/, "", $3); print $3; exit }
|
||||
' "$ROOT/Cargo.toml"
|
||||
return
|
||||
}
|
||||
|
||||
VERSION="${CADCRAFT_VERSION:-$(workspace_version)}"
|
||||
if [ -z "$VERSION" ]; then
|
||||
if [[ -z "$VERSION" ]]; then
|
||||
echo "error: could not read [workspace.package] version from $ROOT/Cargo.toml" >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -32,7 +33,7 @@ DIST="${DIST:-$ROOT/dist/release}"
|
||||
mkdir -p "$DIST"
|
||||
export DIST
|
||||
|
||||
if [ -z "${CADCRAFT_BUILD_SHA:-}" ]; then
|
||||
if [[ -z "${CADCRAFT_BUILD_SHA:-}" ]]; then
|
||||
CADCRAFT_BUILD_SHA="$(git -C "$ROOT" rev-parse HEAD 2>/dev/null || true)"
|
||||
fi
|
||||
export CADCRAFT_BUILD_SHA
|
||||
@@ -41,29 +42,33 @@ export CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-$ROOT/target}"
|
||||
|
||||
# Emit a GitHub Actions warning (plain stderr outside Actions).
|
||||
warn() {
|
||||
if [ -n "${GITHUB_ACTIONS:-}" ]; then echo "::warning::$*"; else echo "warning: $*" >&2; fi
|
||||
if [[ -n "${GITHUB_ACTIONS:-}" ]]; then echo "::warning::$*"; else echo "warning: $*" >&2; fi
|
||||
return
|
||||
}
|
||||
|
||||
# Copy licence and readme files that exist into a package directory.
|
||||
copy_docs() {
|
||||
local dest="$1" f
|
||||
for f in README.md LICENSE LICENSE-MIT LICENSE-APACHE COPYRIGHT; do
|
||||
if [ -f "$ROOT/$f" ]; then cp "$ROOT/$f" "$dest/"; fi
|
||||
if [[ -f "$ROOT/$f" ]]; then cp "$ROOT/$f" "$dest/"; fi
|
||||
done
|
||||
copy_font_licences "$dest"
|
||||
return
|
||||
}
|
||||
|
||||
# Builds made with craft-fonts (CRAFT_FONTS_DIR, set by the release workflow) embed its fonts, so
|
||||
# the package carries their licences as OFL-<family-dir>.txt. Nothing without CRAFT_FONTS_DIR.
|
||||
copy_font_licences() {
|
||||
local dest="$1" f
|
||||
[ -n "${CRAFT_FONTS_DIR:-}" ] || return 0
|
||||
[[ -n "${CRAFT_FONTS_DIR:-}" ]] || return 0
|
||||
for f in "$CRAFT_FONTS_DIR"/fonts/*/OFL.txt; do
|
||||
if [ -f "$f" ]; then cp "$f" "$dest/OFL-$(basename "$(dirname "$f")").txt"; fi
|
||||
if [[ -f "$f" ]]; then cp "$f" "$dest/OFL-$(basename "$(dirname "$f")").txt"; fi
|
||||
done
|
||||
}
|
||||
|
||||
# Portable SHA-256 of a file (prints just the hash).
|
||||
sha256() {
|
||||
if command -v sha256sum >/dev/null; then sha256sum "$1" | cut -d' ' -f1; else shasum -a 256 "$1" | cut -d' ' -f1; fi
|
||||
local file="$1"
|
||||
if command -v sha256sum >/dev/null; then sha256sum "$file" | cut -d' ' -f1; else shasum -a 256 "$file" | cut -d' ' -f1; fi
|
||||
return
|
||||
}
|
||||
|
||||
+4
-1
@@ -12,7 +12,10 @@ TMP="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
command -v sips >/dev/null || { echo "error: sips not found (macOS only)" >&2; exit 1; }
|
||||
|
||||
resize() { sips -s format png -z "$2" "$2" "$1" --out "$3" >/dev/null; }
|
||||
resize() {
|
||||
local src="$1" size="$2" out="$3"
|
||||
sips -s format png -z "$size" "$size" "$src" --out "$out" >/dev/null
|
||||
}
|
||||
|
||||
for s in 16 24 32 48 64 128 256 512; do
|
||||
mkdir -p "$DIR/hicolor/${s}x${s}/apps"
|
||||
|
||||
@@ -21,7 +21,7 @@ FLATHUB=https://dl.flathub.org/repo/flathub.flatpakrepo
|
||||
|
||||
TEST=1
|
||||
TARBALL=""
|
||||
while [ $# -gt 0 ]; do
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--no-test) TEST=0; shift ;;
|
||||
-h | --help) sed -n '2,14p' "$0"; exit 0 ;;
|
||||
@@ -38,7 +38,7 @@ case "$ARCH" in
|
||||
esac
|
||||
BASENAME="cadcraft-$VERSION-linux-$ARCH"
|
||||
TARBALL="${TARBALL:-$DIST/$BASENAME.tar.gz}"
|
||||
[ -f "$TARBALL" ] || { echo "error: $TARBALL not found (run packaging/linux/package.sh --formats tar first)" >&2; exit 1; }
|
||||
[[ -f "$TARBALL" ]] || { echo "error: $TARBALL not found (run packaging/linux/package.sh --formats tar first)" >&2; exit 1; }
|
||||
for tool in flatpak flatpak-builder; do
|
||||
command -v "$tool" >/dev/null || { echo "error: $tool not found (apt install flatpak flatpak-builder librsvg2-common)" >&2; exit 1; }
|
||||
done
|
||||
@@ -49,7 +49,7 @@ WORK="$CARGO_TARGET_DIR/flatpak-bundle"
|
||||
rm -rf "$WORK"
|
||||
mkdir -p "$WORK/stage"
|
||||
tar -xzf "$TARBALL" -C "$WORK/stage" --strip-components=1
|
||||
[ -x "$WORK/stage/bin/cadcraft" ] || { echo "error: $TARBALL has no bin/cadcraft" >&2; exit 1; }
|
||||
[[ -x "$WORK/stage/bin/cadcraft" ]] || { echo "error: $TARBALL has no bin/cadcraft" >&2; exit 1; }
|
||||
cp "$HERE/flatpak/$APP_ID.bundle.yml" "$WORK/$APP_ID.yml"
|
||||
|
||||
flatpak remote-add --user --if-not-exists flathub "$FLATHUB"
|
||||
@@ -62,7 +62,7 @@ OUT="$DIST/$BASENAME.flatpak"
|
||||
flatpak build-bundle --runtime-repo="$FLATHUB" "$WORK/repo" "$OUT" "$APP_ID" stable
|
||||
echo "wrote $OUT"
|
||||
|
||||
if [ "$TEST" = 1 ]; then
|
||||
if [[ "$TEST" = 1 ]]; then
|
||||
flatpak install --user -y --noninteractive --reinstall "$OUT"
|
||||
flatpak info --user "$APP_ID"
|
||||
flatpak run --command=cadcraft-cli "$APP_ID" --version
|
||||
|
||||
@@ -21,7 +21,7 @@ APP_ID=ai.storyteller.cadcraft
|
||||
|
||||
SKIP_BUILD=0
|
||||
FORMATS="appimage deb rpm tar"
|
||||
while [ $# -gt 0 ]; do
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--skip-build) SKIP_BUILD=1; shift ;;
|
||||
--formats) FORMATS="$2"; shift 2 ;;
|
||||
@@ -44,8 +44,8 @@ BASENAME="cadcraft-$VERSION-linux-$ARCH"
|
||||
|
||||
echo "==> CADCraft $VERSION for Linux $ARCH ($FORMATS)"
|
||||
|
||||
if [ "$SKIP_BUILD" = 0 ]; then
|
||||
if [ -n "${CROSS_TARGET:-}" ]; then
|
||||
if [[ "$SKIP_BUILD" = 0 ]]; then
|
||||
if [[ -n "${CROSS_TARGET:-}" ]]; then
|
||||
(cd "$ROOT" && cargo build --release --locked -p cadcraft -p cadcraft-cli --target "$CROSS_TARGET")
|
||||
else
|
||||
(cd "$ROOT" && cargo build --release --locked -p cadcraft -p cadcraft-cli)
|
||||
@@ -77,7 +77,7 @@ if command -v appstreamcli >/dev/null; then
|
||||
appstreamcli validate --no-net --explain "$STAGE/usr/share/metainfo/$APP_ID.metainfo.xml"
|
||||
fi
|
||||
|
||||
has() { case " $FORMATS " in *" $1 "*) return 0 ;; *) return 1 ;; esac; }
|
||||
has() { local format="$1"; case " $FORMATS " in *" $format "*) return 0 ;; *) return 1 ;; esac; }
|
||||
|
||||
# ---- .tar.gz ------------------------------------------------------------------------------------
|
||||
if has tar; then
|
||||
@@ -110,9 +110,9 @@ if has appimage; then
|
||||
ln -s "$APP_ID.png" "$APPDIR/.DirIcon"
|
||||
|
||||
TOOL="${APPIMAGETOOL:-$(command -v appimagetool || true)}"
|
||||
if [ -z "$TOOL" ]; then
|
||||
if [[ -z "$TOOL" ]]; then
|
||||
TOOL="$CARGO_TARGET_DIR/appimagetool-$ARCH.AppImage"
|
||||
if [ ! -x "$TOOL" ]; then
|
||||
if [[ ! -x "$TOOL" ]]; then
|
||||
curl --proto '=https' -fsSL -o "$TOOL" "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-$ARCH.AppImage"
|
||||
chmod +x "$TOOL"
|
||||
fi
|
||||
@@ -135,16 +135,16 @@ if has appimage; then
|
||||
# package) appimagetool also writes the .zsync, into its working directory, hence the cd.
|
||||
(cd "$DIST" && ARCH="$ARCH" APPIMAGE_EXTRACT_AND_RUN=1 "$TOOL" --no-appstream -u "$UPDATE_INFO" "$APPDIR" "$OUT")
|
||||
echo "wrote $OUT"
|
||||
if [ -s "$OUT.zsync" ]; then
|
||||
if [[ -s "$OUT.zsync" ]]; then
|
||||
echo "wrote $OUT.zsync"
|
||||
else
|
||||
warn "zsyncmake not found, so $OUT.zsync was not written; AppImage delta updates need it"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "${CROSS_TARGET:-}" ]; then
|
||||
if [[ -z "${CROSS_TARGET:-}" ]]; then
|
||||
"$STAGE/usr/bin/cadcraft-cli" --version
|
||||
elif [ -n "${EMULATOR:-}" ]; then
|
||||
elif [[ -n "${EMULATOR:-}" ]]; then
|
||||
"$EMULATOR" "$STAGE/usr/bin/cadcraft-cli" --version
|
||||
fi
|
||||
echo "==> done"
|
||||
|
||||
@@ -12,7 +12,7 @@ umask 077
|
||||
# shellcheck source=../env.sh
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/../env.sh"
|
||||
|
||||
if [ -z "${APPLE_CERTIFICATE:-}" ]; then
|
||||
if [[ -z "${APPLE_CERTIFICATE:-}" ]]; then
|
||||
warn "APPLE_CERTIFICATE is not set: macOS builds will be ad-hoc signed and not notarized"
|
||||
exit 0
|
||||
fi
|
||||
@@ -41,13 +41,13 @@ security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KC_PASS"
|
||||
security list-keychains -d user -s "$KEYCHAIN" $(security list-keychains -d user | tr -d '"')
|
||||
|
||||
IDENTITY="$(security find-identity -v -p codesigning "$KEYCHAIN" | awk '/Developer ID Application/ { print $2; exit }')"
|
||||
if [ -z "$IDENTITY" ]; then
|
||||
if [[ -z "$IDENTITY" ]]; then
|
||||
security find-identity -v -p codesigning "$KEYCHAIN" >&2
|
||||
echo "error: no 'Developer ID Application' identity in APPLE_CERTIFICATE" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Imported signing identity $IDENTITY"
|
||||
if [ -n "${GITHUB_ENV:-}" ]; then
|
||||
if [[ -n "${GITHUB_ENV:-}" ]]; then
|
||||
{ echo "MACOS_SIGN_IDENTITY=$IDENTITY"; echo "MACOS_KEYCHAIN=$KEYCHAIN"; } >>"$GITHUB_ENV"
|
||||
else
|
||||
echo "MACOS_SIGN_IDENTITY=$IDENTITY"
|
||||
|
||||
+11
-11
@@ -19,7 +19,7 @@ HERE="$ROOT/packaging/macos"
|
||||
|
||||
ARCH=universal
|
||||
SKIP_BUILD=0
|
||||
while [ $# -gt 0 ]; do
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--arch) ARCH="$2"; shift 2 ;;
|
||||
--skip-build) SKIP_BUILD=1; shift ;;
|
||||
@@ -44,9 +44,9 @@ DMG="$DIST/cadcraft-$VERSION-macos-$ARCH.dmg"
|
||||
CLI_ZIP="$DIST/cadcraft-cli-$VERSION-macos-$ARCH.zip"
|
||||
|
||||
NOTARIZE=0
|
||||
if [ "$IDENTITY" = "-" ]; then
|
||||
if [[ "$IDENTITY" = "-" ]]; then
|
||||
warn "macOS: ad-hoc signing (no MACOS_SIGN_IDENTITY); artifacts are not notarized"
|
||||
elif [ -n "${APPLE_ID:-}" ] && [ -n "${APPLE_PASSWORD:-}" ] && [ -n "${APPLE_TEAM_ID:-}" ]; then
|
||||
elif [[ -n "${APPLE_ID:-}" ]] && [[ -n "${APPLE_PASSWORD:-}" ]] && [[ -n "${APPLE_TEAM_ID:-}" ]]; then
|
||||
NOTARIZE=1
|
||||
else
|
||||
warn "macOS: APPLE_ID / APPLE_PASSWORD / APPLE_TEAM_ID incomplete; signed but not notarized"
|
||||
@@ -55,7 +55,7 @@ fi
|
||||
echo "==> CADCraft $VERSION for macOS ($ARCH), identity: $IDENTITY, notarize: $NOTARIZE"
|
||||
|
||||
# ---- build -------------------------------------------------------------------------------------
|
||||
if [ "$SKIP_BUILD" = 0 ]; then
|
||||
if [[ "$SKIP_BUILD" = 0 ]]; then
|
||||
args=()
|
||||
for t in "${TARGETS[@]}"; do args+=(--target "$t"); done
|
||||
(cd "$ROOT" && cargo build --release --locked -p cadcraft -p cadcraft-cli "${args[@]}")
|
||||
@@ -74,9 +74,9 @@ done
|
||||
sign() {
|
||||
# Hardened runtime + secure timestamp for a real identity; ad-hoc can't be timestamped.
|
||||
local ts=(--timestamp)
|
||||
[ "$IDENTITY" = "-" ] && ts=(--timestamp=none)
|
||||
[[ "$IDENTITY" = "-" ]] && ts=(--timestamp=none)
|
||||
local kc=()
|
||||
[ -n "${MACOS_KEYCHAIN:-}" ] && kc=(--keychain "$MACOS_KEYCHAIN")
|
||||
[[ -n "${MACOS_KEYCHAIN:-}" ]] && kc=(--keychain "$MACOS_KEYCHAIN")
|
||||
codesign --force --sign "$IDENTITY" ${kc[@]+"${kc[@]}"} "${ts[@]}" "$@"
|
||||
}
|
||||
|
||||
@@ -88,8 +88,8 @@ notarize() {
|
||||
echo "$out"
|
||||
id="$(printf '%s' "$out" | plutil -extract id raw -o - - 2>/dev/null || true)"
|
||||
status="$(printf '%s' "$out" | plutil -extract status raw -o - - 2>/dev/null || true)"
|
||||
if [ "$status" != "Accepted" ]; then
|
||||
if [ -n "$id" ]; then
|
||||
if [[ "$status" != "Accepted" ]]; then
|
||||
if [[ -n "$id" ]]; then
|
||||
xcrun notarytool log "$id" --apple-id "$APPLE_ID" --password "$APPLE_PASSWORD" --team-id "$APPLE_TEAM_ID" || true
|
||||
fi
|
||||
echo "error: notarization of $(basename "$file") failed (status: ${status:-unknown})" >&2
|
||||
@@ -117,7 +117,7 @@ sign --options runtime --entitlements "$HERE/entitlements.plist" "$APP/Contents/
|
||||
sign --options runtime --entitlements "$HERE/entitlements.plist" "$APP"
|
||||
codesign --verify --strict --deep --verbose=2 "$APP"
|
||||
|
||||
if [ "$NOTARIZE" = 1 ]; then
|
||||
if [[ "$NOTARIZE" = 1 ]]; then
|
||||
ditto -c -k --keepParent "$APP" "$WORK/CADCraft-notarize.zip"
|
||||
notarize "$WORK/CADCraft-notarize.zip"
|
||||
xcrun stapler staple "$APP"
|
||||
@@ -144,7 +144,7 @@ hdiutil convert "$WORK/raw.dmg" -format UDZO -imagekey zlib-level=9 -o "$DMG"
|
||||
rm -f "$WORK/raw.dmg"
|
||||
sign "$DMG"
|
||||
codesign --verify --strict --verbose=2 "$DMG"
|
||||
if [ "$NOTARIZE" = 1 ]; then
|
||||
if [[ "$NOTARIZE" = 1 ]]; then
|
||||
notarize "$DMG"
|
||||
xcrun stapler staple "$DMG"
|
||||
xcrun stapler validate "$DMG"
|
||||
@@ -162,7 +162,7 @@ codesign --verify --strict --verbose=2 "$CLI_DIR/cadcraft-cli"
|
||||
rm -f "$CLI_ZIP"
|
||||
ditto -c -k --keepParent "$CLI_DIR" "$CLI_ZIP"
|
||||
# A bare Mach-O can't carry a stapled ticket; Gatekeeper looks the notarization up online.
|
||||
if [ "$NOTARIZE" = 1 ]; then notarize "$CLI_ZIP"; fi
|
||||
if [[ "$NOTARIZE" = 1 ]]; then notarize "$CLI_ZIP"; fi
|
||||
|
||||
"$WORK/bin/cadcraft-cli" --version
|
||||
echo "==> done"
|
||||
|
||||
@@ -11,13 +11,13 @@ set -euo pipefail
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/../env.sh"
|
||||
HERE="$ROOT/packaging/web"
|
||||
|
||||
if [ "${1:-}" != "--skip-build" ]; then
|
||||
if [[ "${1:-}" != "--skip-build" ]]; then
|
||||
command -v trunk >/dev/null || { echo "error: trunk not found (cargo install trunk --locked)" >&2; exit 1; }
|
||||
(cd "$ROOT/apps/cadcraft-web" && trunk build --release)
|
||||
fi
|
||||
|
||||
SITE="$ROOT/dist/web"
|
||||
[ -f "$SITE/index.html" ] || { echo "error: $SITE/index.html missing; run without --skip-build" >&2; exit 1; }
|
||||
[[ -f "$SITE/index.html" ]] || { echo "error: $SITE/index.html missing; run without --skip-build" >&2; exit 1; }
|
||||
# Paths must be relative so the site works under any prefix (public_url = "./" in Trunk.toml).
|
||||
if grep -Eq '(src|href)="/[^/]' "$SITE/index.html"; then
|
||||
echo "error: $SITE/index.html has root-absolute URLs; it would break when served from a sub-path" >&2
|
||||
|
||||
@@ -14,11 +14,14 @@ struct Counts {
|
||||
/// never make the stats command run unbounded work.
|
||||
const MAX_ENTRIES: usize = 2_000_000;
|
||||
|
||||
/// Accumulate Rust source counts under one directory with a fresh traversal-entry budget.
|
||||
fn walk(dir: &Path, c: &mut Counts) -> Result<(), String> {
|
||||
let mut budget = MAX_ENTRIES;
|
||||
walk_inner(dir, c, &mut budget)
|
||||
}
|
||||
|
||||
/// Traverse entries without following symlinks, sharing one budget across descendant directories.
|
||||
/// Skip unreadable entries and excluded directories; report budget exhaustion to the caller.
|
||||
fn walk_inner(dir: &Path, c: &mut Counts, budget: &mut usize) -> Result<(), String> {
|
||||
let Ok(rd) = std::fs::read_dir(dir) else {
|
||||
return Ok(());
|
||||
@@ -84,6 +87,7 @@ fn exact_tests(pkg: &str) -> Option<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();
|
||||
@@ -138,6 +142,7 @@ mod tests {
|
||||
assert_eq!(count_tests(src), (2, 1));
|
||||
}
|
||||
|
||||
/// A directory alias does not cause its Rust source to be counted twice.
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn walk_does_not_follow_directory_symlinks() {
|
||||
@@ -154,6 +159,7 @@ mod tests {
|
||||
assert_eq!(c.files, 1, "the aliased directory was followed and counted twice");
|
||||
std::fs::remove_dir_all(&base).ok();
|
||||
}
|
||||
/// Traversal reports an exhausted entry budget and accepts a tree that exactly fits it.
|
||||
#[test]
|
||||
fn walk_reports_budget_exhaustion() {
|
||||
let base = std::env::temp_dir().join(format!("cadcraft-stats-budget-{}", std::process::id()));
|
||||
|
||||
Reference in New Issue
Block a user