mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-02 18:13:39 +00:00
Merge current main fixes and validate craft-print rollout
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
71e1f5bf865131b5d9996e76a78372146283e5089e50f900ae927a6458b62101 PngSuite.LICENSE
|
||||
a2853f2be68e460dcb9992819125b74503a01383e79f79a04b971ffa9d248544 PngSuite.README
|
||||
6cf3bcd1757bfad2a7ce9c9659d4f609297a0828cafc7c9eddee18c5576ba9e9 PngSuite.png
|
||||
9bf1c4610280efecc291361e2b7b026d602d03a7137f476ff84af626bb7b3a62 basi0g01.png
|
||||
50ed4ff9c1c80578ae58ead6fb28ea2e44016b5b56a7b3e95c235ec86f97c0d8 basi0g02.png
|
||||
0cce4212157dae7c0f71f5f72cef39cecab3347de455da0ace7de83eebd47849 basi0g04.png
|
||||
b9300750e7414c9fed68cc7f4f531f5e2cdd2724f50142e8aa3a0070836bece9 basi0g08.png
|
||||
d354026bdd0aa6ed7bedea7c92e5eef2ebbd4854848b8a0a48f562cc71bfe80f basi0g16.png
|
||||
b2690d4475cdc39faf5a7d2de20de4e14eb96c6360fa791ec2003b4085ecacde basi2c08.png
|
||||
ae13eba5fabbf7cbe8c01ec4458cdaa7e6bfdfcbd5109e831efaac9f39096027 basi2c16.png
|
||||
8e1edfbc0382e0b545c70209271cedead2d530df1ce40f5296f0749226b36006 basi3p01.png
|
||||
85f304eaaf6831b2bcc2c08892b947fef52f06337aaf23312d6326e367d73f77 basi3p02.png
|
||||
78890ee2c4da71e460b5a1527efe7dc17d673ebe997f49ca1b4dcd7347d21e56 basi3p04.png
|
||||
ca103eef7a7c50abc7af9d5ec0dc9fdc07668e2f030adbce226e050dcec9eea3 basi3p08.png
|
||||
dfae01a4548e30519157bb51ce29d4de3b35b74288e9568b4d5911de01c6048b basi4a08.png
|
||||
ef7df23ccd912309a4f89ca3c3094bf5ab55add25218e571e51cc6cb11cfad9e basi4a16.png
|
||||
fe70ea20c8447e82abb7bc9dd05344a61a4631cc69e0974378df8f6263af0337 basi6a08.png
|
||||
7fdd6bf08f04692bcf06b5c4262e7f76c49d8adb992789fd3f05aa0435645cd8 basi6a16.png
|
||||
c8b1364d7771dd2f5a1b2d7d633abcf3f48dafee608558ecd2e5fc98f61894cd basn0g01.png
|
||||
ca172e92d179c77b3d321ad501e452a59bdaafafde8ebd74fd0669c4eae58efc basn0g02.png
|
||||
fb2d42c4c8f5c51590e675994a9b0c718fbf71c80e28142f231cfa7c5b0e26e8 basn0g04.png
|
||||
268d061075d1dd2eeec62b31303d09f6998549e1bfb447a5f09c80a2b0978ac3 basn0g08.png
|
||||
1a1fe155f40c11d79350b3f84c7eb2b29719037e57073cb7ae2ab9e8323f9360 basn0g16.png
|
||||
c90e86090a625661b19960cafdde6e347d6e32d73837aaae533f66dd3f099506 basn2c08.png
|
||||
1e1c28ec0ee5da6224404b4005076dd30e2335a1a80c0ca722046a245c4a2b3b basn2c16.png
|
||||
c2d7cd682df5f74506b33a5d70c344aaee248fda79fdfef8e873426fd6f2b75b basn3p01.png
|
||||
0466bb7ed9984cf03b70704564bcffab1df8ec0e8167473ba0f75e4fedce5a8f basn3p02.png
|
||||
e1fc7be978d3149b98533d0076245ae64353b7967290f4204c1282ecb4ec1aba basn3p04.png
|
||||
d58256cd2eb16b5740d4c1403d25ce43d8dd03e270627ab709d2fb141e3d904c basn3p08.png
|
||||
7be24b618bfd437b921ae0caf9341d112338c6290cc62466deb19d2c7a512968 basn4a08.png
|
||||
1c92ffd11d2fc89a36f170d0239668436407f5c8e3f8d93483fb3fc6bca361d7 basn4a16.png
|
||||
559c594166eb156f461c9beff0f053196730dc998fdb0d2b801c89e6680860a5 basn6a08.png
|
||||
8f9d81060aebf4576461403c5057de7f23f73157016b659402b906df805845aa basn6a16.png
|
||||
dfae01a4548e30519157bb51ce29d4de3b35b74288e9568b4d5911de01c6048b bgai4a08.png
|
||||
ef7df23ccd912309a4f89ca3c3094bf5ab55add25218e571e51cc6cb11cfad9e bgai4a16.png
|
||||
559c594166eb156f461c9beff0f053196730dc998fdb0d2b801c89e6680860a5 bgan6a08.png
|
||||
8f9d81060aebf4576461403c5057de7f23f73157016b659402b906df805845aa bgan6a16.png
|
||||
71d041df5c8949ce07d5e93b3c9f752cefc75ef7326c49df05ae73915f1a755f bgbn4a08.png
|
||||
c45de44d3442bd33072d68456541b10e0cc57c5d8bfc809208995d939091eb2d bggn4a16.png
|
||||
2da3c3efa01bf166958852a104ece92daaede8ffb5e0452a55d40526258a8bf5 bgwn6a08.png
|
||||
875bc2b44a4355551c51260def9664da68be1af8c14b82f76173aaf01ddb1929 bgyn6a16.png
|
||||
c88909e74e039dd3df829bf24144b487171e53b17f5c5c07cbc08688247d24b5 ccwn2c08.png
|
||||
dc365b39d49c287669d837872dd59aef30763a611dfc8c046c481e35e49a390a ccwn3p08.png
|
||||
438018f19c85e582cb586ac7cca2220008ecb7fc70ce50e8f2a76b494c128a20 cdfn2c08.png
|
||||
a068eaf1f7040490e08eda3259befb6689849dd0ff8bb4cc03c705d117cb2b9f cdhn2c08.png
|
||||
ee08dfa303a3c09171280fc1821180dc0a4c3591af7e9077e8ce0435642c76d2 cdsn2c08.png
|
||||
a849d89ba3d92146940d7c56d2fe74c9c3ec96725c7f24eefaf44160a6304de2 cdun2c08.png
|
||||
587a8624228d3f605f475bd751a1f3a97f0bbab590c0ff518bce5da3abcb6f2b ch1n3p04.png
|
||||
e3cace549047c6e5e693b9b135d53f7f9ca5b1088bf30cc408634598ba53758a ch2n3p08.png
|
||||
79ccaf4abe5091e1f65f8a61496363ed3312ebb9ea1aaa98affadac3d98507de cm0n0g04.png
|
||||
127bb50d224ac3ebff36160482a03b9e6686072e86c8ff4cd43e3ce2271b2315 cm7n0g04.png
|
||||
774b384d0ad73698e7d93fc5a29976b332c619045e5966276c609b11459a4152 cm9n0g04.png
|
||||
bc812de2b83abc9ae2a6d1ae01e91f9090214b9ed8fee1d56cdfea58f4f259d0 cs3n2c16.png
|
||||
267104dced0858dd828cdd41edc0e87042c7094c77191565192a67045064b23f cs3n3p08.png
|
||||
028fa49fbba3954c2eba27a4c3607196e87c1dae74192df3b3ad7afe5e0a3f0c cs5n2c08.png
|
||||
a7a4021983d6b22685529cbafb65cd087c9e1af1c5d205c2e13e3efc4ae87b6a cs5n3p08.png
|
||||
d90ddab7313e4e1fd7f20f16a6c546b449697fbcac1a7776298a74896f4662da cs8n2c08.png
|
||||
5bffdb88e307a851f8e85b137a69c59e557a881db816e990061934c0155c894c cs8n3p08.png
|
||||
081d1ec26b4157fbc032b76dc716321420f2d032a425de046557c7842766826d ct0n0g04.png
|
||||
259116f8ecf849d83d824688eb02b4575aed11980f02ab6fb123281c25a6459d ct1n0g04.png
|
||||
4583e63d1bdfa18b6abb47439dee9a73bed311ed1b0a71d22bed75fe4767832b cten0g04.png
|
||||
d0607280c3539a8934c5cfd22788b382cdc31321cdedc75ae8a29e3948580ba6 ctfn0g04.png
|
||||
5f4c87f5a8f589d029a908050880ac5d6aed3810e55e75444802d6d3fb27cf33 ctgn0g04.png
|
||||
8ef799e59871755578e563c5b411bc3895c2f7112c115aaa921b6c04fe255bf2 cthn0g04.png
|
||||
ca91332ecc04e5faac1f39ca4df4996b072a93f3e574e7f4b50d52aafbfe67b6 ctjn0g04.png
|
||||
c0765e635a6423ec64f0314400c3df20745c4a4469ffcc323c7b00ba66b4aceb ctzn0g04.png
|
||||
d04140d74bc60597c47b5aac371a3553d3add9354437bbd8c57007d94e197809 exif2c08.png
|
||||
d055cc0bb505b37c6ecf88a808d71675eca5dd0ee683d70ebc9c0be0e1bb8e57 f00n0g08.png
|
||||
0fe92b2aa2da04c885d1dbd85c834716f6cdd946364d97dcd597bb79d9e14427 f00n2c08.png
|
||||
956ddaf133e6d9a8d36f1e00604d87fbc2d0295e933bb8caaef517e7d87e5342 f01n0g08.png
|
||||
ef072ec6815ebf9b33e0553d2e4e4e7ed6911860a2512c67bcd10a9f0f09b9de f01n2c08.png
|
||||
1f5b49c06ecc2a1756f0423b3de936bd18497866d035ac8eded78a6f17408a2c f02n0g08.png
|
||||
ca4b937b3c587d5c007f193a2eec14dc96b0d23ff7d6aa9004e3badd1af9fe8f f02n2c08.png
|
||||
d0563ae30c6ce7a01eae7a5cdc0ef780175bfa7653888ed93f17bef5fe4b2107 f03n0g08.png
|
||||
2d101e3ef4f78a69437034671e93fe11faac0cfc4d44210dcca1b944caa886f7 f03n2c08.png
|
||||
fec1a3c53833d0ddefa4486a98934c393cb4335b2dc31da70e6540a4686f1457 f04n0g08.png
|
||||
c365c24153cb69fd3c162f00b296ae23a71a1595645d1aeb0ad23af680d7b4be f04n2c08.png
|
||||
8ac0f095d2a943157e820fa121bccde08d5230af1b5830c3041d5f4da3524eba f99n0g04.png
|
||||
3494b914dd1b094afa9a74a89bfa75219030e21af6fca3ed66c9d45032a047e9 g03n0g16.png
|
||||
abce774b9624952c2d0c57ff681790ba620cf5616fe7e0ef0600600cdce3c22a g03n2c08.png
|
||||
541389db0c72721c6c49501e1dbf6e32df41b4fcfa318676955e278f3f4332b3 g03n3p04.png
|
||||
72350a2d9db2df09b626f1f1dd48061e9c749d471bf19814d2c5c10acbf007e5 g04n0g16.png
|
||||
bfd3edfc4d85a43383556a51023533fa3888184aa53a6b43769910782beebfce g04n2c08.png
|
||||
5ea85ebefb58b2ab113fc05bf1cb79ffb068359e3a5620843590a0a7b59bd49a g04n3p04.png
|
||||
be19721d28e0b268af1e29e8190c952b94315bd08899383ae2c91e6dc8c4f889 g05n0g16.png
|
||||
74ab5b8477992d65e220eeaff8df466522c1b392fa90c6f9935da5bdb7113c9d g05n2c08.png
|
||||
2f46afd7ec15836b523cbce6965f34d89833b8d777afffaf0f53bce620d5f65e g05n3p04.png
|
||||
9070f843981f765cc8e26a63751be5d58631d85a8a7b4e826358f80e8a679d03 g07n0g16.png
|
||||
ca4cd32d222f65fb7beb7c9f40d8b6833b36d552b830ff1a1d67dae73b8ed0d8 g07n2c08.png
|
||||
b8e7abeabcfd50b71cca78b6cdff7372310cfc179cd63304c3e904f469b333cb g07n3p04.png
|
||||
a22486acb74ee5f947db3a20b1f314a9dd8aca3339840049ee559f4d676a6b5c g10n0g16.png
|
||||
4ac23729aea109e8f6b1e831448115cf02dcd97559e9403847cbe6dd7e5c7347 g10n2c08.png
|
||||
36cc2ea4b5b33cd18e0a01c7de85dbcaf7161d258cd0f2265a90cd835b15cf6d g10n3p04.png
|
||||
1fae707d809296d2cf0d6aa653bead6199644fb1c859558ec7d2d69e709a423a g25n0g16.png
|
||||
9b128cfa1bb417dd99251914d729062621fcea9f346168549b16a75fe030b0f6 g25n2c08.png
|
||||
4197a0a25c4f74d42c2285d75b9fabe1eedbed45bc224ff1ec1784f5797e9dbf g25n3p04.png
|
||||
1a1fe155f40c11d79350b3f84c7eb2b29719037e57073cb7ae2ab9e8323f9360 oi1n0g16.png
|
||||
1e1c28ec0ee5da6224404b4005076dd30e2335a1a80c0ca722046a245c4a2b3b oi1n2c16.png
|
||||
142bc4b1e9f8a9e3b0ba3d2c2025e485bf7e76df88f914bf8c9041b4562cc080 oi2n0g16.png
|
||||
2c523f138e0ade1bed8f31d59ec0f34b82660b192d6af8b8f395f82b9ba9dabb oi2n2c16.png
|
||||
aa2d77723ba44f654a41a2ab97e01271fe942fecd2f299c0a81da90acb2887ac oi4n0g16.png
|
||||
661220a0ed03ca6d63ec8dcaac738d4a9fd089b491c40dd896267ffb6cf5015d oi4n2c16.png
|
||||
ae70c9effd0d9957a0d42771bc987529a1f3715e068057fae63f0c7e50d5c53a oi9n0g16.png
|
||||
fef98e9bebe6c4046c29f2568f7a735305c8661aa01f9c37a718135e5bc4f941 oi9n2c16.png
|
||||
72406b6173ec20dd059e8cea594ef5bc8f81bac5e416e2b53058d2debd51d195 pp0n2c16.png
|
||||
d5a6f964a601a5e1815525117c12c32b61e2aa09fd6040776d35f2f7115b7110 pp0n6a08.png
|
||||
b321b9ac8f2aa641475b6936365954181726dcb8662dbf641284d82a83cc22a5 ps1n0g08.png
|
||||
6c59d673032fe2d04dbd2ea91cc9bc7bebe7d5ab84911f00e36fb28994793576 ps1n2c16.png
|
||||
bd2ae1b45fd771aaa8c57dccd183f3834fc9742f6dfc692606865049efb116e5 ps2n0g08.png
|
||||
120c62ce9d014f7772c4b38a446991b5bbe6a22ef5ea59239d32e023bc167585 ps2n2c16.png
|
||||
235eb82f1337e3fcefbdca566a53af3d3523db7588ba3cd7fa5dab19afe48a41 s01i3p01.png
|
||||
6a4f6da9fe36d195219ad2beeac1a0423e2ee6de03dfb064a3b6fa31623eb230 s01n3p01.png
|
||||
e7d26ab4b31e7066b0674a4374c093967a9f1761b96836aa3dbe624acd73ab52 s02i3p01.png
|
||||
f4b324f2b1d013ee77f632e42129c7cd3eca55da846e775e744a1f224ad1efcd s02n3p01.png
|
||||
4513dec80f54ffd04a28834b114254d6e6d66e52e32e0059174474f2fb15d6bc s03i3p01.png
|
||||
4dee297bd7f747f87488509af11a1e1e0531fa67e6e8d0f8ab7f4df7940f0ff3 s03n3p01.png
|
||||
918f94f3557e10de91efa680df507c8eafc347fa5023af43037701398128ad0f s04i3p01.png
|
||||
e73c52f764b684ec5133cce11f7f18cc60065acdd308e2c3d871e0a792a1080e s04n3p01.png
|
||||
77fe040e17167f9656afe46cf0031832e635745e4e7f19ba12dee32f5ec03d96 s05i3p02.png
|
||||
1b9e244a5c1d6cc9830391fff4eac565ada026d000e60faf7525c8add6bdc726 s05n3p02.png
|
||||
cf8d5fdb34c0afd38b1b4745bed6167771767ccdfe847c4d8766f7fcc5c41176 s06i3p02.png
|
||||
5caabfc4b147fad5e49e558143faaab91888324240acf45e5db8fc49f7a7f667 s06n3p02.png
|
||||
d9874bfad10353a9ea2876ee388c3e7f81f697ed141d0d79c5c2019d62a7a698 s07i3p02.png
|
||||
e7d447394f5fa64c27353216af387352a77a1fb534922a2d974e60d965d4e414 s07n3p02.png
|
||||
62adf855c6b1e3a007f9dcf3ce2ba244616c089269a511dd939e89a4b0853453 s08i3p02.png
|
||||
544fdd476e566e8aef4c98604124ae05acef48be20d1aebb9b6674b08b2abbb6 s08n3p02.png
|
||||
f2cf5b3109690704d106a0b22ea079e36064cc6077e1fb9a1ba87397d0bbcca1 s09i3p02.png
|
||||
6be8f58b638e7f7114750f5796f66f650db8fb7242a36183850f02117f4eaa65 s09n3p02.png
|
||||
a27e0dbf923ef868e7399560b76c23a50b2d8926cddd9775e2bc4e050273f205 s32i3p04.png
|
||||
c80d9fc7ded0b7834ec11d0e324540c7be74016b61bc302a1d167218fa35fbf9 s32n3p04.png
|
||||
cc6ad98dcf686d92f1165d951f90fac70e568cedb1fcbba33e8890d0e6cdad4d s33i3p04.png
|
||||
998d08e084e3eea288984475bfed0d48ee2d8250e20b96755898b59152e36a34 s33n3p04.png
|
||||
83eb2e82eb14ae3c608833ef371a6f472c4c6e63b7e9f4f92f5472f975793b97 s34i3p04.png
|
||||
a25451168c0b03ab8f52dcc60c6b9c12bc5df5d55afabe1892b87d0a856bb4a0 s34n3p04.png
|
||||
db5988c27ad577de1e494cb50229dc2772678531adde2db84493907eae003e54 s35i3p04.png
|
||||
cd99fc4cc69bda1ff8e8697d8b423ae8d9e84a58ba1d2b480751953b6869e3b3 s35n3p04.png
|
||||
a36b40927b730a31d60c7818fb4a32e67ace06393cdaa26edc09520b6cee066d s36i3p04.png
|
||||
ebc08f6a1e0918c5d3a9a08e184c23019a3cf90d8843e4b71bf2ae16040b1a84 s36n3p04.png
|
||||
565c79404c4b9e46c27edb2f93d7b26b93ec0a0481ab7a21fc405152a21f53ba s37i3p04.png
|
||||
964078b1f3f472268b8ac97a8976b4d1cdb6a47e6243d67eddcb0205e65b9f61 s37n3p04.png
|
||||
b107f120ffaffcb949ac4547e0f91555ac681f78c7e68c724e7ceddfd8e5387b s38i3p04.png
|
||||
045b0f5ea35874741bf6cac40f2e63ec8726450ab241475df4b7e9303c39e339 s38n3p04.png
|
||||
4b5a6d8c04c8c70e1de3bcb6f7b5cc3ba359b13e28de6f52e21a291d4bc8a937 s39i3p04.png
|
||||
2115b6cad0140ea97fe6ec056b47307ef1e8b5c00db5f08707a0572b165e5f08 s39n3p04.png
|
||||
48a3f19b5c3b2f7eca17fd34765a2a3e385d770f24c357bc66bd74417eb60a5a s40i3p04.png
|
||||
c4598ea73b1ec015bef7dcfd7f67bc7bfc41546619953d1a92cd9b8657a7033a s40n3p04.png
|
||||
c5e7eeaffa677bfac002ee968a0423e0fea3be7701d39ccd6a015df1d2d85e40 tbbn0g04.png
|
||||
8fcfd031a5b716201854fdab25691feed6cd3ed7b713a51b94e1f43fc63fe0c1 tbbn2c16.png
|
||||
8eb43a6020fda8ed7c8c9b88b974ef794ee8452db29212ae7e5c11003bfd76a3 tbbn3p08.png
|
||||
49711ee271f96099521b83b75d58ed2f76d42ed46cb3c06656fde2cad697cf59 tbgn2c16.png
|
||||
6f798628935a021fe446dce3d62861d1f7369aaf01bad10daa4917c5abccad7d tbgn3p08.png
|
||||
8223d8b0c5f84b149f0c1f0e1f658346e07fcbd03ffc02b2e9eeea1481eb19bf tbrn2c08.png
|
||||
01e86f108b244bdee4d72e384aced8a62369965ca2993ee6ef88f131c647f7d1 tbwn0g16.png
|
||||
978b28cec43bd219517083b63661bb30a7b9cdf9fd4476ab4e70844565980d51 tbwn3p08.png
|
||||
7b4eb64b4bd7eb36417752873ddaca027ce77a07f474b2af569f4eea1e984111 tbyn3p08.png
|
||||
f05e317f64a038123faf96fdbd6fc3ab3eec14e1fc1eb87fd7da4d996be63526 tm3n3p02.png
|
||||
a253e055d8cd94049cd95e9392e7aa60f3d5c922a8f0cccb84762b0c5f547545 tp0n0g08.png
|
||||
5dced3fde56b950e61ce86f4ad54458336f66389ce8ca68524d4fd8ec546ff5b tp0n2c08.png
|
||||
da40ddb18bd45db32ed03dc89e2250b096d808efe975a88477e4f5a91a4677c9 tp0n3p08.png
|
||||
50bc927fe6cf1816454a7c27f8c743734be8db45e37e5ee3ede540f4e895e6c5 tp1n3p08.png
|
||||
4059f7e6a1c5bac1801f70e09f9ec1e1297dcdce34055c13ab2703d6d9613c7e xc1n0g08.png
|
||||
e252a0e7df3e794e52ce4a831edafef76e7043d0d8d84019db0f7fd0b30e20f4 xc9n2c08.png
|
||||
3c0c2a68dd416a7be79400e0e2ceb358ba3191d5309d4bad9dd04b32ff6d5b60 xcrn0g04.png
|
||||
71e4b2826f61556eda39f3a93c8769b14d3ac90f135177b9373061199dbef39a xcsn0g01.png
|
||||
c1287690808e809dc5d4fb89d8a7fd69ed93521f290abd42021ca00a061a1ba4 xd0n2c08.png
|
||||
00b53c3bbd0641454521b982bc6f6bcfda7c91f1874cefb3a9bac37d80a1a269 xd3n2c08.png
|
||||
16e5b40fb2600db1af20fb79ff715c2869255e2d4bef20702f16534e5dd6a847 xd9n2c08.png
|
||||
f9d1fb2a708703518368c392c74765a6e3e5b49dbb9717df3974452291032df9 xdtn0g01.png
|
||||
318864720c8fc0dbe4884035f2183cc3bd3d92ec60d447879982942600e9fe2e xhdn0g08.png
|
||||
968fd21abb8acf40fbca90676de3abcb3a6e35b01ba7a3b9190184eafb99c83d xlfn0g04.png
|
||||
776701227c7094dd10b78f508430b8ea4f03471d072096382cbcca628dea1d2b xs1n0g01.png
|
||||
9dd7e93ba9211f0caee09d8f12e37742936c469f45fcad21e577c9f0640cf99e xs2n0g01.png
|
||||
166c8633d116d1c26631cb98f3a75340cf4385fa7ae6a67913bc78e709bcb30d xs4n0g01.png
|
||||
7a380568beeac969908196ac89cd2347c96ac2f26fdc2b8e0314b62c59f0e308 xs7n0g01.png
|
||||
e4767d3259e1b331c9ece9fb07fc30f385879ac12b3cd0e1cb923f3bfac81dcf z00n2c08.png
|
||||
939abfc6e0a3e349bb5bcd8c4e427a6cdb2c7e0c23a773aefa1c4fc9f07e4cb4 z03n2c08.png
|
||||
21652fe42f472acd83dcb207806add5737db0020b21d6b38b0a00be3e9e37990 z06n2c08.png
|
||||
98d90a7e61aa1a9ac9d99af44dd25c70cd5a7b4a11a85da60f82923e18a78f9f z09n2c08.png
|
||||
+514
-24
@@ -1,10 +1,15 @@
|
||||
//! `cargo xtask corpus [...]` (fetch test corpora) and `cargo xtask test-corpus [...]` (fetch,
|
||||
//! then run the corpus tests). Pins: `corpus_pins.rs`. Docs: `docs/development.md` › Test corpora.
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::collections::BTreeMap;
|
||||
use std::io::{Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
use crate::corpus_pins::{self, AG_PSD_COMMIT, HEIC_RS_COMMIT, OPENEXR_COMMIT, PHOTOCRAFT_CORPUS_COMMIT, PILLOW_HEIF_COMMIT, PNGSUITE_URL, PSD_TOOLS_COMMIT};
|
||||
use crate::corpus_pins::{
|
||||
self, AG_PSD_COMMIT, HEIC_RS_COMMIT, OPENEXR_COMMIT, PHOTOCRAFT_CORPUS_COMMIT, PILLOW_HEIF_COMMIT, PNGSUITE_SHA256, PNGSUITE_URL, PSD_TOOLS_COMMIT,
|
||||
};
|
||||
use crate::pinned::{PinnedCorpus, USER_AGENT};
|
||||
use crate::{cargo, root, run};
|
||||
|
||||
@@ -119,7 +124,7 @@ fn status(present: bool) -> &'static str {
|
||||
fn list() {
|
||||
let corpus = root().join("corpus");
|
||||
let png = corpus.join("pngsuite");
|
||||
let png_ok = pngsuite_count(&png) > 0;
|
||||
let png_ok = verify_pngsuite(&png, PNGSUITE_MANIFEST).is_ok();
|
||||
println!(
|
||||
"Test corpora live under {} (gitignored, never committed). The corpus tests are opt-in
|
||||
(cargo feature `corpus`) and fail, not skip, when a corpus is missing. CI always runs them.
|
||||
@@ -162,31 +167,212 @@ Pins: xtask/src/corpus_pins.rs. Moving one: change it, then --<name> --update-ma
|
||||
);
|
||||
}
|
||||
|
||||
fn pngsuite_count(dir: &std::path::Path) -> usize {
|
||||
std::fs::read_dir(dir).map(|d| d.flatten().filter(|e| e.path().extension().is_some_and(|x| x == "png")).count()).unwrap_or(0)
|
||||
const PNGSUITE_MANIFEST: &str = include_str!("../pngsuite.sha256");
|
||||
const PNGSUITE_MAX_ARCHIVE_BYTES: u64 = 1024 * 1024;
|
||||
const PNGSUITE_MAX_FILE_BYTES: u64 = 4 * 1024 * 1024;
|
||||
static PNGSUITE_STAGE_ID: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
/// Do not trust cached filenames, symlinks, or a file's size remaining unchanged during a read.
|
||||
fn pngsuite_bytes(path: &Path, limit: u64) -> Result<Vec<u8>, String> {
|
||||
let meta = std::fs::symlink_metadata(path).map_err(|e| format!("{}: {e}", path.display()))?;
|
||||
if !meta.is_file() || meta.len() > limit {
|
||||
return Err(format!("{}: expected a regular file of at most {limit} bytes", path.display()));
|
||||
}
|
||||
let mut bytes = Vec::new();
|
||||
std::fs::File::open(path)
|
||||
.map_err(|e| format!("{}: {e}", path.display()))?
|
||||
.take(limit + 1)
|
||||
.read_to_end(&mut bytes)
|
||||
.map_err(|e| format!("{}: {e}", path.display()))?;
|
||||
if bytes.len() as u64 > limit {
|
||||
return Err(format!("{}: exceeds {limit} bytes", path.display()));
|
||||
}
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
/// PngSuite: 175 PNGs from a fixed release archive (skipped when already present).
|
||||
fn fetch_pngsuite() -> Result<(), String> {
|
||||
let corpus = root().join("corpus");
|
||||
fn verify_pngsuite(dir: &Path, manifest: &str) -> Result<(), String> {
|
||||
let mut expected = BTreeMap::new();
|
||||
for line in manifest.lines() {
|
||||
let Some((hash, name)) = line.split_once(" ") else { return Err("pngsuite: invalid manifest line".into()) };
|
||||
if hash.len() != 64
|
||||
|| !hash.bytes().all(|c| c.is_ascii_hexdigit())
|
||||
|| name.is_empty()
|
||||
|| name.contains("..")
|
||||
|| !name.bytes().all(|c| c.is_ascii_alphanumeric() || b"._-".contains(&c))
|
||||
|| expected.insert(name, hash).is_some()
|
||||
{
|
||||
return Err("pngsuite: invalid manifest entry".into());
|
||||
}
|
||||
}
|
||||
if expected.is_empty() {
|
||||
return Err("pngsuite: empty manifest".into());
|
||||
}
|
||||
let meta = std::fs::symlink_metadata(dir).map_err(|e| format!("{}: {e}", dir.display()))?;
|
||||
if !meta.is_dir() {
|
||||
return Err(format!("{}: expected a regular directory", dir.display()));
|
||||
}
|
||||
let entries = std::fs::read_dir(dir).map_err(|e| format!("{}: {e}", dir.display()))?;
|
||||
let mut seen = 0;
|
||||
for entry in entries {
|
||||
let entry = entry.map_err(|e| format!("{}: {e}", dir.display()))?;
|
||||
let name = entry.file_name();
|
||||
let hash = name.to_str().and_then(|n| expected.get(n)).ok_or_else(|| format!("pngsuite: unexpected file {}", entry.path().display()))?;
|
||||
let bytes = pngsuite_bytes(&entry.path(), PNGSUITE_MAX_FILE_BYTES)?;
|
||||
if crate::sha256::hex(&bytes) != **hash {
|
||||
return Err(format!("pngsuite: {}: sha256 mismatch", entry.path().display()));
|
||||
}
|
||||
seen += 1;
|
||||
}
|
||||
if seen != expected.len() {
|
||||
return Err(format!("pngsuite: missing files ({seen} of {})", expected.len()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct PngSuiteStage {
|
||||
path: PathBuf,
|
||||
keep: bool,
|
||||
}
|
||||
|
||||
impl PngSuiteStage {
|
||||
fn new(corpus: &Path) -> Result<Self, String> {
|
||||
std::fs::create_dir_all(corpus).map_err(|e| format!("create {}: {e}", corpus.display()))?;
|
||||
for _ in 0..100 {
|
||||
let id = PNGSUITE_STAGE_ID.fetch_add(1, Ordering::Relaxed);
|
||||
let path = corpus.join(format!(".pngsuite-{}-{id}", std::process::id()));
|
||||
match std::fs::create_dir(&path) {
|
||||
Ok(()) => return Ok(Self { path, keep: false }),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {}
|
||||
Err(e) => return Err(format!("create {}: {e}", path.display())),
|
||||
}
|
||||
}
|
||||
Err("pngsuite: could not create a staging directory".into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PngSuiteStage {
|
||||
fn drop(&mut self) {
|
||||
if !self.keep {
|
||||
let _ = std::fs::remove_dir_all(&self.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Keep the old cache until the verified replacement is in place. On a failed rollback its
|
||||
/// backup remains in staging and the error names it, rather than deleting the only copy.
|
||||
fn install_pngsuite(
|
||||
dest: &Path,
|
||||
files: &Path,
|
||||
backup: &Path,
|
||||
backup_retained: &mut bool,
|
||||
mut rename: impl FnMut(&Path, &Path) -> std::io::Result<()>,
|
||||
) -> Result<(), String> {
|
||||
let had_cache = match std::fs::symlink_metadata(dest) {
|
||||
Ok(_) => true,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => false,
|
||||
Err(e) => return Err(format!("{}: {e}", dest.display())),
|
||||
};
|
||||
if had_cache {
|
||||
rename(dest, backup).map_err(|e| format!("pngsuite: preserve {}: {e}", dest.display()))?;
|
||||
*backup_retained = true;
|
||||
}
|
||||
if let Err(e) = rename(files, dest) {
|
||||
if had_cache && let Err(restore) = rename(backup, dest) {
|
||||
return Err(format!("pngsuite: install failed ({e}); restore failed ({restore}); old cache retained at {}", backup.display()));
|
||||
}
|
||||
*backup_retained = false;
|
||||
return Err(format!("pngsuite: install failed: {e}"));
|
||||
}
|
||||
*backup_retained = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fetch_pngsuite_in(
|
||||
corpus: &Path,
|
||||
archive_hash: &str,
|
||||
manifest: &str,
|
||||
download: impl FnOnce(&Path) -> Result<(), String>,
|
||||
extract: impl FnOnce(&Path, &Path) -> Result<(), String>,
|
||||
rename: impl FnMut(&Path, &Path) -> std::io::Result<()>,
|
||||
) -> Result<(), String> {
|
||||
let dest = corpus.join("pngsuite");
|
||||
let have = pngsuite_count(&dest);
|
||||
if have >= 175 {
|
||||
println!("pngsuite: {have} PNG files in {} already", dest.display());
|
||||
if verify_pngsuite(&dest, manifest).is_ok() {
|
||||
println!("pngsuite: {} already matches its manifest", dest.display());
|
||||
return Ok(());
|
||||
}
|
||||
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", "--retry", "3", "-A", USER_AGENT, "-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 = pngsuite_count(&dest);
|
||||
println!("pngsuite: {n} PNG files in {}", dest.display());
|
||||
if n == 0 { Err("no PNG files extracted".into()) } else { Ok(()) }
|
||||
let mut stage = PngSuiteStage::new(corpus)?;
|
||||
let archive = stage.path.join("archive.tgz");
|
||||
download(&archive)?;
|
||||
let bytes = pngsuite_bytes(&archive, PNGSUITE_MAX_ARCHIVE_BYTES)?;
|
||||
if crate::sha256::hex(&bytes) != archive_hash {
|
||||
return Err("pngsuite: archive sha256 mismatch; nothing extracted".into());
|
||||
}
|
||||
let files = stage.path.join("files");
|
||||
std::fs::create_dir(&files).map_err(|e| format!("create {}: {e}", files.display()))?;
|
||||
extract(&archive, &files)?;
|
||||
verify_pngsuite(&files, manifest)?;
|
||||
let backup = stage.path.join("previous");
|
||||
install_pngsuite(&dest, &files, &backup, &mut stage.keep, rename)?;
|
||||
println!("pngsuite: verified {}", dest.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Older curl versions cannot enforce --max-filesize for unknown-length responses. Write
|
||||
/// only the allowed prefix, then probe for excess without spooling that byte to disk.
|
||||
fn copy_pngsuite_download(reader: &mut impl Read, writer: &mut impl Write) -> std::io::Result<()> {
|
||||
std::io::copy(&mut reader.take(PNGSUITE_MAX_ARCHIVE_BYTES), writer)?;
|
||||
let mut extra = [0; 1];
|
||||
loop {
|
||||
match reader.read(&mut extra) {
|
||||
Ok(0) => return Ok(()),
|
||||
Ok(_) => return Err(std::io::Error::other(format!("pngsuite: archive exceeds {PNGSUITE_MAX_ARCHIVE_BYTES} bytes"))),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn finish_pngsuite_download(child: &mut Child, mut reader: impl Read, writer: &mut impl Write) -> Result<(), String> {
|
||||
let result = copy_pngsuite_download(&mut reader, writer).map_err(|e| format!("pngsuite: download: {e}")).and_then(|()| {
|
||||
let status = child.wait().map_err(|e| format!("pngsuite: wait for curl: {e}"))?;
|
||||
if status.success() { Ok(()) } else { Err(format!("pngsuite: curl failed: {status}")) }
|
||||
});
|
||||
if result.is_err() {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn download_pngsuite(mut curl: Command, archive: &Path) -> Result<(), String> {
|
||||
let mut file = std::fs::File::create(archive).map_err(|e| format!("create {}: {e}", archive.display()))?;
|
||||
let mut child = curl.stdout(Stdio::piped()).spawn().map_err(|e| format!("pngsuite: start curl: {e}"))?;
|
||||
let Some(stdout) = child.stdout.take() else {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
return Err("pngsuite: curl stdout unavailable".into());
|
||||
};
|
||||
finish_pngsuite_download(&mut child, stdout, &mut file)
|
||||
}
|
||||
|
||||
/// PngSuite: fixed archive checked before tar; every cached file checked on each invocation.
|
||||
fn fetch_pngsuite() -> Result<(), String> {
|
||||
fetch_pngsuite_in(
|
||||
&root().join("corpus"),
|
||||
PNGSUITE_SHA256,
|
||||
PNGSUITE_MANIFEST,
|
||||
|archive| {
|
||||
let mut curl = Command::new("curl");
|
||||
curl.args(["-fsSL", "--retry", "3", "--max-filesize", &PNGSUITE_MAX_ARCHIVE_BYTES.to_string(), "-A", USER_AGENT, "-o", "-", PNGSUITE_URL]);
|
||||
download_pngsuite(curl, archive)
|
||||
},
|
||||
|archive, files| {
|
||||
let mut tar = Command::new("tar");
|
||||
tar.arg("-xzf").arg(archive).arg("-C").arg(files);
|
||||
run(tar, "tar -xzf PngSuite-2017jul19.tgz")
|
||||
},
|
||||
|from, to| std::fs::rename(from, to),
|
||||
)
|
||||
}
|
||||
|
||||
/// Files changed relative to `origin/main` (committed, staged or not).
|
||||
@@ -286,4 +472,308 @@ mod tests {
|
||||
fn all_without_update_manifest_only_verifies() {
|
||||
assert!(calls(false, false).iter().all(|(_, u)| !*u));
|
||||
}
|
||||
|
||||
const FIXTURE_FILES: &[(&str, &[u8])] = &[("a.png", b"alpha"), ("b.png", b"beta"), ("PngSuite.LICENSE", b"fixture licence")];
|
||||
const FIXTURE_ARCHIVE: &[u8] = b"owned archive fixture";
|
||||
|
||||
struct Fixture(PathBuf);
|
||||
|
||||
impl Fixture {
|
||||
fn new() -> Self {
|
||||
let id = PNGSUITE_STAGE_ID.fetch_add(1, Ordering::Relaxed);
|
||||
let path = std::env::temp_dir().join(format!("photocraft-pngsuite-test-{}-{id}", std::process::id()));
|
||||
std::fs::create_dir(&path).unwrap();
|
||||
Self(path)
|
||||
}
|
||||
|
||||
fn cache(&self) -> PathBuf {
|
||||
self.0.join("pngsuite")
|
||||
}
|
||||
|
||||
fn old_cache(&self) {
|
||||
std::fs::create_dir(self.cache()).unwrap();
|
||||
std::fs::write(self.cache().join("old.png"), b"old cache bytes").unwrap();
|
||||
}
|
||||
|
||||
fn assert_old_cache(&self) {
|
||||
assert_eq!(std::fs::read(self.cache().join("old.png")).unwrap(), b"old cache bytes");
|
||||
assert_eq!(std::fs::read_dir(&self.0).unwrap().count(), 1, "staging was not cleaned");
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Fixture {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn fixture_manifest() -> String {
|
||||
FIXTURE_FILES.iter().map(|(name, bytes)| format!("{} {name}\n", crate::sha256::hex(bytes))).collect()
|
||||
}
|
||||
|
||||
fn fixture_extract(_: &Path, files: &Path) -> Result<(), String> {
|
||||
for (name, bytes) in FIXTURE_FILES {
|
||||
std::fs::write(files.join(name), bytes).map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fixture_download(archive: &Path) -> Result<(), String> {
|
||||
std::fs::write(archive, FIXTURE_ARCHIVE).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn fixture_fetch(corpus: &Path) -> Result<(), String> {
|
||||
fetch_pngsuite_in(corpus, &crate::sha256::hex(FIXTURE_ARCHIVE), &fixture_manifest(), fixture_download, fixture_extract, |from, to| {
|
||||
std::fs::rename(from, to)
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pngsuite_cache_checks_bytes_names_and_regular_files() {
|
||||
let f = Fixture::new();
|
||||
std::fs::create_dir(f.cache()).unwrap();
|
||||
fixture_extract(Path::new("unused"), &f.cache()).unwrap();
|
||||
assert!(verify_pngsuite(&f.cache(), &fixture_manifest()).is_ok());
|
||||
std::fs::write(f.cache().join("a.png"), b"changed").unwrap();
|
||||
assert!(verify_pngsuite(&f.cache(), &fixture_manifest()).unwrap_err().contains("sha256 mismatch"));
|
||||
fixture_extract(Path::new("unused"), &f.cache()).unwrap();
|
||||
std::fs::remove_file(f.cache().join("a.png")).unwrap();
|
||||
assert!(verify_pngsuite(&f.cache(), &fixture_manifest()).unwrap_err().contains("missing files"));
|
||||
fixture_extract(Path::new("unused"), &f.cache()).unwrap();
|
||||
std::fs::write(f.cache().join("extra.png"), b"extra").unwrap();
|
||||
assert!(verify_pngsuite(&f.cache(), &fixture_manifest()).unwrap_err().contains("unexpected file"));
|
||||
std::fs::remove_file(f.cache().join("extra.png")).unwrap();
|
||||
std::fs::remove_file(f.cache().join("a.png")).unwrap();
|
||||
std::fs::create_dir(f.cache().join("a.png")).unwrap();
|
||||
assert!(verify_pngsuite(&f.cache(), &fixture_manifest()).unwrap_err().contains("regular file"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pngsuite_valid_cache_needs_no_download_or_extraction() {
|
||||
let f = Fixture::new();
|
||||
fixture_fetch(&f.0).unwrap();
|
||||
fetch_pngsuite_in(
|
||||
&f.0,
|
||||
"unused",
|
||||
&fixture_manifest(),
|
||||
|_| panic!("valid cache downloaded"),
|
||||
|_, _| panic!("valid cache extracted"),
|
||||
|_, _| panic!("valid cache renamed"),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pngsuite_repairs_unverified_175_file_cache_and_removes_extras() {
|
||||
let f = Fixture::new();
|
||||
std::fs::create_dir(f.cache()).unwrap();
|
||||
for i in 0..175 {
|
||||
std::fs::write(f.cache().join(format!("fake-{i}.png")), b"not a PNG").unwrap();
|
||||
}
|
||||
fixture_fetch(&f.0).unwrap();
|
||||
assert!(verify_pngsuite(&f.cache(), &fixture_manifest()).is_ok());
|
||||
assert_eq!(std::fs::read_dir(f.cache()).unwrap().count(), FIXTURE_FILES.len());
|
||||
assert_eq!(std::fs::read_dir(&f.0).unwrap().count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pngsuite_unexpected_or_truncated_archive_never_reaches_extractor() {
|
||||
for bad in [b"unexpected".as_slice(), &FIXTURE_ARCHIVE[..4]] {
|
||||
let f = Fixture::new();
|
||||
f.old_cache();
|
||||
let e = fetch_pngsuite_in(
|
||||
&f.0,
|
||||
&crate::sha256::hex(FIXTURE_ARCHIVE),
|
||||
&fixture_manifest(),
|
||||
|a| std::fs::write(a, bad).map_err(|e| e.to_string()),
|
||||
|_, _| panic!("unverified archive reached extractor"),
|
||||
|_, _| panic!("unverified archive reached installation"),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(e.contains("archive sha256 mismatch"));
|
||||
f.assert_old_cache();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pngsuite_download_or_extraction_failure_keeps_old_cache() {
|
||||
let f = Fixture::new();
|
||||
f.old_cache();
|
||||
assert!(
|
||||
fetch_pngsuite_in(
|
||||
&f.0,
|
||||
"unused",
|
||||
&fixture_manifest(),
|
||||
|_| Err("offline".into()),
|
||||
|_, _| panic!("failed download extracted"),
|
||||
|_, _| panic!("failed download installed")
|
||||
)
|
||||
.unwrap_err()
|
||||
.contains("offline")
|
||||
);
|
||||
f.assert_old_cache();
|
||||
assert!(
|
||||
fetch_pngsuite_in(
|
||||
&f.0,
|
||||
&crate::sha256::hex(FIXTURE_ARCHIVE),
|
||||
&fixture_manifest(),
|
||||
fixture_download,
|
||||
|_, files| {
|
||||
std::fs::write(files.join("partial.png"), b"partial").unwrap();
|
||||
Err("extract failed".into())
|
||||
},
|
||||
|_, _| panic!("failed extraction installed")
|
||||
)
|
||||
.unwrap_err()
|
||||
.contains("extract failed")
|
||||
);
|
||||
f.assert_old_cache();
|
||||
for extra in [false, true] {
|
||||
let e = fetch_pngsuite_in(
|
||||
&f.0,
|
||||
&crate::sha256::hex(FIXTURE_ARCHIVE),
|
||||
&fixture_manifest(),
|
||||
fixture_download,
|
||||
|a, files| {
|
||||
fixture_extract(a, files)?;
|
||||
if extra {
|
||||
std::fs::write(files.join("extra.png"), b"extra").unwrap();
|
||||
} else {
|
||||
std::fs::remove_file(files.join("a.png")).unwrap();
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
|_, _| panic!("unverified extraction installed"),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(e.contains(if extra { "unexpected file" } else { "missing files" }));
|
||||
f.assert_old_cache();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pngsuite_failed_installation_restores_old_cache() {
|
||||
for fail_backup in [false, true] {
|
||||
let f = Fixture::new();
|
||||
f.old_cache();
|
||||
let e = fetch_pngsuite_in(&f.0, &crate::sha256::hex(FIXTURE_ARCHIVE), &fixture_manifest(), fixture_download, fixture_extract, |from, to| {
|
||||
if (fail_backup && from == f.cache()) || (!fail_backup && from.file_name().is_some_and(|n| n == "files")) {
|
||||
Err(std::io::Error::other("injected rename failure"))
|
||||
} else {
|
||||
std::fs::rename(from, to)
|
||||
}
|
||||
})
|
||||
.unwrap_err();
|
||||
assert!(e.contains("injected rename failure"));
|
||||
f.assert_old_cache();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pngsuite_failed_rollback_retains_and_names_the_backup() {
|
||||
let f = Fixture::new();
|
||||
f.old_cache();
|
||||
let e = fetch_pngsuite_in(&f.0, &crate::sha256::hex(FIXTURE_ARCHIVE), &fixture_manifest(), fixture_download, fixture_extract, |from, to| {
|
||||
if from == f.cache() { std::fs::rename(from, to) } else { Err(std::io::Error::other("injected install and rollback failure")) }
|
||||
})
|
||||
.unwrap_err();
|
||||
let stage = std::fs::read_dir(&f.0).unwrap().next().unwrap().unwrap().path();
|
||||
let backup = stage.join("previous");
|
||||
assert!(e.contains(&backup.display().to_string()), "{e}");
|
||||
assert_eq!(std::fs::read(backup.join("old.png")).unwrap(), b"old cache bytes");
|
||||
std::fs::rename(backup, f.cache()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pngsuite_reads_are_bounded_and_malformed_manifests_are_rejected() {
|
||||
let f = Fixture::new();
|
||||
let file = f.0.join("large");
|
||||
std::fs::File::create(&file).unwrap().set_len(PNGSUITE_MAX_ARCHIVE_BYTES + 1).unwrap();
|
||||
assert!(pngsuite_bytes(&file, PNGSUITE_MAX_ARCHIVE_BYTES).unwrap_err().contains("regular file"));
|
||||
for malformed in ["", "invalid", "00 a.png", &format!("{} ../outside\n", crate::sha256::hex(b""))] {
|
||||
assert!(verify_pngsuite(&f.0, malformed).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pngsuite_download_copy_accepts_boundary_and_never_writes_excess() {
|
||||
let limit = PNGSUITE_MAX_ARCHIVE_BYTES as usize;
|
||||
for len in [limit - 1, limit, limit + 1, limit * 2] {
|
||||
let data = vec![0xa5; len];
|
||||
let mut out = Vec::new();
|
||||
let result = copy_pngsuite_download(&mut std::io::Cursor::new(&data), &mut out);
|
||||
assert_eq!(result.is_ok(), len <= limit);
|
||||
assert_eq!(out, data[..len.min(limit)]);
|
||||
}
|
||||
}
|
||||
|
||||
// The ordinary test invocation returns immediately. Only this test's child command
|
||||
// enables emission; it stays alive so cleanup must kill and reap that particular child.
|
||||
#[test]
|
||||
fn pngsuite_stream_fixture() {
|
||||
if std::env::var_os("PHOTOCRAFT_PNGSUITE_STREAM_TEST").is_some() {
|
||||
let mut out = std::io::stdout().lock();
|
||||
for _ in 0..256 {
|
||||
let _ = out.write_all(&[0xa5; 8192]);
|
||||
}
|
||||
std::thread::park();
|
||||
}
|
||||
}
|
||||
|
||||
fn stream_child() -> Child {
|
||||
Command::new(std::env::current_exe().unwrap())
|
||||
.args(["--exact", "corpus::tests::pngsuite_stream_fixture", "--nocapture"])
|
||||
.env("PHOTOCRAFT_PNGSUITE_STREAM_TEST", "1")
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pngsuite_oversize_and_io_failures_kill_and_reap_only_the_download_child() {
|
||||
struct FailedRead;
|
||||
impl Read for FailedRead {
|
||||
fn read(&mut self, _: &mut [u8]) -> std::io::Result<usize> {
|
||||
Err(std::io::Error::other("injected read failure"))
|
||||
}
|
||||
}
|
||||
struct FailedWrite;
|
||||
impl Write for FailedWrite {
|
||||
fn write(&mut self, _: &[u8]) -> std::io::Result<usize> {
|
||||
Err(std::io::Error::other("injected write failure"))
|
||||
}
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
let mut child = stream_child();
|
||||
let stdout = child.stdout.take().unwrap();
|
||||
let mut out = Vec::new();
|
||||
assert!(finish_pngsuite_download(&mut child, stdout, &mut out).unwrap_err().contains("archive exceeds"));
|
||||
assert_eq!(out.len() as u64, PNGSUITE_MAX_ARCHIVE_BYTES);
|
||||
assert!(child.try_wait().unwrap().is_some());
|
||||
|
||||
let mut child = stream_child();
|
||||
assert!(child.try_wait().unwrap().is_none());
|
||||
assert!(finish_pngsuite_download(&mut child, FailedRead, &mut Vec::new()).unwrap_err().contains("injected read failure"));
|
||||
assert!(child.try_wait().unwrap().is_some());
|
||||
|
||||
let mut child = stream_child();
|
||||
let stdout = child.stdout.take().unwrap();
|
||||
assert!(finish_pngsuite_download(&mut child, stdout, &mut FailedWrite).unwrap_err().contains("injected write failure"));
|
||||
assert!(child.try_wait().unwrap().is_some());
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn pngsuite_cache_rejects_symlinks_even_when_target_bytes_match() {
|
||||
let f = Fixture::new();
|
||||
std::fs::create_dir(f.cache()).unwrap();
|
||||
fixture_extract(Path::new("unused"), &f.cache()).unwrap();
|
||||
std::fs::write(f.0.join("target"), b"alpha").unwrap();
|
||||
std::fs::remove_file(f.cache().join("a.png")).unwrap();
|
||||
std::os::unix::fs::symlink(f.0.join("target"), f.cache().join("a.png")).unwrap();
|
||||
assert!(verify_pngsuite(&f.cache(), &fixture_manifest()).unwrap_err().contains("regular file"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,10 @@ pub const ASSET_STORE_TEMPLATE_COMMIT: &str = "f671981ee89d15526285b3cc87b4393d3
|
||||
pub const AFFINITY_SAMPLES_COMMIT: &str = "96aca6bd86bbe7a677f1cf0630bd832d23943be8";
|
||||
/// PngSuite (public domain), a fixed release archive.
|
||||
pub const PNGSUITE_URL: &str = "http://www.schaik.com/pngsuite/PngSuite-2017jul19.tgz";
|
||||
/// Fixed archive, corroborated by HTTPS records in opencodecs at 6f070029 and rivet-png at
|
||||
/// 134faad8 (corpus/checksums.json and tests/pngsuite/SHA256SUMS). Check the legacy HTTP
|
||||
/// download against this committed digest before extraction.
|
||||
pub const PNGSUITE_SHA256: &str = "0294b244c95a8342c01b00010cf34abdcabc7c6a34fd0fe1bd963917537bfdc8";
|
||||
|
||||
/// `corpus/photoshop`: 258 PSDs authored with Photoshop by the photocraft-corpus generator.
|
||||
pub const PHOTOSHOP: PinnedCorpus = PinnedCorpus {
|
||||
|
||||
@@ -579,6 +579,8 @@ mod tests {
|
||||
("el", "el.tsv"),
|
||||
("uk", "uk.tsv"),
|
||||
("nl", "nl.tsv"),
|
||||
("hu", "hu.tsv"),
|
||||
("tr", "tr.tsv"),
|
||||
("vi", "vi.tsv"),
|
||||
] {
|
||||
let catalog_path = root.join("crates/ui-egui/src/i18n").join(path);
|
||||
|
||||
Reference in New Issue
Block a user