M14: verify and bound PngSuite corpus downloads (#1729)
This commit is contained in:
parent
fdf6ba9c9d
commit
dfb0ce36bd
|
|
@ -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
File diff suppressed because one or more lines are too long
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
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue