mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
M14: verify and bound PngSuite corpus downloads (#1729)
This commit is contained in:
+4
-1
@@ -6,7 +6,7 @@
|
||||
|
||||
**Stage: alpha** · next: beta, ~13 points (ready for real work ~62% → ~75%, and reliable `.ai` exchange) and ~170–270 h away
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** minor (synthetic control-input budgets, #1709; merges of 2026-10-14: anchor Align and Distribute, #1636; Direct Selection fixes, #1674, #1666, #1743; Escape while typing, #1686; Pen clicks and a Precise Cursors command, #1663; Expand on repeats, #1756; Roughen and Scribble, #1643, #1648; file and input hardening, #1776–#1783, #1786, #1807; MCP limits and envelope, #1727, #1728; Image Trace levelling and Flat Logo on scans, #1647, #1657; a guide's position in the Transform panel is a convenience, not a parity claim, #1659; headline numbers unchanged; later merges of 2026-10-13: SVG import of CSS transforms, percentage lengths in symbols, Inkscape flowed text and pages, #1565, #1596, #1568, #1601; Warp Arc, #1578; Pathfinder Trim, Merge and Crop strokes, #1570; saves to folders that refuse the temporary file, #1620 in part; OpenType stylistic sets, #1607; intersect Smart Guides, #1602 in part; headline numbers unchanged; control transport deadlines, #1726; Image Trace edges, strokes, memory and three new options, measured in `docs/image-trace-quality.md`, headline numbers unchanged; community PRs and fixes of 2026-10-13: Polish interface, #1498; compact native saves, #1492; PDF and EPS paste as vector art, #1499; `.ai`, PDF, EPS and SVG import fixes, #1528, #1530, #1531, #1544, #1551, #1553; guide objects, #1534; Join after Average, #1533; Keyboard Shortcuts lists commands outside the menus, #1539; headline numbers unchanged; community PRs and fixes of late 2026-10-12: type that does not fit its frame is reported, #1471; Tabs panel drags, #1458; template layers in Outline view, #1486; dialog slider rails, #1483; Direct Selection anchor precedence, #1508; print-size images, #1517; French catalog complete again, #1511; headline numbers unchanged; Image Trace colour and photo presets, measured in `docs/image-trace-quality.md`, headline numbers unchanged; community PRs and fixes of 2026-10-12: Split Into Grid options, #1462; Transform Objects and Reflect across an angle, #1448 and #1460; Pathfinder on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; many-object edits, #1429 in part; headline numbers unchanged; Image Trace places edges to a fraction of a pixel, measured in `docs/image-trace-quality.md`, headline numbers unchanged; Transform dialogs preview and the transform tools act on direct-selected anchors, #1412 and #1408; `.ai` compound path paint, #1410, overprint, #1388, and text-only PDF parts, #1387; community PRs: strokes wider than their shape, corner drags without the bounding box, pasted pictures, compound paths in `document.inspect`, MCP guide; headline numbers unchanged; community PRs: `.ai` editing-data follow-ups (gradient stops, type of objects with an appearance, short area-type frames), PDF bleed with editing data, dialog keyboard focus, art flush with an artboard, font rescan, symbol instance redraw, `--control` port check; headline numbers unchanged; Rectangular Grid and Polar Grid tool options, #1377 in part; `.ai` files with placed (linked) pictures open from their editing data, #1264; community PRs: `.ai` and PDF bleed and editing-data import, gradient shapes imported as paths, ruler origin, Align To, Live Corners corner selection; headline numbers unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; community PRs: graph designs, Graph Data worksheet and Graph Type options; Align both centres; Pen endpoint fixes; earlier: Effect › Brush Strokes landed: Photoshop-style filters 28 of 57, raster effects 40% → 48%; `.ai` gradients, meshes and artboard names from the editing data; headline numbers unchanged; `.ai` art styles and clipping fills from the editing data; headline numbers unchanged; `editingData: "always"`; headline numbers unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** minor (verified PngSuite downloads, #1729; synthetic control-input budgets, #1709; merges of 2026-10-14: anchor Align and Distribute, #1636; Direct Selection fixes, #1674, #1666, #1743; Escape while typing, #1686; Pen clicks and a Precise Cursors command, #1663; Expand on repeats, #1756; Roughen and Scribble, #1643, #1648; file and input hardening, #1776–#1783, #1786, #1807; MCP limits and envelope, #1727, #1728; Image Trace levelling and Flat Logo on scans, #1647, #1657; a guide's position in the Transform panel is a convenience, not a parity claim, #1659; headline numbers unchanged; later merges of 2026-10-13: SVG import of CSS transforms, percentage lengths in symbols, Inkscape flowed text and pages, #1565, #1596, #1568, #1601; Warp Arc, #1578; Pathfinder Trim, Merge and Crop strokes, #1570; saves to folders that refuse the temporary file, #1620 in part; OpenType stylistic sets, #1607; intersect Smart Guides, #1602 in part; headline numbers unchanged; control transport deadlines, #1726; Image Trace edges, strokes, memory and three new options, measured in `docs/image-trace-quality.md`, headline numbers unchanged; community PRs and fixes of 2026-10-13: Polish interface, #1498; compact native saves, #1492; PDF and EPS paste as vector art, #1499; `.ai`, PDF, EPS and SVG import fixes, #1528, #1530, #1531, #1544, #1551, #1553; guide objects, #1534; Join after Average, #1533; Keyboard Shortcuts lists commands outside the menus, #1539; headline numbers unchanged; community PRs and fixes of late 2026-10-12: type that does not fit its frame is reported, #1471; Tabs panel drags, #1458; template layers in Outline view, #1486; dialog slider rails, #1483; Direct Selection anchor precedence, #1508; print-size images, #1517; French catalog complete again, #1511; headline numbers unchanged; Image Trace colour and photo presets, measured in `docs/image-trace-quality.md`, headline numbers unchanged; community PRs and fixes of 2026-10-12: Split Into Grid options, #1462; Transform Objects and Reflect across an angle, #1448 and #1460; Pathfinder on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; many-object edits, #1429 in part; headline numbers unchanged; Image Trace places edges to a fraction of a pixel, measured in `docs/image-trace-quality.md`, headline numbers unchanged; Transform dialogs preview and the transform tools act on direct-selected anchors, #1412 and #1408; `.ai` compound path paint, #1410, overprint, #1388, and text-only PDF parts, #1387; community PRs: strokes wider than their shape, corner drags without the bounding box, pasted pictures, compound paths in `document.inspect`, MCP guide; headline numbers unchanged; community PRs: `.ai` editing-data follow-ups (gradient stops, type of objects with an appearance, short area-type frames), PDF bleed with editing data, dialog keyboard focus, art flush with an artboard, font rescan, symbol instance redraw, `--control` port check; headline numbers unchanged; Rectangular Grid and Polar Grid tool options, #1377 in part; `.ai` files with placed (linked) pictures open from their editing data, #1264; community PRs: `.ai` and PDF bleed and editing-data import, gradient shapes imported as paths, ruler origin, Align To, Live Corners corner selection; headline numbers unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; community PRs: graph designs, Graph Data worksheet and Graph Type options; Align both centres; Pen endpoint fixes; earlier: Effect › Brush Strokes landed: Photoshop-style filters 28 of 57, raster effects 40% → 48%; `.ai` gradients, meshes and artboard names from the editing data; headline numbers unchanged; `.ai` art styles and clipping fills from the editing data; headline numbers unchanged; `editingData: "always"`; headline numbers unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
VectorCraft is a clean-room, open-source, pure-Rust reimplementation of the Adobe Illustrator workflow. It runs
|
||||
on macOS, Windows, Linux, FreeBSD and the web (WASM), and agents can drive all of it over MCP, the CLI and a JSON
|
||||
@@ -125,6 +125,8 @@ Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gap
|
||||
|
||||
## Progress log
|
||||
|
||||
- 2026-10-13: PngSuite downloads use pinned HTTPS files and a SHA-256 manifest; no archive extraction (#1729).
|
||||
|
||||
| Date | What landed |
|
||||
|---|---|
|
||||
| 2026-10-13 | Synthetic control input has finite click/drag counts and pending-event limits, checked before queue mutation; control-channel bursts yield after 64 requests per frame (#1709). |
|
||||
@@ -163,6 +165,7 @@ The full pre-standard record of what landed is kept in [docs/roadmap.md](docs/ro
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-13 | minor | Pinned PngSuite HTTPS files verified by SHA-256 before publication; bounded downloads replace archive extraction (#1729). Scores and hours unchanged. |
|
||||
| 2026-10-13 | minor | Bounded synthetic control input (#1709): counts and queued events validated before mutation; control-channel bursts yield between frames. Scores and hours unchanged. |
|
||||
| 2026-10-14 | minor | Progress log row for the merges of 2026-10-14 (#1641–#1808 as listed there) and Image Trace's #1647 and #1657; the 2026-10-13 row now says that a guide's position in the Transform panel (#1659) is not established by the documentation found and is kept as a convenience pending the owner's decision; headline numbers unchanged. |
|
||||
| 2026-10-13 | minor | Control transport deadlines (#1726): idle and partial clients and blocked replies release connection slots; parity scores and estimates unchanged. |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Architecture
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (synthetic control-input budgets, #1709; desktop printing uses craft-print; Windows 7 fallback retained) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (verified PngSuite downloads, #1729; synthetic control-input budgets, #1709; desktop printing uses craft-print; Windows 7 fallback retained) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
How VectorCraft is built today. The design rationale lives in the local, gitignored `plan/architecture.md`;
|
||||
this file describes what is committed. Day-to-day rules (clean-room, never crash, quality gates) are in
|
||||
@@ -74,10 +74,15 @@ No panics in shipped code (workspace clippy lints ban `unwrap`, `expect`, `panic
|
||||
`vectorcraft_engine::guard` catches a panic at every entry point and rolls the document back; Data Recovery
|
||||
saves copies in the background. See [development.md](development.md#robustness-vectorcraft-never-crashes).
|
||||
|
||||
## Corpus fetching
|
||||
|
||||
`cargo xtask corpus --download` retrieves only allowlisted regular PngSuite files over HTTPS at a pinned mirror commit. SHA-256 and exact size checks precede publication; the downloader never extracts an archive. Existing verified files are reused; failed downloads leave existing corpus files intact. See [development.md](development.md#verified-pngsuite-corpus).
|
||||
|
||||
## Revision history
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-13 | minor | Pinned PngSuite HTTPS files verified by SHA-256 before publication; bounded downloads replace archive extraction (#1729). Scores and hours unchanged. |
|
||||
| 2026-10-13 | minor | Bounded synthetic control input (#1709): counts and queued events validated before mutation; control-channel bursts yield between frames. Scores and hours unchanged. |
|
||||
| 2026-10-12 | minor | Desktop PDF spooler shared through craft-print; exact physical sizes, composed copies and Windows 7 fallback retained. Scores and remaining gaps unchanged. |
|
||||
| 2026-10-10 | minor | Added the `three-d` crate |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Development
|
||||
|
||||
> **Last updated:** 2026-10-13 · **Change:** verified PngSuite downloads (#1729).
|
||||
|
||||
## Web build
|
||||
|
||||
`apps/vectorcraft-web` runs the same `VectorcraftApp` in the browser through eframe's web runner. The renderer is wgpu: WebGPU where the browser has it, WebGL2 otherwise. It is Rust only; the only JavaScript is the glue wasm-bindgen generates.
|
||||
@@ -313,3 +315,31 @@ Vertical type keeps its logical order down the column. SVG export outlines text
|
||||
lines (with a warning) until it is written with `direction`/`unicode-bidi`; PDF and raster exports
|
||||
use the laid-out glyphs. No font is bundled for these scripts: the installed fonts' fallback
|
||||
covers them (the web build needs a font with that coverage).
|
||||
|
||||
## Verified PngSuite corpus
|
||||
|
||||
`cargo xtask corpus --download` fetches the 175 original PngSuite PNGs and their redistribution
|
||||
license/README from [the pinned lunapaint mirror](https://github.com/lunapaint/pngsuite/tree/8cd768dd0d0063195174d0d01cacbd5a7d1e5605).
|
||||
Its README identifies `png/` as the original 2017-07-19 files; converted BMP/JSON files are excluded.
|
||||
Each file's SHA-256 and exact byte length are committed in `xtask/pngsuite.sha256`.
|
||||
|
||||
Only HTTPS downloads and redirects are allowed. The manifest admits at most 256 flat filenames,
|
||||
1 MiB per file and 16 MiB total; current PNG data totals 114,649 bytes. Curl limits each download
|
||||
to its expected length, with a 10-second connect timeout and 60-second transfer timeout.
|
||||
Network fetching requires curl 8.4 or newer: earlier versions do not bound unknown-length
|
||||
responses ([curl documentation](https://curl.se/docs/manpage.html#--max-filesize)). Verified
|
||||
caches remain usable without invoking curl.
|
||||
Reads are independently bounded. Files are staged under temporary filenames, checked, then
|
||||
renamed into place; failed or substituted downloads never replace an existing oracle. The fetch
|
||||
stops on unexpected directories/symlinks or an existing staging file rather than overwriting them.
|
||||
Verified cached files need no network access. The corpus remains gitignored and optional.
|
||||
|
||||
The original site's plain HTTP archive is no longer fetched or extracted. To refresh the suite,
|
||||
review the mirror commit, original-file list, redistribution license, sizes and hashes together;
|
||||
do not replace expected hashes automatically in response to a download mismatch.
|
||||
|
||||
## Revision history
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-13 | minor | Document pinned PngSuite files, integrity checks and download budgets (#1729). |
|
||||
|
||||
File diff suppressed because one or more lines are too long
+4
-1
File diff suppressed because one or more lines are too long
+3
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,180 @@
|
||||
# PngSuite original PNGs and redistribution notice
|
||||
# Source: https://github.com/lunapaint/pngsuite/tree/8cd768dd0d0063195174d0d01cacbd5a7d1e5605
|
||||
# sha256 bytes filename
|
||||
71e1f5bf865131b5d9996e76a78372146283e5089e50f900ae927a6458b62101 167 LICENSE
|
||||
a2853f2be68e460dcb9992819125b74503a01383e79f79a04b971ffa9d248544 716 PngSuite.README
|
||||
9bf1c4610280efecc291361e2b7b026d602d03a7137f476ff84af626bb7b3a62 217 basi0g01.png
|
||||
50ed4ff9c1c80578ae58ead6fb28ea2e44016b5b56a7b3e95c235ec86f97c0d8 154 basi0g02.png
|
||||
0cce4212157dae7c0f71f5f72cef39cecab3347de455da0ace7de83eebd47849 247 basi0g04.png
|
||||
b9300750e7414c9fed68cc7f4f531f5e2cdd2724f50142e8aa3a0070836bece9 254 basi0g08.png
|
||||
d354026bdd0aa6ed7bedea7c92e5eef2ebbd4854848b8a0a48f562cc71bfe80f 299 basi0g16.png
|
||||
b2690d4475cdc39faf5a7d2de20de4e14eb96c6360fa791ec2003b4085ecacde 315 basi2c08.png
|
||||
ae13eba5fabbf7cbe8c01ec4458cdaa7e6bfdfcbd5109e831efaac9f39096027 595 basi2c16.png
|
||||
8e1edfbc0382e0b545c70209271cedead2d530df1ce40f5296f0749226b36006 132 basi3p01.png
|
||||
85f304eaaf6831b2bcc2c08892b947fef52f06337aaf23312d6326e367d73f77 193 basi3p02.png
|
||||
78890ee2c4da71e460b5a1527efe7dc17d673ebe997f49ca1b4dcd7347d21e56 327 basi3p04.png
|
||||
ca103eef7a7c50abc7af9d5ec0dc9fdc07668e2f030adbce226e050dcec9eea3 1527 basi3p08.png
|
||||
dfae01a4548e30519157bb51ce29d4de3b35b74288e9568b4d5911de01c6048b 214 basi4a08.png
|
||||
ef7df23ccd912309a4f89ca3c3094bf5ab55add25218e571e51cc6cb11cfad9e 2855 basi4a16.png
|
||||
fe70ea20c8447e82abb7bc9dd05344a61a4631cc69e0974378df8f6263af0337 361 basi6a08.png
|
||||
7fdd6bf08f04692bcf06b5c4262e7f76c49d8adb992789fd3f05aa0435645cd8 4180 basi6a16.png
|
||||
c8b1364d7771dd2f5a1b2d7d633abcf3f48dafee608558ecd2e5fc98f61894cd 164 basn0g01.png
|
||||
ca172e92d179c77b3d321ad501e452a59bdaafafde8ebd74fd0669c4eae58efc 104 basn0g02.png
|
||||
fb2d42c4c8f5c51590e675994a9b0c718fbf71c80e28142f231cfa7c5b0e26e8 145 basn0g04.png
|
||||
268d061075d1dd2eeec62b31303d09f6998549e1bfb447a5f09c80a2b0978ac3 138 basn0g08.png
|
||||
1a1fe155f40c11d79350b3f84c7eb2b29719037e57073cb7ae2ab9e8323f9360 167 basn0g16.png
|
||||
c90e86090a625661b19960cafdde6e347d6e32d73837aaae533f66dd3f099506 145 basn2c08.png
|
||||
1e1c28ec0ee5da6224404b4005076dd30e2335a1a80c0ca722046a245c4a2b3b 302 basn2c16.png
|
||||
c2d7cd682df5f74506b33a5d70c344aaee248fda79fdfef8e873426fd6f2b75b 112 basn3p01.png
|
||||
0466bb7ed9984cf03b70704564bcffab1df8ec0e8167473ba0f75e4fedce5a8f 146 basn3p02.png
|
||||
e1fc7be978d3149b98533d0076245ae64353b7967290f4204c1282ecb4ec1aba 216 basn3p04.png
|
||||
d58256cd2eb16b5740d4c1403d25ce43d8dd03e270627ab709d2fb141e3d904c 1286 basn3p08.png
|
||||
7be24b618bfd437b921ae0caf9341d112338c6290cc62466deb19d2c7a512968 126 basn4a08.png
|
||||
1c92ffd11d2fc89a36f170d0239668436407f5c8e3f8d93483fb3fc6bca361d7 2206 basn4a16.png
|
||||
559c594166eb156f461c9beff0f053196730dc998fdb0d2b801c89e6680860a5 184 basn6a08.png
|
||||
8f9d81060aebf4576461403c5057de7f23f73157016b659402b906df805845aa 3435 basn6a16.png
|
||||
dfae01a4548e30519157bb51ce29d4de3b35b74288e9568b4d5911de01c6048b 214 bgai4a08.png
|
||||
ef7df23ccd912309a4f89ca3c3094bf5ab55add25218e571e51cc6cb11cfad9e 2855 bgai4a16.png
|
||||
559c594166eb156f461c9beff0f053196730dc998fdb0d2b801c89e6680860a5 184 bgan6a08.png
|
||||
8f9d81060aebf4576461403c5057de7f23f73157016b659402b906df805845aa 3435 bgan6a16.png
|
||||
71d041df5c8949ce07d5e93b3c9f752cefc75ef7326c49df05ae73915f1a755f 140 bgbn4a08.png
|
||||
c45de44d3442bd33072d68456541b10e0cc57c5d8bfc809208995d939091eb2d 2220 bggn4a16.png
|
||||
2da3c3efa01bf166958852a104ece92daaede8ffb5e0452a55d40526258a8bf5 202 bgwn6a08.png
|
||||
875bc2b44a4355551c51260def9664da68be1af8c14b82f76173aaf01ddb1929 3453 bgyn6a16.png
|
||||
c88909e74e039dd3df829bf24144b487171e53b17f5c5c07cbc08688247d24b5 1514 ccwn2c08.png
|
||||
dc365b39d49c287669d837872dd59aef30763a611dfc8c046c481e35e49a390a 1554 ccwn3p08.png
|
||||
438018f19c85e582cb586ac7cca2220008ecb7fc70ce50e8f2a76b494c128a20 404 cdfn2c08.png
|
||||
a068eaf1f7040490e08eda3259befb6689849dd0ff8bb4cc03c705d117cb2b9f 344 cdhn2c08.png
|
||||
ee08dfa303a3c09171280fc1821180dc0a4c3591af7e9077e8ce0435642c76d2 232 cdsn2c08.png
|
||||
a849d89ba3d92146940d7c56d2fe74c9c3ec96725c7f24eefaf44160a6304de2 724 cdun2c08.png
|
||||
587a8624228d3f605f475bd751a1f3a97f0bbab590c0ff518bce5da3abcb6f2b 258 ch1n3p04.png
|
||||
e3cace549047c6e5e693b9b135d53f7f9ca5b1088bf30cc408634598ba53758a 1810 ch2n3p08.png
|
||||
79ccaf4abe5091e1f65f8a61496363ed3312ebb9ea1aaa98affadac3d98507de 292 cm0n0g04.png
|
||||
127bb50d224ac3ebff36160482a03b9e6686072e86c8ff4cd43e3ce2271b2315 292 cm7n0g04.png
|
||||
774b384d0ad73698e7d93fc5a29976b332c619045e5966276c609b11459a4152 292 cm9n0g04.png
|
||||
bc812de2b83abc9ae2a6d1ae01e91f9090214b9ed8fee1d56cdfea58f4f259d0 214 cs3n2c16.png
|
||||
267104dced0858dd828cdd41edc0e87042c7094c77191565192a67045064b23f 259 cs3n3p08.png
|
||||
028fa49fbba3954c2eba27a4c3607196e87c1dae74192df3b3ad7afe5e0a3f0c 186 cs5n2c08.png
|
||||
a7a4021983d6b22685529cbafb65cd087c9e1af1c5d205c2e13e3efc4ae87b6a 271 cs5n3p08.png
|
||||
d90ddab7313e4e1fd7f20f16a6c546b449697fbcac1a7776298a74896f4662da 149 cs8n2c08.png
|
||||
5bffdb88e307a851f8e85b137a69c59e557a881db816e990061934c0155c894c 256 cs8n3p08.png
|
||||
081d1ec26b4157fbc032b76dc716321420f2d032a425de046557c7842766826d 273 ct0n0g04.png
|
||||
259116f8ecf849d83d824688eb02b4575aed11980f02ab6fb123281c25a6459d 792 ct1n0g04.png
|
||||
4583e63d1bdfa18b6abb47439dee9a73bed311ed1b0a71d22bed75fe4767832b 742 cten0g04.png
|
||||
d0607280c3539a8934c5cfd22788b382cdc31321cdedc75ae8a29e3948580ba6 716 ctfn0g04.png
|
||||
5f4c87f5a8f589d029a908050880ac5d6aed3810e55e75444802d6d3fb27cf33 1182 ctgn0g04.png
|
||||
8ef799e59871755578e563c5b411bc3895c2f7112c115aaa921b6c04fe255bf2 1269 cthn0g04.png
|
||||
ca91332ecc04e5faac1f39ca4df4996b072a93f3e574e7f4b50d52aafbfe67b6 941 ctjn0g04.png
|
||||
c0765e635a6423ec64f0314400c3df20745c4a4469ffcc323c7b00ba66b4aceb 753 ctzn0g04.png
|
||||
d04140d74bc60597c47b5aac371a3553d3add9354437bbd8c57007d94e197809 1788 exif2c08.png
|
||||
d055cc0bb505b37c6ecf88a808d71675eca5dd0ee683d70ebc9c0be0e1bb8e57 319 f00n0g08.png
|
||||
0fe92b2aa2da04c885d1dbd85c834716f6cdd946364d97dcd597bb79d9e14427 2475 f00n2c08.png
|
||||
956ddaf133e6d9a8d36f1e00604d87fbc2d0295e933bb8caaef517e7d87e5342 321 f01n0g08.png
|
||||
ef072ec6815ebf9b33e0553d2e4e4e7ed6911860a2512c67bcd10a9f0f09b9de 1180 f01n2c08.png
|
||||
1f5b49c06ecc2a1756f0423b3de936bd18497866d035ac8eded78a6f17408a2c 355 f02n0g08.png
|
||||
ca4b937b3c587d5c007f193a2eec14dc96b0d23ff7d6aa9004e3badd1af9fe8f 1729 f02n2c08.png
|
||||
d0563ae30c6ce7a01eae7a5cdc0ef780175bfa7653888ed93f17bef5fe4b2107 389 f03n0g08.png
|
||||
2d101e3ef4f78a69437034671e93fe11faac0cfc4d44210dcca1b944caa886f7 1291 f03n2c08.png
|
||||
fec1a3c53833d0ddefa4486a98934c393cb4335b2dc31da70e6540a4686f1457 269 f04n0g08.png
|
||||
c365c24153cb69fd3c162f00b296ae23a71a1595645d1aeb0ad23af680d7b4be 985 f04n2c08.png
|
||||
8ac0f095d2a943157e820fa121bccde08d5230af1b5830c3041d5f4da3524eba 426 f99n0g04.png
|
||||
3494b914dd1b094afa9a74a89bfa75219030e21af6fca3ed66c9d45032a047e9 345 g03n0g16.png
|
||||
abce774b9624952c2d0c57ff681790ba620cf5616fe7e0ef0600600cdce3c22a 370 g03n2c08.png
|
||||
541389db0c72721c6c49501e1dbf6e32df41b4fcfa318676955e278f3f4332b3 214 g03n3p04.png
|
||||
72350a2d9db2df09b626f1f1dd48061e9c749d471bf19814d2c5c10acbf007e5 363 g04n0g16.png
|
||||
bfd3edfc4d85a43383556a51023533fa3888184aa53a6b43769910782beebfce 377 g04n2c08.png
|
||||
5ea85ebefb58b2ab113fc05bf1cb79ffb068359e3a5620843590a0a7b59bd49a 219 g04n3p04.png
|
||||
be19721d28e0b268af1e29e8190c952b94315bd08899383ae2c91e6dc8c4f889 339 g05n0g16.png
|
||||
74ab5b8477992d65e220eeaff8df466522c1b392fa90c6f9935da5bdb7113c9d 350 g05n2c08.png
|
||||
2f46afd7ec15836b523cbce6965f34d89833b8d777afffaf0f53bce620d5f65e 206 g05n3p04.png
|
||||
9070f843981f765cc8e26a63751be5d58631d85a8a7b4e826358f80e8a679d03 321 g07n0g16.png
|
||||
ca4cd32d222f65fb7beb7c9f40d8b6833b36d552b830ff1a1d67dae73b8ed0d8 340 g07n2c08.png
|
||||
b8e7abeabcfd50b71cca78b6cdff7372310cfc179cd63304c3e904f469b333cb 207 g07n3p04.png
|
||||
a22486acb74ee5f947db3a20b1f314a9dd8aca3339840049ee559f4d676a6b5c 262 g10n0g16.png
|
||||
4ac23729aea109e8f6b1e831448115cf02dcd97559e9403847cbe6dd7e5c7347 285 g10n2c08.png
|
||||
36cc2ea4b5b33cd18e0a01c7de85dbcaf7161d258cd0f2265a90cd835b15cf6d 214 g10n3p04.png
|
||||
1fae707d809296d2cf0d6aa653bead6199644fb1c859558ec7d2d69e709a423a 383 g25n0g16.png
|
||||
9b128cfa1bb417dd99251914d729062621fcea9f346168549b16a75fe030b0f6 405 g25n2c08.png
|
||||
4197a0a25c4f74d42c2285d75b9fabe1eedbed45bc224ff1ec1784f5797e9dbf 215 g25n3p04.png
|
||||
1a1fe155f40c11d79350b3f84c7eb2b29719037e57073cb7ae2ab9e8323f9360 167 oi1n0g16.png
|
||||
1e1c28ec0ee5da6224404b4005076dd30e2335a1a80c0ca722046a245c4a2b3b 302 oi1n2c16.png
|
||||
142bc4b1e9f8a9e3b0ba3d2c2025e485bf7e76df88f914bf8c9041b4562cc080 179 oi2n0g16.png
|
||||
2c523f138e0ade1bed8f31d59ec0f34b82660b192d6af8b8f395f82b9ba9dabb 314 oi2n2c16.png
|
||||
aa2d77723ba44f654a41a2ab97e01271fe942fecd2f299c0a81da90acb2887ac 203 oi4n0g16.png
|
||||
661220a0ed03ca6d63ec8dcaac738d4a9fd089b491c40dd896267ffb6cf5015d 338 oi4n2c16.png
|
||||
ae70c9effd0d9957a0d42771bc987529a1f3715e068057fae63f0c7e50d5c53a 1283 oi9n0g16.png
|
||||
fef98e9bebe6c4046c29f2568f7a735305c8661aa01f9c37a718135e5bc4f941 3038 oi9n2c16.png
|
||||
72406b6173ec20dd059e8cea594ef5bc8f81bac5e416e2b53058d2debd51d195 962 pp0n2c16.png
|
||||
d5a6f964a601a5e1815525117c12c32b61e2aa09fd6040776d35f2f7115b7110 818 pp0n6a08.png
|
||||
b321b9ac8f2aa641475b6936365954181726dcb8662dbf641284d82a83cc22a5 1456 ps1n0g08.png
|
||||
6c59d673032fe2d04dbd2ea91cc9bc7bebe7d5ab84911f00e36fb28994793576 1620 ps1n2c16.png
|
||||
bd2ae1b45fd771aaa8c57dccd183f3834fc9742f6dfc692606865049efb116e5 2320 ps2n0g08.png
|
||||
120c62ce9d014f7772c4b38a446991b5bbe6a22ef5ea59239d32e023bc167585 2484 ps2n2c16.png
|
||||
235eb82f1337e3fcefbdca566a53af3d3523db7588ba3cd7fa5dab19afe48a41 113 s01i3p01.png
|
||||
6a4f6da9fe36d195219ad2beeac1a0423e2ee6de03dfb064a3b6fa31623eb230 113 s01n3p01.png
|
||||
e7d26ab4b31e7066b0674a4374c093967a9f1761b96836aa3dbe624acd73ab52 114 s02i3p01.png
|
||||
f4b324f2b1d013ee77f632e42129c7cd3eca55da846e775e744a1f224ad1efcd 115 s02n3p01.png
|
||||
4513dec80f54ffd04a28834b114254d6e6d66e52e32e0059174474f2fb15d6bc 118 s03i3p01.png
|
||||
4dee297bd7f747f87488509af11a1e1e0531fa67e6e8d0f8ab7f4df7940f0ff3 120 s03n3p01.png
|
||||
918f94f3557e10de91efa680df507c8eafc347fa5023af43037701398128ad0f 126 s04i3p01.png
|
||||
e73c52f764b684ec5133cce11f7f18cc60065acdd308e2c3d871e0a792a1080e 121 s04n3p01.png
|
||||
77fe040e17167f9656afe46cf0031832e635745e4e7f19ba12dee32f5ec03d96 134 s05i3p02.png
|
||||
1b9e244a5c1d6cc9830391fff4eac565ada026d000e60faf7525c8add6bdc726 129 s05n3p02.png
|
||||
cf8d5fdb34c0afd38b1b4745bed6167771767ccdfe847c4d8766f7fcc5c41176 143 s06i3p02.png
|
||||
5caabfc4b147fad5e49e558143faaab91888324240acf45e5db8fc49f7a7f667 131 s06n3p02.png
|
||||
d9874bfad10353a9ea2876ee388c3e7f81f697ed141d0d79c5c2019d62a7a698 149 s07i3p02.png
|
||||
e7d447394f5fa64c27353216af387352a77a1fb534922a2d974e60d965d4e414 138 s07n3p02.png
|
||||
62adf855c6b1e3a007f9dcf3ce2ba244616c089269a511dd939e89a4b0853453 149 s08i3p02.png
|
||||
544fdd476e566e8aef4c98604124ae05acef48be20d1aebb9b6674b08b2abbb6 139 s08n3p02.png
|
||||
f2cf5b3109690704d106a0b22ea079e36064cc6077e1fb9a1ba87397d0bbcca1 147 s09i3p02.png
|
||||
6be8f58b638e7f7114750f5796f66f650db8fb7242a36183850f02117f4eaa65 143 s09n3p02.png
|
||||
a27e0dbf923ef868e7399560b76c23a50b2d8926cddd9775e2bc4e050273f205 355 s32i3p04.png
|
||||
c80d9fc7ded0b7834ec11d0e324540c7be74016b61bc302a1d167218fa35fbf9 263 s32n3p04.png
|
||||
cc6ad98dcf686d92f1165d951f90fac70e568cedb1fcbba33e8890d0e6cdad4d 385 s33i3p04.png
|
||||
998d08e084e3eea288984475bfed0d48ee2d8250e20b96755898b59152e36a34 329 s33n3p04.png
|
||||
83eb2e82eb14ae3c608833ef371a6f472c4c6e63b7e9f4f92f5472f975793b97 349 s34i3p04.png
|
||||
a25451168c0b03ab8f52dcc60c6b9c12bc5df5d55afabe1892b87d0a856bb4a0 248 s34n3p04.png
|
||||
db5988c27ad577de1e494cb50229dc2772678531adde2db84493907eae003e54 399 s35i3p04.png
|
||||
cd99fc4cc69bda1ff8e8697d8b423ae8d9e84a58ba1d2b480751953b6869e3b3 338 s35n3p04.png
|
||||
a36b40927b730a31d60c7818fb4a32e67ace06393cdaa26edc09520b6cee066d 356 s36i3p04.png
|
||||
ebc08f6a1e0918c5d3a9a08e184c23019a3cf90d8843e4b71bf2ae16040b1a84 258 s36n3p04.png
|
||||
565c79404c4b9e46c27edb2f93d7b26b93ec0a0481ab7a21fc405152a21f53ba 393 s37i3p04.png
|
||||
964078b1f3f472268b8ac97a8976b4d1cdb6a47e6243d67eddcb0205e65b9f61 336 s37n3p04.png
|
||||
b107f120ffaffcb949ac4547e0f91555ac681f78c7e68c724e7ceddfd8e5387b 357 s38i3p04.png
|
||||
045b0f5ea35874741bf6cac40f2e63ec8726450ab241475df4b7e9303c39e339 245 s38n3p04.png
|
||||
4b5a6d8c04c8c70e1de3bcb6f7b5cc3ba359b13e28de6f52e21a291d4bc8a937 420 s39i3p04.png
|
||||
2115b6cad0140ea97fe6ec056b47307ef1e8b5c00db5f08707a0572b165e5f08 352 s39n3p04.png
|
||||
48a3f19b5c3b2f7eca17fd34765a2a3e385d770f24c357bc66bd74417eb60a5a 357 s40i3p04.png
|
||||
c4598ea73b1ec015bef7dcfd7f67bc7bfc41546619953d1a92cd9b8657a7033a 256 s40n3p04.png
|
||||
c5e7eeaffa677bfac002ee968a0423e0fea3be7701d39ccd6a015df1d2d85e40 429 tbbn0g04.png
|
||||
8fcfd031a5b716201854fdab25691feed6cd3ed7b713a51b94e1f43fc63fe0c1 2041 tbbn2c16.png
|
||||
8eb43a6020fda8ed7c8c9b88b974ef794ee8452db29212ae7e5c11003bfd76a3 1499 tbbn3p08.png
|
||||
49711ee271f96099521b83b75d58ed2f76d42ed46cb3c06656fde2cad697cf59 2041 tbgn2c16.png
|
||||
6f798628935a021fe446dce3d62861d1f7369aaf01bad10daa4917c5abccad7d 1499 tbgn3p08.png
|
||||
8223d8b0c5f84b149f0c1f0e1f658346e07fcbd03ffc02b2e9eeea1481eb19bf 1633 tbrn2c08.png
|
||||
01e86f108b244bdee4d72e384aced8a62369965ca2993ee6ef88f131c647f7d1 1313 tbwn0g16.png
|
||||
978b28cec43bd219517083b63661bb30a7b9cdf9fd4476ab4e70844565980d51 1496 tbwn3p08.png
|
||||
7b4eb64b4bd7eb36417752873ddaca027ce77a07f474b2af569f4eea1e984111 1499 tbyn3p08.png
|
||||
f05e317f64a038123faf96fdbd6fc3ab3eec14e1fc1eb87fd7da4d996be63526 116 tm3n3p02.png
|
||||
a253e055d8cd94049cd95e9392e7aa60f3d5c922a8f0cccb84762b0c5f547545 719 tp0n0g08.png
|
||||
5dced3fde56b950e61ce86f4ad54458336f66389ce8ca68524d4fd8ec546ff5b 1594 tp0n2c08.png
|
||||
da40ddb18bd45db32ed03dc89e2250b096d808efe975a88477e4f5a91a4677c9 1476 tp0n3p08.png
|
||||
50bc927fe6cf1816454a7c27f8c743734be8db45e37e5ee3ede540f4e895e6c5 1483 tp1n3p08.png
|
||||
4059f7e6a1c5bac1801f70e09f9ec1e1297dcdce34055c13ab2703d6d9613c7e 138 xc1n0g08.png
|
||||
e252a0e7df3e794e52ce4a831edafef76e7043d0d8d84019db0f7fd0b30e20f4 145 xc9n2c08.png
|
||||
3c0c2a68dd416a7be79400e0e2ceb358ba3191d5309d4bad9dd04b32ff6d5b60 145 xcrn0g04.png
|
||||
71e4b2826f61556eda39f3a93c8769b14d3ac90f135177b9373061199dbef39a 164 xcsn0g01.png
|
||||
c1287690808e809dc5d4fb89d8a7fd69ed93521f290abd42021ca00a061a1ba4 145 xd0n2c08.png
|
||||
00b53c3bbd0641454521b982bc6f6bcfda7c91f1874cefb3a9bac37d80a1a269 145 xd3n2c08.png
|
||||
16e5b40fb2600db1af20fb79ff715c2869255e2d4bef20702f16534e5dd6a847 145 xd9n2c08.png
|
||||
f9d1fb2a708703518368c392c74765a6e3e5b49dbb9717df3974452291032df9 61 xdtn0g01.png
|
||||
318864720c8fc0dbe4884035f2183cc3bd3d92ec60d447879982942600e9fe2e 138 xhdn0g08.png
|
||||
968fd21abb8acf40fbca90676de3abcb3a6e35b01ba7a3b9190184eafb99c83d 145 xlfn0g04.png
|
||||
776701227c7094dd10b78f508430b8ea4f03471d072096382cbcca628dea1d2b 164 xs1n0g01.png
|
||||
9dd7e93ba9211f0caee09d8f12e37742936c469f45fcad21e577c9f0640cf99e 164 xs2n0g01.png
|
||||
166c8633d116d1c26631cb98f3a75340cf4385fa7ae6a67913bc78e709bcb30d 164 xs4n0g01.png
|
||||
7a380568beeac969908196ac89cd2347c96ac2f26fdc2b8e0314b62c59f0e308 164 xs7n0g01.png
|
||||
e4767d3259e1b331c9ece9fb07fc30f385879ac12b3cd0e1cb923f3bfac81dcf 3172 z00n2c08.png
|
||||
939abfc6e0a3e349bb5bcd8c4e427a6cdb2c7e0c23a773aefa1c4fc9f07e4cb4 232 z03n2c08.png
|
||||
21652fe42f472acd83dcb207806add5737db0020b21d6b38b0a00be3e9e37990 224 z06n2c08.png
|
||||
98d90a7e61aa1a9ac9d99af44dd25c70cd5a7b4a11a85da60f82923e18a78f9f 224 z09n2c08.png
|
||||
+4
-17
@@ -9,6 +9,7 @@ mod brands;
|
||||
mod bundle;
|
||||
mod ico;
|
||||
mod layers;
|
||||
mod pngsuite;
|
||||
mod sha256;
|
||||
mod stats;
|
||||
mod version;
|
||||
@@ -224,16 +225,14 @@ fn cmd_ci() -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const PNGSUITE_URL: &str = "http://www.schaik.com/pngsuite/PngSuite-2017jul19.tgz";
|
||||
|
||||
fn cmd_corpus(download: bool) -> Result<(), String> {
|
||||
let corpus = root().join("corpus");
|
||||
println!(
|
||||
"Test corpora live under {} (git-ignored, never committed).
|
||||
Tests that use a corpus skip cleanly when it is absent.
|
||||
|
||||
corpus/pngsuite/ PngSuite (public domain) — vectorcraft-codecs compares every file
|
||||
against the `image` crate. Fetch: cargo xtask corpus --download
|
||||
corpus/pngsuite/ Original PngSuite PNGs (redistribution permitted), pinned and SHA-256
|
||||
verified. Fetch: cargo xtask corpus --download
|
||||
corpus/psd/ PSD samples from MIT/BSD projects (ag-psd, psd-tools test data).
|
||||
Copy files in manually; licences must be MIT/BSD/CC0.
|
||||
corpus/tiff/ libtiff pics (optional)
|
||||
@@ -247,17 +246,5 @@ Tests that use a corpus skip cleanly when it is absent.
|
||||
if !download {
|
||||
return Ok(());
|
||||
}
|
||||
let dest = corpus.join("pngsuite");
|
||||
std::fs::create_dir_all(&dest).map_err(|e| format!("create {}: {e}", dest.display()))?;
|
||||
let tgz = corpus.join("PngSuite-2017jul19.tgz");
|
||||
let mut curl = Command::new("curl");
|
||||
curl.args(["-fsSL", "-o"]).arg(&tgz).arg(PNGSUITE_URL);
|
||||
run(curl, &format!("curl {PNGSUITE_URL}"))?;
|
||||
let mut tar = Command::new("tar");
|
||||
tar.arg("-xzf").arg(&tgz).arg("-C").arg(&dest);
|
||||
run(tar, "tar -xzf PngSuite-2017jul19.tgz")?;
|
||||
let _ = std::fs::remove_file(&tgz);
|
||||
let n = std::fs::read_dir(&dest).map(|d| d.flatten().filter(|e| e.path().extension().is_some_and(|x| x == "png")).count()).unwrap_or(0);
|
||||
println!("PngSuite: {n} PNG files in {}", dest.display());
|
||||
if n == 0 { Err("no PNG files extracted".into()) } else { Ok(()) }
|
||||
pngsuite::fetch()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
//! Pinned PngSuite files: verify bounded HTTPS downloads before publishing, without extracting archives.
|
||||
use std::collections::HashSet;
|
||||
use std::fs::{self, File, OpenOptions};
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
use crate::{root, run, sha256};
|
||||
|
||||
const SOURCE: &str = "https://raw.githubusercontent.com/lunapaint/pngsuite/8cd768dd0d0063195174d0d01cacbd5a7d1e5605";
|
||||
const MANIFEST: &str = include_str!("../pngsuite.sha256");
|
||||
const MAX_FILE: u64 = 1024 * 1024;
|
||||
const MAX_TOTAL: u64 = 16 * MAX_FILE;
|
||||
const MAX_FILES: usize = 256;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Entry<'a> {
|
||||
sha: &'a str,
|
||||
bytes: u64,
|
||||
name: &'a str,
|
||||
}
|
||||
|
||||
fn entries(manifest: &str) -> Result<Vec<Entry<'_>>, String> {
|
||||
let mut result = Vec::new();
|
||||
let mut names = HashSet::new();
|
||||
let mut total = 0u64;
|
||||
for line in manifest.lines().filter(|l| !l.trim().is_empty() && !l.starts_with('#')) {
|
||||
let fields: Vec<_> = line.split_whitespace().collect();
|
||||
let [sha, size, name] = fields.as_slice() else { return Err("invalid PngSuite manifest row".into()) };
|
||||
let png = name.strip_suffix(".png").is_some_and(|s| !s.is_empty() && s.bytes().all(|b| b.is_ascii_lowercase() || b.is_ascii_digit()));
|
||||
if !png && !matches!(*name, "LICENSE" | "PngSuite.README") {
|
||||
return Err(format!("unexpected PngSuite filename: {name}"));
|
||||
}
|
||||
if sha.len() != 64 || !sha.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)) {
|
||||
return Err(format!("invalid PngSuite sha256: {name}"));
|
||||
}
|
||||
let bytes = size.parse::<u64>().map_err(|_| format!("invalid PngSuite size: {name}"))?;
|
||||
if bytes == 0 || bytes > MAX_FILE || bytes > MAX_TOTAL.saturating_sub(total) || result.len() >= MAX_FILES || !names.insert(*name) {
|
||||
return Err(format!("PngSuite manifest budget or duplicate filename: {name}"));
|
||||
}
|
||||
total += bytes;
|
||||
result.push(Entry { sha, bytes, name });
|
||||
}
|
||||
if !result.iter().any(|e| e.name.ends_with(".png")) {
|
||||
return Err("PngSuite manifest has no PNG files".into());
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn verified(path: &Path, entry: &Entry<'_>) -> Result<bool, String> {
|
||||
let metadata = match fs::symlink_metadata(path) {
|
||||
Ok(m) => m,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(false),
|
||||
Err(e) => return Err(format!("{}: {e}", path.display())),
|
||||
};
|
||||
if !metadata.file_type().is_file() {
|
||||
return Err(format!("{}: expected a regular corpus file", path.display()));
|
||||
}
|
||||
if metadata.len() != entry.bytes {
|
||||
return Ok(false);
|
||||
}
|
||||
let mut data = Vec::new();
|
||||
File::open(path).and_then(|f| f.take(entry.bytes + 1).read_to_end(&mut data)).map_err(|e| format!("{}: {e}", path.display()))?;
|
||||
Ok(data.len() as u64 == entry.bytes && sha256::hex(&data) == entry.sha)
|
||||
}
|
||||
|
||||
struct Download(PathBuf);
|
||||
impl Drop for Download {
|
||||
fn drop(&mut self) {
|
||||
// Best-effort removal of our disposable download on success, rejection or network failure.
|
||||
let _ = fs::remove_file(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_into(dest: &Path, manifest: &str, mut download: impl FnMut(&str, &Path, u64) -> Result<(), String>) -> Result<usize, String> {
|
||||
let entries = entries(manifest)?;
|
||||
fs::create_dir_all(dest).map_err(|e| format!("{}: {e}", dest.display()))?;
|
||||
if !fs::symlink_metadata(dest).map_err(|e| e.to_string())?.file_type().is_dir() {
|
||||
return Err("PngSuite destination must be a regular directory".into());
|
||||
}
|
||||
let mut count = 0;
|
||||
for entry in entries {
|
||||
let out = dest.join(entry.name);
|
||||
if !verified(&out, &entry)? {
|
||||
let temporary = dest.join(format!(".{}-{}.download", entry.name, std::process::id()));
|
||||
// Reserve a new regular file, never reuse somebody else's stale download or link.
|
||||
OpenOptions::new().write(true).create_new(true).open(&temporary).map_err(|e| format!("{}: {e}", temporary.display()))?;
|
||||
let temporary = Download(temporary);
|
||||
let folder = if entry.name.ends_with(".png") { "/png" } else { "" };
|
||||
download(&format!("{SOURCE}{folder}/{}", entry.name), &temporary.0, entry.bytes)?;
|
||||
if !verified(&temporary.0, &entry)? {
|
||||
return Err(format!("{}: size or sha256 does not match xtask/pngsuite.sha256", entry.name));
|
||||
}
|
||||
fs::rename(&temporary.0, &out).map_err(|e| format!("publish {}: {e}", out.display()))?;
|
||||
}
|
||||
count += usize::from(entry.name.ends_with(".png"));
|
||||
}
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
fn supported_curl(version: &str) -> bool {
|
||||
let Some(version) = version.strip_prefix("curl ").and_then(|s| s.split_whitespace().next()) else { return false };
|
||||
let mut parts = version.split('.');
|
||||
let major = parts.next().and_then(|s| s.parse::<u64>().ok());
|
||||
let minor = parts.next().and_then(|s| s.parse::<u64>().ok());
|
||||
matches!((major, minor), (Some(major), Some(minor)) if (major, minor) >= (8, 4))
|
||||
}
|
||||
|
||||
pub fn fetch() -> Result<(), String> {
|
||||
let dest = root().join("corpus/pngsuite");
|
||||
let mut checked_curl = false;
|
||||
let count = fetch_into(&dest, MANIFEST, |url, path, bytes| {
|
||||
if !checked_curl {
|
||||
// Older curl only bounds responses whose length is known before downloading.
|
||||
let version = Command::new("curl").arg("--version").output().map_err(|e| format!("curl --version: {e}"))?;
|
||||
if !version.status.success() || !supported_curl(&String::from_utf8_lossy(&version.stdout)) {
|
||||
return Err("PngSuite downloading requires curl 8.4 or newer to bound unknown-length responses".into());
|
||||
}
|
||||
checked_curl = true;
|
||||
}
|
||||
let mut curl = Command::new("curl");
|
||||
curl.args([
|
||||
"-fsSL",
|
||||
"--proto",
|
||||
"=https",
|
||||
"--proto-redir",
|
||||
"=https",
|
||||
"--connect-timeout",
|
||||
"10",
|
||||
"--max-time",
|
||||
"60",
|
||||
"--retry",
|
||||
"2",
|
||||
"--max-filesize",
|
||||
])
|
||||
.arg(bytes.to_string())
|
||||
.arg("-o")
|
||||
.arg(path)
|
||||
.arg(url);
|
||||
run(curl, &format!("curl {url}"))
|
||||
})?;
|
||||
println!("PngSuite: {count} PNG files verified in {}", dest.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
static NEXT: AtomicUsize = AtomicUsize::new(0);
|
||||
struct Temp(PathBuf);
|
||||
impl Temp {
|
||||
fn new() -> Self {
|
||||
let path = std::env::temp_dir().join(format!("vectorcraft-pngsuite-{}-{}", std::process::id(), NEXT.fetch_add(1, Ordering::Relaxed)));
|
||||
fs::create_dir(&path).unwrap();
|
||||
Self(path)
|
||||
}
|
||||
}
|
||||
impl Drop for Temp {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
fn manifest(data: &[u8]) -> String {
|
||||
format!("{} {} test.png\n", sha256::hex(data), data.len())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn substituted_and_oversized_downloads_never_replace_corpus_files() {
|
||||
let temp = Temp::new();
|
||||
let out = temp.0.join("test.png");
|
||||
let good = b"original PNG oracle";
|
||||
for replacement in [b"substitutePNGoracle".as_slice(), &[0; 100]] {
|
||||
fs::write(&out, b"old corpus data").unwrap();
|
||||
let error = fetch_into(&temp.0, &manifest(good), |_, path, limit| {
|
||||
assert_eq!(limit, good.len() as u64);
|
||||
fs::write(path, replacement).unwrap();
|
||||
Ok(())
|
||||
})
|
||||
.unwrap_err();
|
||||
assert!(error.contains("size or sha256"));
|
||||
assert_eq!(fs::read(&out).unwrap(), b"old corpus data");
|
||||
assert_eq!(fs::read_dir(&temp.0).unwrap().count(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verified_cache_is_reused_and_corrupt_cache_is_repaired() {
|
||||
let temp = Temp::new();
|
||||
let data = b"verified oracle";
|
||||
let m = manifest(data);
|
||||
let mut requests = 0;
|
||||
for _ in 0..2 {
|
||||
assert_eq!(
|
||||
fetch_into(&temp.0, &m, |url, path, _| {
|
||||
requests += 1;
|
||||
assert!(url.starts_with(SOURCE) && url.ends_with("/png/test.png"));
|
||||
fs::write(path, data).unwrap();
|
||||
Ok(())
|
||||
})
|
||||
.unwrap(),
|
||||
1
|
||||
);
|
||||
}
|
||||
assert_eq!(requests, 1);
|
||||
fs::write(temp.0.join("test.png"), b"wrong oracle!!!").unwrap();
|
||||
fetch_into(&temp.0, &m, |_, path, _| {
|
||||
fs::write(path, data).unwrap();
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(fs::read(temp.0.join("test.png")).unwrap(), data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn network_failures_clean_staging_and_nonregular_targets_are_rejected() {
|
||||
let temp = Temp::new();
|
||||
let m = manifest(b"png");
|
||||
assert!(
|
||||
fetch_into(&temp.0, &m, |_, path, _| {
|
||||
fs::write(path, b"partial").unwrap();
|
||||
Err("network failed".into())
|
||||
})
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(fs::read_dir(&temp.0).unwrap().count(), 0);
|
||||
fs::create_dir(temp.0.join("test.png")).unwrap();
|
||||
let e = fetch_into(&temp.0, &m, |_, _, _| panic!("must not download over a nonregular file")).unwrap_err();
|
||||
assert!(e.contains("regular corpus file"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_download_is_never_overwritten() {
|
||||
let temp = Temp::new();
|
||||
let stale = temp.0.join(format!(".test.png-{}.download", std::process::id()));
|
||||
fs::write(&stale, b"keep this earlier download").unwrap();
|
||||
assert!(fetch_into(&temp.0, &manifest(b"png"), |_, _, _| panic!("must not reuse an existing staging file")).is_err());
|
||||
assert_eq!(fs::read(stale).unwrap(), b"keep this earlier download");
|
||||
assert!(!temp.0.join("test.png").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_rejects_unexpected_names_duplicates_and_resource_budgets() {
|
||||
let sha = sha256::hex(b"png");
|
||||
for name in ["../test.png", "a/b.png", "a\\b.png", "a:stream.png", "script.exe", "a..png"] {
|
||||
assert!(entries(&format!("{sha} 3 {name}\n")).is_err());
|
||||
}
|
||||
for m in [format!("{sha} 0 a.png"), format!("{sha} {} a.png", MAX_FILE + 1), format!("{sha} 3 a.png\n{sha} 3 a.png"), "bad 3 a.png".into()] {
|
||||
assert!(entries(&m).is_err());
|
||||
}
|
||||
let m: String = (0..17).map(|i| format!("{sha} {MAX_FILE} a{i}.png\n")).collect();
|
||||
assert!(entries(&m).is_err());
|
||||
let m: String = (0..257).map(|i| format!("{sha} 1 a{i}.png\n")).collect();
|
||||
assert!(entries(&m).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pinned_manifest_contains_the_original_175_pngs_and_redistribution_notice() {
|
||||
let e = entries(MANIFEST).unwrap();
|
||||
assert_eq!(e.iter().filter(|e| e.name.ends_with(".png")).count(), 175);
|
||||
assert!(e.iter().any(|e| e.name == "LICENSE"));
|
||||
assert!(e.iter().any(|e| e.name == "PngSuite.README"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn old_curl_without_unknown_length_download_limits_is_rejected() {
|
||||
for version in ["curl 7.88.1", "curl 8.3.0", "curl invalid", "not curl"] {
|
||||
assert!(!supported_curl(version));
|
||||
}
|
||||
for version in ["curl 8.4.0 libcurl/8.4.0", "curl 8.21.0 (Windows)", "curl 9.0.0"] {
|
||||
assert!(supported_curl(version));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user