diff --git a/Cargo.lock b/Cargo.lock index b0dc8f3..7e4ebe7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15,7 +15,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if 1.0.4", - "getrandom 0.3.3", + "getrandom 0.3.4", "once_cell", "version_check", "zerocopy", @@ -30,6 +30,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + [[package]] name = "aligned-vec" version = "0.6.4" @@ -71,11 +80,11 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -97,9 +106,9 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" [[package]] name = "arg_enum_proc_macro" @@ -124,27 +133,37 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "asphalt" version = "1.2.0" dependencies = [ "anyhow", + "assert_cmd", + "assert_fs", "bit-vec", "blake3", "bytes", "clap", "clap-verbosity-flag", - "dashmap", "dotenvy", "env_logger", "fs-err", - "futures", "globset", "image", "indicatif", "indicatif-log-bridge", "insta", "log", + "predicates", "rbx_binary", "rbx_xml", "relative-path", @@ -154,16 +173,47 @@ dependencies = [ "schemars", "serde", "serde_json", + "thiserror 2.0.17", "tokio", - "toml 0.9.8", + "toml", "walkdir", ] +[[package]] +name = "assert_cmd" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcbb6924530aa9e0432442af08bbcafdad182db80d2e560da42a6d442535bf85" +dependencies = [ + "anstyle", + "bstr", + "libc", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + +[[package]] +name = "assert_fs" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a652f6cb1f516886fcfee5e7a5c078b9ade62cfcb889524efe5a64d682dd27a9" +dependencies = [ + "anstyle", + "doc-comment", + "globwalk", + "predicates", + "predicates-core", + "predicates-tree", + "tempfile", +] + [[package]] name = "async-compression" -version = "0.4.32" +version = "0.4.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a89bce6054c720275ac2432fbba080a66a2106a44a1b804553930ca6909f4e0" +checksum = "98ec5f6c2f8bc326c994cb9e241cc257ddaba9afa8555a43cffbb5dd86efaa37" dependencies = [ "compression-codecs", "compression-core", @@ -184,11 +234,31 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.17", + "v_frame", + "y4m", +] + [[package]] name = "av1-grain" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f3efb2ca85bc610acfa917b5aaa36f3fcbebed5b3182d7f877b02531c4b80c8" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" dependencies = [ "anyhow", "arrayvec", @@ -245,9 +315,12 @@ checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "bitstream-io" -version = "2.6.0" +version = "4.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" +checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757" +dependencies = [ + "core2", +] [[package]] name = "blake3" @@ -269,20 +342,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" dependencies = [ "memchr", + "regex-automata", "serde", ] [[package]] name = "built" -version = "0.7.7" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" +checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "bytemuck" @@ -304,15 +378,15 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "cc" -version = "1.2.41" +version = "1.2.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" +checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" dependencies = [ "find-msvc-tools", "jobserver", @@ -320,16 +394,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "cfg-expr" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" -dependencies = [ - "smallvec", - "target-lexicon", -] - [[package]] name = "cfg-if" version = "0.1.10" @@ -342,17 +406,11 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - [[package]] name = "clap" -version = "4.5.50" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2cfd7bf8a6017ddaa4e32ffe7403d547790db06bd171c1c53926faab501623" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" dependencies = [ "clap_builder", "clap_derive", @@ -370,9 +428,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.50" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a4c05b9e80c5ccd3a7ef080ad7b6ba7d6fc00a985b8b157197075677c82c7a0" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" dependencies = [ "anstream", "anstyle", @@ -412,9 +470,9 @@ checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "compression-codecs" -version = "0.4.31" +version = "0.4.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef8a506ec4b81c460798f572caead636d57d3d7e940f998160f52bd254bf2d23" +checksum = "b0f7ac3e5b97fdce45e8922fb05cae2c37f7bbd63d30dd94821dacfd8f3f2bf2" dependencies = [ "compression-core", "flate2", @@ -423,9 +481,9 @@ dependencies = [ [[package]] name = "compression-core" -version = "0.4.29" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb" +checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" [[package]] name = "console" @@ -441,9 +499,9 @@ dependencies = [ [[package]] name = "console" -version = "0.16.1" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b430743a6eb14e9764d4260d4c0d8123087d504eeb9c48f2b2a5e810dd369df4" +checksum = "03e45a4a8926227e4197636ba97a9fc9b00477e9f4bd711395687c5f0734bec4" dependencies = [ "encode_unicode", "libc", @@ -458,6 +516,15 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" +[[package]] +name = "core2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +dependencies = [ + "memchr", +] + [[package]] name = "core_maths" version = "0.1.1" @@ -507,26 +574,18 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" -[[package]] -name = "dashmap" -version = "6.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" -dependencies = [ - "cfg-if 1.0.4", - "crossbeam-utils", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core", -] - [[package]] name = "data-url" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + [[package]] name = "dirs" version = "2.0.2" @@ -580,6 +639,12 @@ dependencies = [ "syn", ] +[[package]] +name = "doc-comment" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "780955b8b195a21ab8e4ac6b60dd1dbdcec1dc6c51c0617964b08c81785e12c9" + [[package]] name = "dotenvy" version = "0.15.7" @@ -653,11 +718,21 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "exr" -version = "1.73.0" +version = "1.74.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" dependencies = [ "bit_field", "half", @@ -668,6 +743,12 @@ dependencies = [ "zune-inflate", ] +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "fax" version = "0.2.6" @@ -699,15 +780,15 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" +checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" [[package]] name = "flate2" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", "miniz_oxide", @@ -720,10 +801,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" [[package]] -name = "fnv" -version = "1.0.7" +name = "float-cmp" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" +dependencies = [ + "num-traits", +] [[package]] name = "fontconfig-parser" @@ -759,29 +843,14 @@ dependencies = [ [[package]] name = "fs-err" -version = "3.1.3" +version = "3.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ad492b2cf1d89d568a43508ab24f98501fe03f2f31c01e1d0fe7366a71745d2" +checksum = "baf68cef89750956493a66a10f512b9e58d9db21f2a573c079c0bdf1207a54a7" dependencies = [ "autocfg", "tokio", ] -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - [[package]] name = "futures-channel" version = "0.3.31" @@ -789,7 +858,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", - "futures-sink", ] [[package]] @@ -798,34 +866,6 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "futures-sink" version = "0.3.31" @@ -844,16 +884,10 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ - "futures-channel", "futures-core", - "futures-io", - "futures-macro", - "futures-sink", "futures-task", - "memchr", "pin-project-lite", "pin-utils", - "slab", ] [[package]] @@ -863,24 +897,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if 1.0.4", - "js-sys", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "wasm-bindgen", + "wasi", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if 1.0.4", - "js-sys", "libc", "r-efi", - "wasi 0.14.7+wasi-0.2.4", - "wasm-bindgen", + "wasip2", ] [[package]] @@ -893,6 +923,16 @@ dependencies = [ "weezl", ] +[[package]] +name = "gif" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "globset" version = "0.4.18" @@ -907,11 +947,22 @@ dependencies = [ "serde", ] +[[package]] +name = "globwalk" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" +dependencies = [ + "bitflags 2.10.0", + "ignore", + "walkdir", +] + [[package]] name = "half" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e54c115d4f30f52c67202f079c5f9d8b49db4691f460fdb0b4c2e838261b2ba5" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "cfg-if 1.0.4", "crunchy", @@ -920,15 +971,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" - -[[package]] -name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heck" @@ -938,12 +983,11 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "http" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] @@ -978,9 +1022,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ "atomic-waker", "bytes", @@ -997,28 +1041,11 @@ dependencies = [ "want", ] -[[package]] -name = "hyper-rustls" -version = "0.27.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", - "webpki-roots", -] - [[package]] name = "hyper-util" -version = "0.1.17" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" dependencies = [ "base64 0.22.1", "bytes", @@ -1040,9 +1067,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", @@ -1053,9 +1080,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -1066,11 +1093,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -1081,42 +1107,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -1145,17 +1167,33 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "ignore" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + [[package]] name = "image" -version = "0.25.8" +version = "0.25.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7" +checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" dependencies = [ "bytemuck", "byteorder-lite", "color_quant", "exr", - "gif", + "gif 0.14.1", "image-webp", "moxcms", "num-traits", @@ -1165,8 +1203,8 @@ dependencies = [ "rayon", "rgb", "tiff", - "zune-core", - "zune-jpeg", + "zune-core 0.5.0", + "zune-jpeg 0.5.8", ] [[package]] @@ -1193,21 +1231,21 @@ checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" [[package]] name = "indexmap" -version = "2.11.4" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown", ] [[package]] name = "indicatif" -version = "0.18.1" +version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e0ddd45fe8e09ee1a607920b12271f8a5528a41ecaf6e1d1440d6493315b6b" +checksum = "9375e112e4b463ec1b1c6c011953545c65a30164fbab5b581df32b3abf0dcb88" dependencies = [ - "console 0.16.1", + "console 0.16.2", "portable-atomic", "unicode-width", "unit-prefix", @@ -1226,14 +1264,15 @@ dependencies = [ [[package]] name = "insta" -version = "1.43.2" +version = "1.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46fdb647ebde000f43b5b53f773c30cf9b0cb4300453208713fa38b2c70935a0" +checksum = "983e3b24350c84ab8a65151f537d67afbbf7153bb9f1110e03e9fa9b07f67a5c" dependencies = [ "console 0.15.11", "once_cell", "serde", "similar", + "tempfile", ] [[package]] @@ -1255,9 +1294,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "iri-string" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" dependencies = [ "memchr", "serde", @@ -1271,18 +1310,18 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" -version = "0.12.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ "either", ] [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010" [[package]] name = "jiff" @@ -1314,15 +1353,15 @@ version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "libc", ] [[package]] name = "js-sys" -version = "0.3.81" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" dependencies = [ "once_cell", "wasm-bindgen", @@ -1352,9 +1391,9 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" [[package]] name = "libc" -version = "0.2.177" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "libfuzzer-sys" @@ -1382,11 +1421,17 @@ dependencies = [ "libc", ] +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "lock_api" @@ -1399,9 +1444,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "loop9" @@ -1412,12 +1457,6 @@ dependencies = [ "imgref", ] -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - [[package]] name = "lz4_flex" version = "0.11.5" @@ -1468,12 +1507,6 @@ dependencies = [ "unicase", ] -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1486,20 +1519,20 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "windows-sys 0.61.2", ] [[package]] name = "moxcms" -version = "0.7.7" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c588e11a3082784af229e23e8e4ecf5bcc6fbe4f69101e0421ce8d79da7f0b40" +checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" dependencies = [ "num-traits", "pxfm", @@ -1513,12 +1546,11 @@ checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" [[package]] name = "nom" -version = "7.1.3" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" dependencies = [ "memchr", - "minimal-lexical", ] [[package]] @@ -1527,6 +1559,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + [[package]] name = "num-bigint" version = "0.4.6" @@ -1624,6 +1662,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1682,9 +1726,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" +checksum = "f59e70c4aef1e55797c2e8fd94a4f2a973fc972cfde0e0b05f683667b0cd39dd" [[package]] name = "portable-atomic-util" @@ -1697,9 +1741,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] @@ -1713,6 +1757,36 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "predicates" +version = "3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +dependencies = [ + "anstyle", + "difflib", + "float-cmp 0.10.0", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" + +[[package]] +name = "predicates-tree" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "proc-macro2" version = "1.0.103" @@ -1743,9 +1817,9 @@ dependencies = [ [[package]] name = "pxfm" -version = "0.1.25" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84" +checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" dependencies = [ "num-traits", ] @@ -1765,66 +1839,11 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2", - "thiserror 2.0.17", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" -dependencies = [ - "bytes", - "getrandom 0.3.3", - "lru-slab", - "rand 0.9.2", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.17", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.60.2", -] - [[package]] name = "quote" -version = "1.0.41" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -1891,18 +1910,20 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", ] [[package]] name = "rav1e" -version = "0.7.1" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" dependencies = [ + "aligned-vec", "arbitrary", "arg_enum_proc_macro", "arrayvec", + "av-scenechange", "av1-grain", "bitstream-io", "built", @@ -1917,23 +1938,21 @@ dependencies = [ "noop_proc_macro", "num-derive", "num-traits", - "once_cell", "paste", "profiling", - "rand 0.8.5", - "rand_chacha 0.3.1", + "rand 0.9.2", + "rand_chacha 0.9.0", "simd_helpers", - "system-deps", - "thiserror 1.0.69", + "thiserror 2.0.17", "v_frame", "wasm-bindgen", ] [[package]] name = "ravif" -version = "0.11.20" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5825c26fddd16ab9f515930d49028a630efec172e903483c94796cfe31893e6b" +checksum = "ef69c1990ceef18a116855938e74793a5f7496ee907562bd0857b6ac734ab285" dependencies = [ "avif-serialize", "imgref", @@ -1966,9 +1985,9 @@ dependencies = [ [[package]] name = "rbx_binary" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d419f67c8012bf83569086e1208c541478b3b8e4f523deaa0b80d723fb5ef22" +checksum = "95e2b4a187679aa3d169ed50ed5eedbf26383459fec83bf1232c2934b35b24de" dependencies = [ "ahash", "log", @@ -1984,9 +2003,9 @@ dependencies = [ [[package]] name = "rbx_dom_weak" -version = "4.0.0" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc74878a4a801afc8014b14ede4b38015a13de5d29ab0095d5ed284a744253f6" +checksum = "a7a5c48c2605913fbb1986bceb3e18ef9f12eadedb7edd62bf9fb03447b57c46" dependencies = [ "ahash", "rbx_types", @@ -1996,9 +2015,9 @@ dependencies = [ [[package]] name = "rbx_reflection" -version = "6.0.0" +version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "565dd3430991f35443fa6d23cc239fade2110c5089deb6bae5de77c400df4fd2" +checksum = "84f635e79d5d710c82e9049faa57d32945e76a6b041280dc6274f732c0dd78dc" dependencies = [ "rbx_types", "serde", @@ -2007,9 +2026,9 @@ dependencies = [ [[package]] name = "rbx_reflection_database" -version = "2.0.0+roblox-694" +version = "2.0.2+roblox-700" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "844ceb61f23bad59b06d7299b69ff276579316eafa9857981da3012a6223f663" +checksum = "de2753b896d08d74316d8b89fbeb2470ebd3986404ebba82fa85fcc0330955cf" dependencies = [ "dirs 5.0.1", "log", @@ -2020,9 +2039,9 @@ dependencies = [ [[package]] name = "rbx_types" -version = "3.0.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03220ffce2bd06ad04f77a003cb807f2e5b2a18e97623066a5ac735a978398af" +checksum = "de3b89eefdd71f5e2a25543b1ead9f6ea2ed3fdfba8b397cbdb0de053eb2463e" dependencies = [ "base64 0.13.1", "bitflags 1.3.2", @@ -2035,9 +2054,9 @@ dependencies = [ [[package]] name = "rbx_xml" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be6c302cefe9c92ed09bcbb075cd24379271de135b0af331409a64c2ea3646ee" +checksum = "e0cbaf53b44c9cc0fad1e5dc8ac63fb32fa0ecaa26d32b269cebe4dca8b7b4de" dependencies = [ "ahash", "base64 0.13.1", @@ -2128,11 +2147,10 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.24" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +checksum = "04e9018c9d814e5f30cc16a0f03271aeab3571e609612d9fe78c1aa8d11c2f62" dependencies = [ - "async-compression", "base64 0.22.1", "bytes", "futures-core", @@ -2141,23 +2159,14 @@ dependencies = [ "http-body", "http-body-util", "hyper", - "hyper-rustls", "hyper-util", "js-sys", "log", "mime_guess", "percent-encoding", "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-rustls", - "tokio-util", "tower", "tower-http", "tower-service", @@ -2165,7 +2174,6 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots", ] [[package]] @@ -2174,7 +2182,7 @@ version = "0.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8928798c0a55e03c9ca6c4c6846f76377427d2c1e1f7e6de3c06ae57942df43" dependencies = [ - "gif", + "gif 0.13.3", "image-webp", "log", "pico-args", @@ -2182,7 +2190,7 @@ dependencies = [ "svgtypes", "tiny-skia", "usvg", - "zune-jpeg", + "zune-jpeg 0.4.21", ] [[package]] @@ -2194,20 +2202,6 @@ dependencies = [ "bytemuck", ] -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if 1.0.4", - "getrandom 0.2.16", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - [[package]] name = "rmp" version = "0.8.14" @@ -2248,44 +2242,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" [[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] -name = "rustls" -version = "0.23.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" -dependencies = [ - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pki-types" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" -dependencies = [ - "web-time", - "zeroize", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.7" +name = "rustix" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", ] [[package]] @@ -2312,12 +2278,6 @@ dependencies = [ "unicode-script", ] -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - [[package]] name = "same-file" version = "1.0.6" @@ -2329,9 +2289,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.0.4" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" dependencies = [ "dyn-clone", "ref-cast", @@ -2342,9 +2302,9 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "1.0.4" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d020396d1d138dc19f1165df7545479dcd58d93810dc5d646a16e55abefa80" +checksum = "4908ad288c5035a8eb12cfdf0d49270def0a268ee162b75eeee0f85d155a7c45" dependencies = [ "proc-macro2", "quote", @@ -2401,47 +2361,26 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_spanned" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" dependencies = [ "serde_core", ] -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - [[package]] name = "shlex" version = "1.3.0" @@ -2450,18 +2389,18 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" dependencies = [ "libc", ] [[package]] name = "simd-adler32" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" [[package]] name = "simd_helpers" @@ -2493,12 +2432,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" -[[package]] -name = "slab" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" - [[package]] name = "slotmap" version = "1.0.7" @@ -2536,7 +2469,7 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" dependencies = [ - "float-cmp", + "float-cmp 0.9.0", ] [[package]] @@ -2545,12 +2478,6 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - [[package]] name = "svgtypes" version = "0.15.3" @@ -2563,9 +2490,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.108" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -2593,23 +2520,23 @@ dependencies = [ ] [[package]] -name = "system-deps" -version = "6.2.2" +name = "tempfile" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ - "cfg-expr", - "heck", - "pkg-config", - "toml 0.8.23", - "version-compare", + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", ] [[package]] -name = "target-lexicon" -version = "0.12.16" +name = "termtree" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] name = "thiserror" @@ -2662,7 +2589,7 @@ dependencies = [ "half", "quick-error", "weezl", - "zune-jpeg", + "zune-jpeg 0.4.21", ] [[package]] @@ -2693,9 +2620,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -2744,21 +2671,11 @@ dependencies = [ "syn", ] -[[package]] -name = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] - [[package]] name = "tokio-util" -version = "0.7.16" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ "bytes", "futures-core", @@ -2769,26 +2686,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "toml_edit", -] - -[[package]] -name = "toml" -version = "0.9.8" +version = "0.9.10+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48" dependencies = [ "indexmap", "serde_core", - "serde_spanned 1.0.3", - "toml_datetime 0.7.3", + "serde_spanned", + "toml_datetime", "toml_parser", "toml_writer", "winnow", @@ -2796,49 +2701,27 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.11" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_datetime" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ "serde_core", ] -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap", - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "winnow", -] - [[package]] name = "toml_parser" -version = "1.0.4" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" dependencies = [ "winnow", ] [[package]] name = "toml_writer" -version = "1.0.4" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" [[package]] name = "tower" @@ -2857,17 +2740,22 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ + "async-compression", "bitflags 2.10.0", "bytes", + "futures-core", "futures-util", "http", "http-body", + "http-body-util", "iri-string", "pin-project-lite", + "tokio", + "tokio-util", "tower", "tower-layer", "tower-service", @@ -2887,9 +2775,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-core", @@ -2897,9 +2785,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", ] @@ -2951,9 +2839,9 @@ checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" [[package]] name = "unicode-ident" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-properties" @@ -2981,15 +2869,9 @@ checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "unit-prefix" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "323402cff2dd658f39ca17c789b502021b3f18707c91cdf22e3838e1b4023817" - -[[package]] -name = "untrusted" -version = "0.9.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" [[package]] name = "url" @@ -3066,18 +2948,21 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "version-compare" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" - [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -3103,15 +2988,6 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasi" -version = "0.14.7+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" -dependencies = [ - "wasip2", -] - [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" @@ -3123,9 +2999,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.104" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" dependencies = [ "cfg-if 1.0.4", "once_cell", @@ -3134,25 +3010,11 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - [[package]] name = "wasm-bindgen-futures" -version = "0.4.54" +version = "0.4.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" dependencies = [ "cfg-if 1.0.4", "js-sys", @@ -3163,9 +3025,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.104" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3173,31 +3035,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.104" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.104" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.81" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" dependencies = [ "js-sys", "wasm-bindgen", @@ -3213,20 +3075,11 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki-roots" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "weezl" -version = "0.1.10" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" [[package]] name = "winapi" @@ -3274,15 +3127,6 @@ dependencies = [ "windows-targets 0.48.5", ] -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.59.0" @@ -3498,12 +3342,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" -dependencies = [ - "memchr", -] +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" [[package]] name = "winreg" @@ -3522,15 +3363,15 @@ checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "xml-rs" -version = "0.8.27" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" [[package]] name = "xmlwriter" @@ -3539,12 +3380,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" [[package]] -name = "yoke" +name = "y4m" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -3552,9 +3398,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", @@ -3564,18 +3410,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" dependencies = [ "proc-macro2", "quote", @@ -3603,17 +3449,11 @@ dependencies = [ "synstructure", ] -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" - [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", @@ -3622,9 +3462,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -3633,15 +3473,21 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "zmij" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317f17ff091ac4515f17cc7a190d2769a8c9a96d227de5d64b500b01cda8f2cd" + [[package]] name = "zstd" version = "0.13.3" @@ -3676,6 +3522,12 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" +[[package]] +name = "zune-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "111f7d9820f05fd715df3144e254d6fc02ee4088b0644c0ffd0efc9e6d9d2773" + [[package]] name = "zune-inflate" version = "0.2.54" @@ -3691,5 +3543,14 @@ version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" dependencies = [ - "zune-core", + "zune-core 0.4.12", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35aee689668bf9bd6f6f3a6c60bb29ba1244b3b43adfd50edd554a371da37d5" +dependencies = [ + "zune-core 0.5.0", ] diff --git a/Cargo.toml b/Cargo.toml index 2c1627b..af24954 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,41 +12,39 @@ license = "MIT" anyhow = "1.0.100" bit-vec = "0.8" blake3 = "1.8.2" -bytes = "1.10.1" -clap = { version = "4.5.50", features = ["derive"] } +bytes = "1.11.0" +clap = { version = "4.5.53", features = ["derive", "env"] } clap-verbosity-flag = "3.0.4" -dashmap = "6.1.0" dotenvy = "0.15.7" env_logger = "0.11.8" -fs-err = { version = "3.1.3", features = ["tokio"] } -futures = "0.3.31" +fs-err = { version = "3.2.2", features = ["tokio"] } globset = { version = "0.4.18", features = ["serde1"] } -image = "0.25.8" -indicatif = "0.18.1" +image = "0.25.9" +indicatif = "0.18.3" indicatif-log-bridge = "0.2.3" -log = "0.4.28" -rbx_binary = { version = "2.0.0", features = ["serde"] } -rbx_xml = "2.0.0" +log = "0.4.29" +rbx_binary = { version = "2.0.1", features = ["serde"] } +rbx_xml = "2.0.1" relative-path = { version = "2.0.1", features = ["serde"] } -reqwest = { version = "0.12.24", default-features = false, features = [ +reqwest = { version = "0.13.1", default-features = false, features = [ "gzip", "multipart", - "rustls-tls", ] } resvg = "0.45.1" roblox_install = "1.0.0" -schemars = "1.0.4" +schemars = "1.2.0" serde = { version = "1.0.228", features = ["derive"] } -serde_json = "1.0.145" +serde_json = "1.0.148" +thiserror = "2.0.17" tokio = { version = "1.48.0", features = ["full"] } -toml = "0.9.8" +toml = "0.9.10" walkdir = "2.5.0" [dev-dependencies] -insta = { version = "1.43.2", features = ["yaml"] } - -[features] -mock_cloud = [] +assert_cmd = "2.1.1" +assert_fs = "1.1.3" +insta = { version = "1.45.1", features = ["yaml"] } +predicates = "3.1.3" [profile.dev.package] insta.opt-level = 3 diff --git a/schema.json b/schema.json index e0dd6df..b1eb540 100644 --- a/schema.json +++ b/schema.json @@ -113,11 +113,6 @@ "description": "A glob pattern to match files to upload", "type": "string" }, - "warn_each_duplicate": { - "description": "Emit a warning each time a duplicate file is found", - "type": "boolean", - "default": true - }, "web": { "description": "A map of paths relative to the input path to existing assets on Roblox", "type": "object", diff --git a/src/asset.rs b/src/asset.rs index c2de6f8..4e60b78 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -1,103 +1,125 @@ -use crate::util::{alpha_bleed::alpha_bleed, svg::svg_to_png}; -use anyhow::{Context, bail}; +use crate::{ + config::WebAsset, + lockfile::LockfileEntry, + util::{alpha_bleed::alpha_bleed, svg::svg_to_png}, +}; +use anyhow::Context; use blake3::Hasher; use bytes::Bytes; use image::DynamicImage; use relative_path::RelativePathBuf; -use resvg::usvg::fontdb::Database; +use resvg::usvg::fontdb::{self}; use serde::Serialize; -use std::{io::Cursor, sync::Arc}; +use std::{ffi::OsStr, fmt, io::Cursor, sync::Arc}; +use tokio::task::spawn_blocking; + +type AssetCtor = fn(&[u8]) -> anyhow::Result; + +const SUPPORTED_EXTENSIONS: &[(&str, AssetCtor)] = &[ + ("mp3", |_| Ok(AssetType::Audio(AudioType::Mp3))), + ("ogg", |_| Ok(AssetType::Audio(AudioType::Ogg))), + ("flac", |_| Ok(AssetType::Audio(AudioType::Flac))), + ("wav", |_| Ok(AssetType::Audio(AudioType::Wav))), + ("png", |_| Ok(AssetType::Image(ImageType::Png))), + ("svg", |_| Ok(AssetType::Image(ImageType::Png))), + ("jpg", |_| Ok(AssetType::Image(ImageType::Jpg))), + ("jpeg", |_| Ok(AssetType::Image(ImageType::Jpg))), + ("bmp", |_| Ok(AssetType::Image(ImageType::Bmp))), + ("tga", |_| Ok(AssetType::Image(ImageType::Tga))), + ("fbx", |_| Ok(AssetType::Model(ModelType::Fbx))), + ("gltf", |_| Ok(AssetType::Model(ModelType::GltfJson))), + ("glb", |_| Ok(AssetType::Model(ModelType::GltfBinary))), + ("rbxm", |data| { + let format = RobloxModelFormat::Binary; + if is_animation(data, &format)? { + Ok(AssetType::Animation) + } else { + Ok(AssetType::Model(ModelType::Roblox)) + } + }), + ("rbxmx", |data| { + let format = RobloxModelFormat::Xml; + if is_animation(data, &format)? { + Ok(AssetType::Animation) + } else { + Ok(AssetType::Model(ModelType::Roblox)) + } + }), + ("mp4", |_| Ok(AssetType::Video(VideoType::Mp4))), + ("mov", |_| Ok(AssetType::Video(VideoType::Mov))), +]; + +pub fn is_supported_extension(ext: &OsStr) -> bool { + SUPPORTED_EXTENSIONS.iter().any(|(e, _)| *e == ext) +} pub struct Asset { /// Relative to Input prefix pub path: RelativePathBuf, pub data: Bytes, pub ty: AssetType, - processed: bool, pub ext: String, /// The hash before processing pub hash: String, } impl Asset { - pub fn new(path: RelativePathBuf, data: Vec) -> anyhow::Result { - let ext = path + pub async fn new( + path: RelativePathBuf, + data: Vec, + font_db: Arc, + bleed: bool, + ) -> anyhow::Result { + let mut ext = path .extension() .context("File has no extension")? .to_string(); - let ty = match ext.as_str() { - "mp3" => AssetType::Audio(AudioType::Mp3), - "ogg" => AssetType::Audio(AudioType::Ogg), - "flac" => AssetType::Audio(AudioType::Flac), - "wav" => AssetType::Audio(AudioType::Wav), - "png" | "svg" => AssetType::Image(ImageType::Png), - "jpg" | "jpeg" => AssetType::Image(ImageType::Jpg), - "bmp" => AssetType::Image(ImageType::Bmp), - "tga" => AssetType::Image(ImageType::Tga), - "fbx" => AssetType::Model(ModelType::Fbx), - "gltf" => AssetType::Model(ModelType::GltfJson), - "glb" => AssetType::Model(ModelType::GltfBinary), - "rbxm" | "rbxmx" => { - let format = if ext == "rbxm" { - RobloxModelFormat::Binary - } else { - RobloxModelFormat::Xml - }; - - if is_animation(&data, &format)? { - AssetType::Animation - } else { - AssetType::Model(ModelType::Roblox) + let ty = SUPPORTED_EXTENSIONS + .iter() + .find(|(e, _)| *e == ext) + .map(|(_, func)| func(&data)) + .context("Unknown file type")??; + + let (data, hash, ext) = spawn_blocking({ + let font_db = font_db.clone(); + move || { + let mut data = Bytes::from(data); + + let mut hasher = Hasher::new(); + hasher.update(&data); + let hash = hasher.finalize().to_string(); + + if ext == "svg" { + data = svg_to_png(&data, font_db)?.into(); + ext = "png".to_string(); } - } - "mp4" => AssetType::Video(VideoType::Mp4), - "mov" => AssetType::Video(VideoType::Mov), - _ => bail!("Unknown extension .{ext}"), - }; - let data = Bytes::from(data); + if matches!(ty, AssetType::Image(ImageType::Png)) && bleed { + let mut image: DynamicImage = image::load_from_memory(&data)?; + alpha_bleed(&mut image); - let mut hasher = Hasher::new(); - hasher.update(&data); - let hash = hasher.finalize().to_string(); + let mut writer = Cursor::new(Vec::new()); + image.write_to(&mut writer, image::ImageFormat::Png)?; + data = Bytes::from(writer.into_inner()); + } + + anyhow::Ok((data, hash, ext)) + } + }) + .await??; Ok(Self { path, data, ty, - processed: false, ext, hash, }) } - - pub async fn process(&mut self, font_db: Arc, bleed: bool) -> anyhow::Result<()> { - if self.processed { - bail!("Asset has already been processed"); - } - - if self.ext == "svg" { - self.data = svg_to_png(&self.data, font_db.clone()).await?.into(); - self.ext = "png".to_string(); - } - - if matches!(self.ty, AssetType::Image(ImageType::Png)) && bleed { - let mut image: DynamicImage = image::load_from_memory(&self.data)?; - alpha_bleed(&mut image); - - let mut writer = Cursor::new(Vec::new()); - image.write_to(&mut writer, image::ImageFormat::Png)?; - self.data = Bytes::from(writer.into_inner()); - } - - self.processed = true; - - Ok(()) - } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub enum AssetType { Model(ModelType), Animation, @@ -153,7 +175,7 @@ impl Serialize for AssetType { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub enum AudioType { Mp3, Ogg, @@ -161,7 +183,7 @@ pub enum AudioType { Wav, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub enum ImageType { Png, Jpg, @@ -169,7 +191,7 @@ pub enum ImageType { Tga, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub enum ModelType { Fbx, GltfJson, @@ -177,7 +199,7 @@ pub enum ModelType { Roblox, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub enum VideoType { Mp4, Mov, @@ -204,3 +226,30 @@ pub enum RobloxModelFormat { Binary, Xml, } + +#[derive(Debug, Clone)] +pub enum AssetRef { + Cloud(u64), + Studio(String), +} + +impl fmt::Display for AssetRef { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + AssetRef::Cloud(id) => write!(f, "rbxassetid://{id}"), + AssetRef::Studio(name) => write!(f, "rbxasset://{name}"), + } + } +} + +impl From for AssetRef { + fn from(value: WebAsset) -> Self { + AssetRef::Cloud(value.id) + } +} + +impl From<&LockfileEntry> for AssetRef { + fn from(value: &LockfileEntry) -> Self { + AssetRef::Cloud(value.asset_id) + } +} diff --git a/src/auth.rs b/src/auth.rs deleted file mode 100644 index ea7245d..0000000 --- a/src/auth.rs +++ /dev/null @@ -1,28 +0,0 @@ -use anyhow::bail; -use std::env; - -pub struct Auth { - pub api_key: Option, -} - -impl Auth { - pub fn new(arg_key: Option, auth_required: bool) -> anyhow::Result { - let env_key = env::var("ASPHALT_API_KEY").ok(); - - let api_key = match arg_key.or(env_key) { - Some(key) => Some(key), - None if auth_required => { - bail!(err_str("API key")) - } - None => None, - }; - - Ok(Self { api_key }) - } -} - -fn err_str(ty: &str) -> String { - format!( - "A {ty} is required to use Asphalt. See the README for more information:\nhttps://github.com/jackTabsCode/asphalt?tab=readme-ov-file#authentication", - ) -} diff --git a/src/cli.rs b/src/cli.rs index 2b62f61..3f12aaf 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,5 +1,5 @@ use crate::config::CreatorType; -use clap::{Args, Parser, Subcommand, ValueEnum}; +use clap::{Args, Parser, Subcommand}; use clap_verbosity_flag::{InfoLevel, Verbosity}; #[derive(Parser)] @@ -32,33 +32,47 @@ pub enum Commands { GenerateConfigSchema, } -#[derive(ValueEnum, Clone, Copy)] +#[derive(Subcommand, Clone, Copy)] pub enum SyncTarget { - Cloud, + /// Upload assets to Roblox cloud. + Cloud { + /// Error if assets would be uploaded. + #[arg(long)] + dry_run: bool, + }, + /// Write assets to the Roblox Studio content folder. Studio, + /// Write assets to the .asphalt-debug folder. Debug, } +impl SyncTarget { + pub fn write_on_sync(&self) -> bool { + matches!(self, SyncTarget::Cloud { dry_run: false }) + } +} + #[derive(Args, Clone)] pub struct SyncArgs { /// Your Open Cloud API key. - /// Can also be set with the ASPHALT_API_KEY environment variable. - #[arg(short, long)] + #[arg(short, long, env = "ASPHALT_API_KEY")] pub api_key: Option, /// Where Asphalt should sync assets to. - #[arg(short, long, default_value = "cloud")] - pub target: SyncTarget, - - /// Skip asset syncing and only display what assets will be synced. - #[arg(long)] - pub dry_run: bool, + #[command(subcommand)] + target: Option, /// Provides Roblox with the amount of Robux that you are willing to spend on each non-free asset upload. #[arg(long)] pub expected_price: Option, } +impl SyncArgs { + pub fn target(&self) -> SyncTarget { + self.target.unwrap_or(SyncTarget::Cloud { dry_run: false }) + } +} + #[derive(Args)] pub struct UploadArgs { /// The file to upload. diff --git a/src/config.rs b/src/config.rs index 224c128..fe18b8f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -18,6 +18,8 @@ pub struct Config { pub inputs: HashMap, } +pub type InputMap = HashMap; + pub const FILE_NAME: &str = "asphalt.toml"; impl Config { @@ -73,7 +75,8 @@ fn default_true() -> bool { pub struct Input { /// A glob pattern to match files to upload #[schemars(with = "String")] - pub path: Glob, + #[serde(rename = "path")] + pub include: Glob, /// The directory path to output the generated code pub output_path: PathBuf, @@ -85,10 +88,6 @@ pub struct Input { #[serde(default)] #[schemars(with = "HashMap")] pub web: HashMap, - - /// Emit a warning each time a duplicate file is found - #[serde(default = "default_true")] - pub warn_each_duplicate: bool, } /// An asset that exists on Roblox diff --git a/src/lockfile.rs b/src/lockfile.rs index eb28a09..e5c8aaf 100644 --- a/src/lockfile.rs +++ b/src/lockfile.rs @@ -1,4 +1,4 @@ -use anyhow::{Context, Result, bail}; +use anyhow::{Context, bail}; use blake3::Hasher; use fs_err::tokio as fs; use serde::{Deserialize, Serialize}; @@ -41,7 +41,7 @@ impl Lockfile { .insert(hash.to_owned(), entry); } - pub async fn write(&self, filename: Option<&Path>) -> Result<()> { + pub async fn write(&self, filename: Option<&Path>) -> anyhow::Result<()> { let mut content = toml::to_string(self)?; content.insert_str(0, "# This file is automatically @generated by Asphalt.\n# It is not intended for manual editing.\n"); @@ -82,7 +82,7 @@ impl Default for RawLockfile { } impl RawLockfile { - pub async fn read() -> Result { + pub async fn read() -> anyhow::Result { let content = fs::read_to_string(FILE_NAME).await; let content = match content { @@ -107,7 +107,7 @@ impl RawLockfile { } } - pub async fn migrate(self, input_name: Option<&str>) -> Result { + pub async fn migrate(self, input_name: Option<&str>) -> anyhow::Result { match (self, input_name) { (Self::V2(_), _) => bail!("Your lockfile is already up to date"), (Self::V1(v1), _) => Ok(migrate_from_v1(&v1)), @@ -157,7 +157,7 @@ async fn migrate_from_v0(lockfile: &LockfileV0, input_name: &str) -> anyhow::Res Ok(new_lockfile) } -async fn read_and_hash(path: &Path) -> Result { +async fn read_and_hash(path: &Path) -> anyhow::Result { let bytes = fs::read(path).await?; let mut hasher = Hasher::new(); hasher.update(&bytes); diff --git a/src/main.rs b/src/main.rs index 6f0b40c..95c6e57 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,13 +12,11 @@ use upload::upload; use crate::config::Config; mod asset; -mod auth; mod cli; mod config; mod glob; mod lockfile; mod migrate_lockfile; -mod progress_bar; mod sync; mod upload; mod util; @@ -46,7 +44,7 @@ async fn main() -> anyhow::Result<()> { log::set_max_level(level); match args.command { - Commands::Sync(args) => sync(multi_progress, args).await, + Commands::Sync(args) => sync(args, multi_progress).await, Commands::Upload(args) => upload(args).await, Commands::MigrateLockfile(args) => migrate_lockfile(args).await, Commands::GenerateConfigSchema => generate_config_schema().await, diff --git a/src/progress_bar.rs b/src/progress_bar.rs deleted file mode 100644 index d9ec0b0..0000000 --- a/src/progress_bar.rs +++ /dev/null @@ -1,38 +0,0 @@ -use indicatif::{MultiProgress, ProgressBar as InnerProgressBar, ProgressStyle}; - -#[derive(Debug, Clone)] -pub struct ProgressBar { - inner: InnerProgressBar, -} - -impl ProgressBar { - pub fn new(mp: MultiProgress, prefix: &str, len: usize) -> Self { - let template = "{prefix:>.bold}\n[{bar:40.cyan/blue}] {pos}/{len}: {msg} ({eta})"; - - let inner = mp.add(InnerProgressBar::new(len as u64)); - - inner.set_style( - ProgressStyle::default_bar() - .template(template) - .unwrap() - .progress_chars("=>"), - ); - inner.set_prefix(prefix.to_string()); - - inner.tick(); - - Self { inner } - } - - pub fn set_msg(&self, msg: impl Into) { - self.inner.set_message(msg.into()); - } - - pub fn inc(&self, delta: u64) { - self.inner.inc(delta); - } - - pub fn finish(&self) { - self.inner.finish_and_clear(); - } -} diff --git a/src/sync/backend/cloud.rs b/src/sync/backend/cloud.rs index 88fcfe1..bc6c0cf 100644 --- a/src/sync/backend/cloud.rs +++ b/src/sync/backend/cloud.rs @@ -1,31 +1,44 @@ -use super::{BackendSyncResult, SyncBackend}; -use crate::{asset::Asset, sync::SyncState}; -use std::sync::Arc; -use tokio::time; +use super::Backend; +use crate::{ + asset::{Asset, AssetRef}, + lockfile::LockfileEntry, + sync::backend::Params, + web_api::WebApiClient, +}; +use anyhow::{Context, bail}; -pub struct CloudBackend; +pub struct Cloud { + client: WebApiClient, +} -impl SyncBackend for CloudBackend { - async fn new() -> anyhow::Result +impl Backend for Cloud { + async fn new(params: Params) -> anyhow::Result where Self: Sized, { - Ok(Self) + Ok(Self { + client: WebApiClient::new( + params + .api_key + .context("An API key is required to use the Cloud backend")?, + params.creator, + params.expected_price, + ), + }) } async fn sync( &self, - state: Arc, - _input_name: String, asset: &Asset, - ) -> anyhow::Result> { - if cfg!(feature = "mock_cloud") { - time::sleep(time::Duration::from_secs(1)).await; - return Ok(Some(BackendSyncResult::Cloud(1337))); + lockfile_entry: Option<&LockfileEntry>, + ) -> anyhow::Result> { + if let Some(lockfile_entry) = lockfile_entry { + return Ok(Some(lockfile_entry.into())); } - let asset_id = state.client.upload(asset).await?; - - Ok(Some(BackendSyncResult::Cloud(asset_id))) + match self.client.upload(asset).await { + Ok(id) => Ok(Some(AssetRef::Cloud(id))), + Err(err) => bail!("Failed to upload asset: {err:?}"), + } } } diff --git a/src/sync/backend/debug.rs b/src/sync/backend/debug.rs index fca4027..2b01520 100644 --- a/src/sync/backend/debug.rs +++ b/src/sync/backend/debug.rs @@ -1,16 +1,16 @@ -use super::{BackendSyncResult, SyncBackend}; -use crate::{asset::Asset, sync::SyncState}; +use super::{AssetRef, Backend}; +use crate::{asset::Asset, lockfile::LockfileEntry, sync::backend::Params}; use anyhow::Context; use fs_err::tokio as fs; use log::info; -use std::{env, path::PathBuf, sync::Arc}; +use std::{env, path::PathBuf}; -pub struct DebugBackend { +pub struct Debug { sync_path: PathBuf, } -impl SyncBackend for DebugBackend { - async fn new() -> anyhow::Result +impl Backend for Debug { + async fn new(_: Params) -> anyhow::Result where Self: Sized, { @@ -34,10 +34,9 @@ impl SyncBackend for DebugBackend { async fn sync( &self, - _state: Arc, - _input_name: String, asset: &Asset, - ) -> anyhow::Result> { + lockfile_entry: Option<&LockfileEntry>, + ) -> anyhow::Result> { let target_path = asset.path.to_logical_path(&self.sync_path); if let Some(parent) = target_path.parent() { @@ -50,6 +49,6 @@ impl SyncBackend for DebugBackend { .await .with_context(|| format!("Failed to write asset to {}", target_path.display()))?; - Ok(None) + Ok(lockfile_entry.map(Into::into)) } } diff --git a/src/sync/backend/mod.rs b/src/sync/backend/mod.rs index 07df72b..4c32d2a 100644 --- a/src/sync/backend/mod.rs +++ b/src/sync/backend/mod.rs @@ -1,26 +1,32 @@ -use std::sync::Arc; +use crate::{ + asset::{Asset, AssetRef}, + config, + lockfile::LockfileEntry, +}; -use super::SyncState; -use crate::asset::Asset; +mod cloud; +pub use cloud::Cloud; -pub mod cloud; -pub mod debug; -pub mod studio; +mod debug; +pub use debug::Debug; -pub enum BackendSyncResult { - Cloud(u64), - Studio(String), -} +mod studio; +pub use studio::Studio; -pub trait SyncBackend { - async fn new() -> anyhow::Result +pub trait Backend { + async fn new(params: Params) -> anyhow::Result where Self: Sized; async fn sync( &self, - state: Arc, - input_name: String, asset: &Asset, - ) -> anyhow::Result>; + lockfile_entry: Option<&LockfileEntry>, + ) -> anyhow::Result>; +} + +pub struct Params { + pub api_key: Option, + pub creator: config::Creator, + pub expected_price: Option, } diff --git a/src/sync/backend/studio.rs b/src/sync/backend/studio.rs index b7edc92..aeb24d3 100644 --- a/src/sync/backend/studio.rs +++ b/src/sync/backend/studio.rs @@ -1,22 +1,23 @@ -use super::{BackendSyncResult, SyncBackend}; +use super::{AssetRef, Backend}; use crate::{ asset::{Asset, AssetType}, - sync::SyncState, + lockfile::LockfileEntry, + sync::backend::Params, }; use anyhow::{Context, bail}; use fs_err::tokio as fs; use log::{debug, info, warn}; use relative_path::RelativePathBuf; use roblox_install::RobloxStudio; -use std::{env, path::PathBuf, sync::Arc}; +use std::{env, path::PathBuf}; -pub struct StudioBackend { +pub struct Studio { identifier: String, sync_path: PathBuf, } -impl SyncBackend for StudioBackend { - async fn new() -> anyhow::Result +impl Backend for Studio { + async fn new(_: Params) -> anyhow::Result where Self: Sized, { @@ -51,13 +52,12 @@ impl SyncBackend for StudioBackend { async fn sync( &self, - state: Arc, - input_name: String, asset: &Asset, - ) -> anyhow::Result> { + lockfile_entry: Option<&LockfileEntry>, + ) -> anyhow::Result> { if matches!(asset.ty, AssetType::Model(_) | AssetType::Animation) { - return match state.existing_lockfile.get(&input_name, &asset.hash) { - Some(entry) => Ok(Some(BackendSyncResult::Studio(format!( + return match lockfile_entry { + Some(entry) => Ok(Some(AssetRef::Studio(format!( "rbxassetid://{}", entry.asset_id )))), @@ -79,7 +79,7 @@ impl SyncBackend for StudioBackend { fs::write(&target_path, &asset.data).await?; - Ok(Some(BackendSyncResult::Studio(format!( + Ok(Some(AssetRef::Studio(format!( "rbxasset://{}/{}", self.identifier, rel_target_path )))) @@ -91,7 +91,10 @@ fn get_content_path() -> anyhow::Result { let path = PathBuf::from(var); if path.exists() { - debug!("Using environment variable content path: {path:?}"); + debug!( + "Using environment variable content path: {}", + path.display() + ); return Ok(path); } else { bail!("Content path `{}` does not exist", path.display()); @@ -101,7 +104,7 @@ fn get_content_path() -> anyhow::Result { let studio = RobloxStudio::locate()?; let path = studio.content_path(); - debug!("Using auto-detected content path: {path:?}"); + debug!("Using auto-detected content path: {}", path.display()); Ok(path.to_owned()) } diff --git a/src/sync/codegen.rs b/src/sync/codegen.rs index 0bd97fb..7962363 100644 --- a/src/sync/codegen.rs +++ b/src/sync/codegen.rs @@ -1,4 +1,4 @@ -use crate::config; +use crate::{asset::AssetRef, config}; use anyhow::bail; use relative_path::{RelativePath, RelativePathBuf}; use std::{collections::BTreeMap, path::Path}; @@ -16,14 +16,16 @@ pub enum Language { Luau, } -pub fn create_node(source: &BTreeMap, config: &config::Codegen) -> Node { +pub type NodeSource = BTreeMap; + +pub fn create_node(source: &NodeSource, config: &config::Codegen) -> Node { let mut root = Node::Table(BTreeMap::new()); for (path, value) in source { let value = if config.content { - Node::Content(value.into()) + Node::Content(value.to_string()) } else { - Node::String(value.into()) + Node::String(value.to_string()) }; match config.style { diff --git a/src/sync/collect.rs b/src/sync/collect.rs new file mode 100644 index 0000000..b02978b --- /dev/null +++ b/src/sync/collect.rs @@ -0,0 +1,196 @@ +use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; +use std::{ + collections::{HashMap, HashSet}, + path::PathBuf, +}; +use tokio::sync::mpsc::UnboundedReceiver; + +use crate::{ + asset::AssetRef, + cli::SyncTarget, + config::InputMap, + lockfile::{Lockfile, LockfileEntry}, + sync::codegen::NodeSource, +}; + +pub struct CollectResults { + pub new_lockfile: Lockfile, + pub input_sources: HashMap, + pub new_count: u64, +} + +pub async fn collect_events( + mut rx: UnboundedReceiver, + target: SyncTarget, + inputs: InputMap, + mp: MultiProgress, +) -> anyhow::Result { + let mut new_lockfile = Lockfile::default(); + + let mut input_sources: HashMap = HashMap::new(); + for (input_name, input) in inputs { + for (rel_path, web_asset) in &input.web { + input_sources + .entry(input_name.clone()) + .or_default() + .insert(rel_path.clone(), web_asset.clone().into()); + } + } + + let mut progress = Progress::new(mp, target); + + let mut seen_paths = HashSet::new(); + + while let Some(event) = rx.recv().await { + match event { + super::Event::Discovered(path) => { + if !seen_paths.contains(&path) { + progress.discovered += 1; + } + } + super::Event::InFlight(path) => { + if !seen_paths.contains(&path) { + progress.in_flight.insert(path.clone()); + } + } + super::Event::Finished { + state, + input_name, + path, + rel_path, + hash, + asset_ref, + } => { + seen_paths.insert(path.clone()); + + if let Some(asset_ref) = asset_ref { + input_sources + .entry(input_name.clone()) + .or_default() + .insert(rel_path.clone(), asset_ref.clone()); + + if let AssetRef::Cloud(id) = asset_ref { + new_lockfile.insert(&input_name, &hash, LockfileEntry { asset_id: id }); + } + } + + match state { + super::EventState::Synced { new } => { + progress.synced += 1; + if new { + progress.new += 1; + if target.write_on_sync() { + new_lockfile.write(None).await?; + } + } + } + super::EventState::Duplicate => { + progress.dupes += 1; + } + } + + progress.in_flight.remove(&path); + } + super::Event::Failed(path) => { + progress.failed += 1; + progress.in_flight.remove(&path); + } + } + + progress.update(); + } + + progress.finish(); + + Ok(CollectResults { + new_lockfile, + input_sources, + new_count: progress.new, + }) +} + +struct Progress { + inner: ProgressBar, + target: SyncTarget, + in_flight: HashSet, + discovered: u64, + synced: u64, + new: u64, + dupes: u64, + failed: u64, +} + +impl Progress { + fn get_style(finished: bool) -> ProgressStyle { + ProgressStyle::default_bar() + .template(&format!( + "{{prefix:.{prefix_color}.bold}}{bar} {{pos}}/{{len}} assets: ({{msg}})", + prefix_color = if finished { "green" } else { "cyan" }, + bar = if finished { "" } else { " [{bar:40}]" }, + )) + .unwrap() + .progress_chars("=> ") + } + + fn new(mp: MultiProgress, target: SyncTarget) -> Self { + let spinner = mp.add(ProgressBar::new_spinner()); + spinner.set_style(Progress::get_style(false)); + spinner.set_prefix("Syncing"); + spinner.enable_steady_tick(std::time::Duration::from_millis(100)); + + Self { + inner: spinner, + target, + in_flight: HashSet::new(), + discovered: 0, + synced: 0, + new: 0, + dupes: 0, + failed: 0, + } + } + + fn get_msg(&self) -> String { + let mut parts = Vec::new(); + + if self.new > 0 { + let target_msg = match self.target { + SyncTarget::Cloud { dry_run: true } => "checked", + SyncTarget::Cloud { dry_run: false } => "uploaded", + SyncTarget::Studio | SyncTarget::Debug => "written", + }; + parts.push(format!("{} {}", self.new, target_msg)); + } + let noop = self.synced - self.new; + if noop > 0 { + parts.push(format!("{} no-op", noop)); + } + if self.dupes > 0 { + parts.push(format!("{} duplicates", self.dupes)); + } + + let in_flight = self.in_flight.len(); + if in_flight > 0 { + parts.push(format!("{} processing", in_flight)); + } + + let failed = self.failed; + if failed > 0 { + parts.push(format!("{} failed", failed)); + } + + parts.join(", ") + } + + fn update(&self) { + self.inner.set_position(self.synced + self.dupes); + self.inner.set_length(self.discovered); + self.inner.set_message(self.get_msg()); + } + + fn finish(&self) { + self.inner.set_prefix("Synced"); + self.inner.set_style(Progress::get_style(true)); + self.inner.finish(); + } +} diff --git a/src/sync/mod.rs b/src/sync/mod.rs index 5c5ae09..9d8b864 100644 --- a/src/sync/mod.rs +++ b/src/sync/mod.rs @@ -1,57 +1,70 @@ use crate::{ - auth::Auth, + asset::{Asset, AssetRef}, cli::{SyncArgs, SyncTarget}, - config::{Config, Input}, - lockfile::{Lockfile, LockfileEntry, RawLockfile}, - web_api::WebApiClient, + config::Config, + lockfile::{LockfileEntry, RawLockfile}, + sync::{backend::Backend, collect::collect_events}, }; -use anyhow::{Context, Result, bail}; -use backend::BackendSyncResult; +use anyhow::{Context, bail}; +use fs_err::tokio as fs; use indicatif::MultiProgress; -use log::{info, warn}; +use log::info; use relative_path::RelativePathBuf; use resvg::usvg::fontdb; -use std::{ - collections::{BTreeMap, HashMap}, - sync::Arc, -}; -use tokio::{ - fs, - sync::mpsc::{self, Receiver, Sender}, -}; -use walk::{DuplicateFile, WalkedFile}; +use std::{path::PathBuf, sync::Arc}; +use tokio::sync::mpsc::{self}; mod backend; mod codegen; -mod perform; -mod process; +mod collect; mod walk; -pub struct SyncState { - args: SyncArgs, - - existing_lockfile: Lockfile, - result_tx: mpsc::Sender, - - multi_progress: MultiProgress, +enum TargetBackend { + Cloud(backend::Cloud), + Debug(backend::Debug), + Studio(backend::Studio), +} - font_db: Arc, +impl TargetBackend { + pub async fn sync( + &self, + asset: &Asset, + lockfile_entry: Option<&LockfileEntry>, + ) -> anyhow::Result> { + match self { + Self::Cloud(cloud_backend) => cloud_backend.sync(asset, lockfile_entry).await, + Self::Debug(debug_backend) => debug_backend.sync(asset, lockfile_entry).await, + Self::Studio(studio_backend) => studio_backend.sync(asset, lockfile_entry).await, + } + } +} - client: WebApiClient, +#[derive(Debug)] +enum Event { + Discovered(PathBuf), + InFlight(PathBuf), + Finished { + state: EventState, + input_name: String, + path: PathBuf, + rel_path: RelativePathBuf, + hash: String, + asset_ref: Option, + }, + Failed(PathBuf), } -pub async fn sync(multi_progress: MultiProgress, args: SyncArgs) -> Result<()> { - if args.dry_run && !matches!(args.target, SyncTarget::Cloud) { - bail!("A dry run doesn't make sense in this context"); - } +#[derive(Debug)] +enum EventState { + Synced { new: bool }, + Duplicate, +} +pub async fn sync(args: SyncArgs, mp: MultiProgress) -> anyhow::Result<()> { let config = Config::read().await?; - let codegen_config = config.codegen.clone(); + let target = args.target(); - let lockfile = RawLockfile::read().await?.into_lockfile()?; - - let key_required = matches!(args.target, SyncTarget::Cloud) && !args.dry_run; - let auth = Auth::new(args.api_key.clone(), key_required)?; + let existing_lockfile = RawLockfile::read().await?.into_lockfile()?; let font_db = Arc::new({ let mut db = fontdb::Database::new(); @@ -59,148 +72,55 @@ pub async fn sync(multi_progress: MultiProgress, args: SyncArgs) -> Result<()> { db }); - let (codegen_tx, codegen_rx) = mpsc::channel::(100); + let (event_tx, event_rx) = mpsc::unbounded_channel::(); - let codegen_handle = { + let collector_handle = tokio::spawn({ let inputs = config.inputs.clone(); - tokio::spawn(async move { collect_codegen_insertions(codegen_rx, inputs).await }) - }; - - let (lockfile_tx, lockfile_rx) = mpsc::channel::(100); - - let lockfile_handle = - tokio::spawn(async move { collect_lockfile_insertions(lockfile_rx).await }); - - let (result_tx, result_rx) = mpsc::channel::(100); - - let result_handle = { - let codegen_tx = codegen_tx.clone(); - let lockfile_tx = lockfile_tx.clone(); - - tokio::spawn(async move { handle_sync_results(result_rx, codegen_tx, lockfile_tx).await }) - }; - - let state = Arc::new(SyncState { - args: args.clone(), - - existing_lockfile: lockfile, - result_tx, - - multi_progress, - - font_db, - - client: WebApiClient::new(auth, config.creator, args.expected_price), + async move { collect_events(event_rx, target, inputs, mp).await } }); - let mut duplicate_assets = HashMap::>::new(); - - for (input_name, input) in &config.inputs { - let walk_results = walk::walk(state.clone(), input_name.clone(), input).await?; - - let mut new_assets = Vec::with_capacity(walk_results.len()); - let mut dupe_count = 0; - - for result in walk_results { - match result { - WalkedFile::New(asset) => { - new_assets.push(asset); - } - WalkedFile::Existing(existing) => { - if args.dry_run { - continue; - } - - if matches!(args.target, SyncTarget::Cloud) { - lockfile_tx - .send(LockfileInsertion { - input_name: input_name.clone(), - hash: existing.hash, - entry: existing.entry.clone(), - // This takes too long, and we're not really losing anything here. - write: false, - }) - .await?; - } - - codegen_tx - .send(CodegenInsertion { - input_name: input_name.clone(), - asset_path: existing.path.clone(), - asset_id: format!("rbxassetid://{}", existing.entry.asset_id), - }) - .await?; + let params = walk::Params { + target, + existing_lockfile, + font_db, + backend: { + let params = backend::Params { + api_key: args.api_key, + creator: config.creator.clone(), + expected_price: args.expected_price, + }; + match &target { + SyncTarget::Cloud { dry_run: false } => { + Some(TargetBackend::Cloud(backend::Cloud::new(params).await?)) } - WalkedFile::Duplicate(dupe) => { - if input.warn_each_duplicate { - warn!( - "Duplicate file found: {} (original at {})", - dupe.path, dupe.original_path - ); - } - - if args.dry_run { - continue; - } - - dupe_count += 1; - - duplicate_assets - .entry(input_name.clone()) - .or_default() - .push(DuplicateFile { - original_path: dupe.original_path, - path: dupe.path, - }); + SyncTarget::Cloud { dry_run: true } => None, + SyncTarget::Debug => Some(TargetBackend::Debug(backend::Debug::new(params).await?)), + SyncTarget::Studio => { + Some(TargetBackend::Studio(backend::Studio::new(params).await?)) } } - } + }, + }; - if dupe_count > 0 { - warn!("{dupe_count} duplicate files found."); - } + walk::walk(params, &config, &event_tx).await; + drop(event_tx); - if args.dry_run { - let new_len = new_assets.len(); + let results = collector_handle.await??; - if new_len > 0 { - bail!("{new_len} new assets would be synced!") - } else { - info!("No new assets would be synced."); - return Ok(()); - } + if matches!(target, SyncTarget::Cloud { dry_run: true }) { + if results.new_count > 0 { + bail!("Dry run: {} new assets would be synced", results.new_count) + } else { + info!("Dry run: No new assets would be synced"); + return Ok(()); } - - let processed_assets = - process::process(new_assets, state.clone(), input_name.clone(), input.bleed).await?; - - perform::perform(&processed_assets, state.clone(), input_name.clone()).await?; - } - - drop(state); - - result_handle.await??; - - drop(codegen_tx); - drop(lockfile_tx); - - let new_lockfile = lockfile_handle.await??; - if matches!(args.target, SyncTarget::Cloud) { - new_lockfile.write(None).await?; } - let mut inputs_to_sources = codegen_handle.await??; - - for (input_name, dupes) in duplicate_assets { - let source = inputs_to_sources.get_mut(&input_name).unwrap(); - - for dupe in dupes { - let original = source.get(&dupe.original_path).unwrap(); - source.insert(dupe.path, original.clone()); - } + if target.write_on_sync() { + results.new_lockfile.write(None).await?; } - for (input_name, source) in inputs_to_sources { + for (input_name, source) in results.input_sources { let input = config .inputs .get(&input_name) @@ -208,7 +128,7 @@ pub async fn sync(multi_progress: MultiProgress, args: SyncArgs) -> Result<()> { let mut langs_to_generate = vec![codegen::Language::Luau]; - if codegen_config.typescript { + if config.codegen.typescript { langs_to_generate.push(codegen::Language::TypeScript); } @@ -227,100 +147,3 @@ pub async fn sync(multi_progress: MultiProgress, args: SyncArgs) -> Result<()> { Ok(()) } - -pub struct SyncResult { - hash: String, - path: RelativePathBuf, - input_name: String, - backend: BackendSyncResult, -} - -async fn handle_sync_results( - mut rx: Receiver, - codegen_tx: Sender, - lockfile_tx: Sender, -) -> anyhow::Result<()> { - while let Some(result) = rx.recv().await { - if let BackendSyncResult::Cloud(asset_id) = result.backend { - lockfile_tx - .send(LockfileInsertion { - input_name: result.input_name.clone(), - hash: result.hash, - entry: LockfileEntry { asset_id }, - write: true, - }) - .await?; - - codegen_tx - .send(CodegenInsertion { - input_name: result.input_name, - asset_path: result.path, - asset_id: format!("rbxassetid://{asset_id}"), - }) - .await?; - } else if let BackendSyncResult::Studio(asset_id) = result.backend { - codegen_tx - .send(CodegenInsertion { - input_name: result.input_name, - asset_path: result.path.clone(), - asset_id, - }) - .await?; - } - } - - Ok(()) -} - -struct CodegenInsertion { - input_name: String, - asset_path: RelativePathBuf, - asset_id: String, -} - -async fn collect_codegen_insertions( - mut rx: Receiver, - inputs: HashMap, -) -> anyhow::Result>> { - let mut inputs_to_sources: HashMap> = HashMap::new(); - - for (input_name, input) in &inputs { - for (rel_path, asset) in &input.web { - let entry = inputs_to_sources.entry(input_name.clone()).or_default(); - - entry.insert(rel_path.clone(), format!("rbxassetid://{}", asset.id)); - } - } - - while let Some(insertion) = rx.recv().await { - let source = inputs_to_sources - .entry(insertion.input_name.clone()) - .or_default(); - - source.insert(insertion.asset_path, insertion.asset_id); - } - - Ok(inputs_to_sources) -} - -struct LockfileInsertion { - input_name: String, - hash: String, - entry: LockfileEntry, - write: bool, -} - -async fn collect_lockfile_insertions( - mut rx: Receiver, -) -> anyhow::Result { - let mut new_lockfile = Lockfile::default(); - - while let Some(insertion) = rx.recv().await { - new_lockfile.insert(&insertion.input_name, &insertion.hash, insertion.entry); - if insertion.write { - new_lockfile.write(None).await?; - } - } - - Ok(new_lockfile) -} diff --git a/src/sync/perform.rs b/src/sync/perform.rs deleted file mode 100644 index d072bc1..0000000 --- a/src/sync/perform.rs +++ /dev/null @@ -1,78 +0,0 @@ -use super::{ - SyncState, - backend::{SyncBackend, cloud::CloudBackend, debug::DebugBackend, studio::StudioBackend}, -}; -use crate::{asset::Asset, cli::SyncTarget, progress_bar::ProgressBar, sync::SyncResult}; -use log::warn; -use std::sync::Arc; - -pub async fn perform( - assets: &Vec, - state: Arc, - input_name: String, -) -> anyhow::Result<()> { - let backend = pick_backend(&state.args.target.clone()).await?; - - let pb = ProgressBar::new( - state.multi_progress.clone(), - &format!("Syncing input \"{input_name}\""), - assets.len(), - ); - - for asset in assets { - let input_name = input_name.clone(); - - let file_name = asset.path.to_string(); - pb.set_msg(&file_name); - - let res = match backend { - TargetBackend::Debug(ref backend) => { - backend.sync(state.clone(), input_name.clone(), asset).await - } - TargetBackend::Cloud(ref backend) => { - backend.sync(state.clone(), input_name.clone(), asset).await - } - TargetBackend::Studio(ref backend) => { - backend.sync(state.clone(), input_name.clone(), asset).await - } - }; - - match res { - Ok(Some(result)) => { - state - .result_tx - .send(SyncResult { - input_name: input_name.clone(), - hash: asset.hash.clone(), - path: asset.path.clone(), - backend: result, - }) - .await?; - } - Err(err) => { - warn!("Failed to sync asset {file_name}: {err:?}"); - } - _ => {} - }; - - pb.inc(1); - } - - pb.finish(); - - Ok(()) -} - -enum TargetBackend { - Debug(DebugBackend), - Cloud(CloudBackend), - Studio(StudioBackend), -} - -async fn pick_backend(target: &SyncTarget) -> anyhow::Result { - match target { - SyncTarget::Debug => Ok(TargetBackend::Debug(DebugBackend::new().await?)), - SyncTarget::Cloud => Ok(TargetBackend::Cloud(CloudBackend::new().await?)), - SyncTarget::Studio => Ok(TargetBackend::Studio(StudioBackend::new().await?)), - } -} diff --git a/src/sync/process.rs b/src/sync/process.rs deleted file mode 100644 index 2f5358d..0000000 --- a/src/sync/process.rs +++ /dev/null @@ -1,37 +0,0 @@ -use super::SyncState; -use crate::{asset::Asset, progress_bar::ProgressBar}; -use log::warn; -use std::sync::Arc; - -pub async fn process( - assets: Vec, - state: Arc, - input_name: String, - bleed: bool, -) -> anyhow::Result> { - let pb = ProgressBar::new( - state.multi_progress.clone(), - &format!("Processing input \"{input_name}\""), - assets.len(), - ); - - let mut processed_assets = Vec::with_capacity(assets.len()); - - for mut asset in assets { - let file_name = asset.path.to_string(); - pb.set_msg(&file_name); - - if let Err(err) = asset.process(state.font_db.clone(), bleed).await { - warn!("Skipping file {file_name} because it failed processing: {err:?}"); - continue; - } - - pb.inc(1); - - processed_assets.push(asset); - } - - pb.finish(); - - Ok(processed_assets) -} diff --git a/src/sync/walk.rs b/src/sync/walk.rs index e30be76..205700f 100644 --- a/src/sync/walk.rs +++ b/src/sync/walk.rs @@ -1,135 +1,171 @@ -use super::SyncState; use crate::{ - asset::Asset, cli::SyncTarget, config::Input, lockfile::LockfileEntry, - progress_bar::ProgressBar, + asset::{self, Asset}, + cli::SyncTarget, + config::Config, + lockfile::Lockfile, + sync::TargetBackend, }; use anyhow::Context; -use dashmap::DashMap; use fs_err::tokio as fs; -use futures::stream::{self, StreamExt}; -use log::debug; -use relative_path::{PathExt, RelativePathBuf}; -use std::{path::PathBuf, sync::Arc}; -use tokio::task::spawn_blocking; +use log::{debug, warn}; +use relative_path::PathExt; +use resvg::usvg::fontdb; +use std::{ + collections::{ + HashMap, + hash_map::{self}, + }, + path::{Path, PathBuf}, + sync::Arc, +}; +use tokio::{ + sync::{Mutex, Semaphore, mpsc::UnboundedSender}, + task::JoinSet, +}; use walkdir::WalkDir; -#[derive(Clone)] -struct WalkCtx { - state: Arc, +pub struct Params { + pub target: SyncTarget, + pub existing_lockfile: Lockfile, + pub font_db: Arc, + pub backend: Option, +} + +struct InputState { + params: Arc, input_name: String, input_prefix: PathBuf, - seen_hashes: Arc>, - pb: ProgressBar, + seen_hashes: Arc>>, + bleed: bool, } -pub async fn walk( - state: Arc, - input_name: String, - input: &Input, -) -> anyhow::Result> { - let input_prefix = input.path.get_prefix(); - - let entries = WalkDir::new(&input_prefix) - .into_iter() - .filter_map(Result::ok) - .filter(|entry| input.path.is_match(entry.path()) && entry.file_type().is_file()) - .map(|entry| entry.path().to_path_buf()) - .collect::>(); - - let total_files = entries.len(); - let pb = ProgressBar::new( - state.multi_progress.clone(), - &format!("Reading input \"{input_name}\""), - total_files, - ); - - let seen_hashes = Arc::new(DashMap::::with_capacity(total_files)); - - let ctx = WalkCtx { - state, - input_name, - seen_hashes, - pb, - input_prefix, - }; +pub async fn walk(params: Params, config: &Config, tx: &UnboundedSender) { + let params = Arc::new(params); + + for (input_name, input) in &config.inputs { + let state = Arc::new(InputState { + params: params.clone(), + input_name: input_name.clone(), + input_prefix: input.include.get_prefix(), + seen_hashes: Arc::new(Mutex::new(HashMap::new())), + bleed: input.bleed, + }); + + let mut join_set = JoinSet::new(); + let semaphore = Arc::new(Semaphore::new(50)); + + for entry in WalkDir::new(input.include.get_prefix()) + .into_iter() + .filter_entry(|entry| { + let path = entry.path(); + path == input.include.get_prefix() || input.include.is_match(path) + }) + { + let Ok(entry) = entry else { continue }; + + let path = entry.into_path(); + if !path.is_file() { + continue; + } + let Some(ext) = path.extension() else { + continue; + }; + if !asset::is_supported_extension(ext) { + continue; + } - let results = stream::iter(entries) - .map(|path| { - let ctx = ctx.clone(); + let state = state.clone(); + let semaphore = semaphore.clone(); + let tx = tx.clone(); - async move { - let result = walk_file(&ctx, path.clone()).await; + tx.send(super::Event::Discovered(path.clone())).unwrap(); - ctx.pb.inc(1); + join_set.spawn(async move { + let _permit = semaphore.acquire_owned().await.unwrap(); - match result { - Ok(res) => Some(res), - Err(err) => { - debug!("Skipping file {}: {:?}", path.display(), err); - None - } - } - } - }) - .buffer_unordered(100) - .filter_map(|result| async move { result }) - .collect::>() - .await; + tx.send(super::Event::InFlight(path.clone())).unwrap(); - ctx.pb.finish(); + if let Err(e) = process_entry(state.clone(), &path, &tx).await { + warn!("Failed to process file {}: {e:?}", path.display()); + tx.send(super::Event::Failed(path.clone())).unwrap(); + } + }); + } - Ok(results) + while join_set.join_next().await.is_some() {} + } } -pub struct ExistingFile { - pub path: RelativePathBuf, - pub hash: String, - pub entry: LockfileEntry, -} +async fn process_entry( + state: Arc, + path: &Path, + tx: &UnboundedSender, +) -> anyhow::Result<()> { + debug!("Handling entry: {}", path.display()); -pub struct DuplicateFile { - pub path: RelativePathBuf, - pub original_path: RelativePathBuf, -} + let rel_path = path.relative_to(&state.input_prefix)?; -pub enum WalkedFile { - New(Asset), - Existing(ExistingFile), - Duplicate(DuplicateFile), -} + let data = fs::read(path).await?; -async fn walk_file(ctx: &WalkCtx, path: PathBuf) -> anyhow::Result { - let data = fs::read(&path).await?; - let rel_path = path.relative_to(&ctx.input_prefix)?; + let asset = Asset::new( + rel_path.clone(), + data, + state.params.font_db.clone(), + state.bleed, + ) + .await + .context("Failed to create asset")?; - let rel_path_clone = rel_path.clone(); - let asset = spawn_blocking(move || Asset::new(rel_path_clone, data)) - .await - .context("Failed to create asset")??; + let lockfile_entry = state + .params + .existing_lockfile + .get(&state.input_name, &asset.hash); - if let Some(seen_path) = ctx.seen_hashes.get(&asset.hash) { - let rel_seen_path = seen_path.relative_to(&ctx.input_prefix)?; + { + let mut seen_hashes = state.seen_hashes.lock().await; - return Ok(WalkedFile::Duplicate(DuplicateFile { - path: rel_path.clone(), - original_path: rel_seen_path, - })); - } + match seen_hashes.entry(asset.hash.clone()) { + hash_map::Entry::Occupied(entry) => { + let seen_path = entry.get(); + let rel_seen_path = seen_path.relative_to(&state.input_prefix)?; - ctx.seen_hashes.insert(asset.hash.clone(), path.clone()); + debug!("Duplicate asset found: {} -> {}", rel_path, rel_seen_path); - let entry = ctx - .state - .existing_lockfile - .get(&ctx.input_name, &asset.hash); - - match (entry, &ctx.state.args.target) { - (Some(entry), SyncTarget::Cloud) => Ok(WalkedFile::Existing(ExistingFile { - path: rel_path, - hash: asset.hash.clone(), - entry: entry.clone(), - })), - (Some(_), SyncTarget::Studio | SyncTarget::Debug) => Ok(WalkedFile::New(asset)), - (None, _) => Ok(WalkedFile::New(asset)), + let event = super::Event::Finished { + state: super::EventState::Duplicate, + input_name: state.input_name.clone(), + path: path.into(), + rel_path: rel_path.clone(), + asset_ref: lockfile_entry.map(Into::into), + hash: asset.hash.clone(), + }; + tx.send(event).unwrap(); + + return Ok(()); + } + hash_map::Entry::Vacant(_) => { + seen_hashes.insert(asset.hash.clone(), path.into()); + } + } } + + let always_target = matches!(state.params.target, SyncTarget::Studio | SyncTarget::Debug); + let is_new = always_target || lockfile_entry.is_none(); + + let asset_ref = match state.params.backend { + Some(ref backend) => backend.sync(&asset, lockfile_entry).await?, + None => lockfile_entry.map(Into::into), + }; + + let event = super::Event::Finished { + state: super::EventState::Synced { new: is_new }, + input_name: state.input_name.clone(), + path: path.into(), + rel_path: asset.path.clone(), + hash: asset.hash.clone(), + asset_ref, + }; + tx.send(event).unwrap(); + + Ok(()) } diff --git a/src/upload.rs b/src/upload.rs index 451578e..3f12f4d 100644 --- a/src/upload.rs +++ b/src/upload.rs @@ -1,4 +1,5 @@ -use crate::{asset::Asset, auth::Auth, cli::UploadArgs, config::Creator, web_api::WebApiClient}; +use crate::{asset::Asset, cli::UploadArgs, config::Creator, web_api::WebApiClient}; +use anyhow::Context; use fs_err::tokio as fs; use relative_path::PathExt; use resvg::usvg::fontdb::Database; @@ -8,20 +9,22 @@ pub async fn upload(args: UploadArgs) -> anyhow::Result<()> { let path = PathBuf::from(&args.path); let data = fs::read(&path).await?; - let mut asset = Asset::new(path.relative_to(".")?, data)?; - let mut font_db = Database::new(); font_db.load_system_fonts(); - asset.process(Arc::new(font_db), args.bleed).await?; + let asset = Asset::new(path.relative_to(".")?, data, Arc::new(font_db), args.bleed).await?; let creator = Creator { ty: args.creator_type, id: args.creator_id, }; - let auth = Auth::new(args.api_key, true)?; - let client = WebApiClient::new(auth, creator, args.expected_price); + let client = WebApiClient::new( + args.api_key + .context("An API key is required to use the upload command")?, + creator, + args.expected_price, + ); let asset_id = client.upload(&asset).await?; diff --git a/src/util/svg.rs b/src/util/svg.rs index d63fd9e..053a5dc 100644 --- a/src/util/svg.rs +++ b/src/util/svg.rs @@ -4,7 +4,7 @@ use resvg::{ }; use std::sync::Arc; -pub async fn svg_to_png(data: &[u8], fontdb: Arc) -> anyhow::Result> { +pub fn svg_to_png(data: &[u8], fontdb: Arc) -> anyhow::Result> { let opt = Options { fontdb, ..Default::default() diff --git a/src/web_api.rs b/src/web_api.rs index b50f0c8..d8c237b 100644 --- a/src/web_api.rs +++ b/src/web_api.rs @@ -1,17 +1,20 @@ use crate::{ asset::{Asset, AssetType}, - auth::Auth, - config::{Creator, CreatorType}, + config, }; use anyhow::{Context, bail}; use log::{debug, warn}; -use reqwest::{ - RequestBuilder, Response, StatusCode, - header::{self}, - multipart, -}; +use reqwest::{RequestBuilder, Response, StatusCode, multipart}; use serde::{Deserialize, Serialize}; -use std::time::Duration; +use std::{ + env, + sync::atomic::{AtomicBool, Ordering}, + time::Duration, +}; +use tokio::sync::Mutex; +use tokio::time::Instant; + +const RATELIMIT_RESET_HEADER: &str = "x-ratelimit-reset"; const UPLOAD_URL: &str = "https://apis.roblox.com/assets/v1/assets"; const OPERATION_URL: &str = "https://apis.roblox.com/assets/v1/operations"; @@ -20,35 +23,38 @@ const MAX_DISPLAY_NAME_LENGTH: usize = 50; pub struct WebApiClient { inner: reqwest::Client, - auth: Auth, - creator: Creator, + api_key: String, + creator: config::Creator, expected_price: Option, + fatally_failed: AtomicBool, + /// Shared rate limit state: when we can next make a request + rate_limit_reset: Mutex>, } impl WebApiClient { - pub fn new(auth: Auth, creator: Creator, expected_price: Option) -> Self { + pub fn new(api_key: String, creator: config::Creator, expected_price: Option) -> Self { WebApiClient { inner: reqwest::Client::new(), - auth, + api_key, creator, expected_price, + fatally_failed: AtomicBool::new(false), + rate_limit_reset: Mutex::new(None), } } pub async fn upload(&self, asset: &Asset) -> anyhow::Result { - let api_key = self - .auth - .api_key - .clone() - .context("An API key is necessary to upload")?; + if env::var("ASPHALT_TEST").is_ok() { + return Ok(1337); + } let file_name = asset.path.file_name().unwrap(); let display_name = trim_display_name(file_name); - let req = WebAssetRequest { + let req = Request { display_name, - asset_type: asset.ty.clone(), - creation_context: WebAssetRequestCreationContext { + asset_type: asset.ty, + creation_context: CreationContext { creator: self.creator.clone().into(), expected_price: self.expected_price, }, @@ -61,7 +67,7 @@ impl WebApiClient { let name = file_name.to_owned(); let res = self - .send_with_retry(|| { + .send_with_retry(|client| { let file_part = multipart::Part::stream_with_length( reqwest::Body::from(asset.data.clone()), len, @@ -74,56 +80,47 @@ impl WebApiClient { .text("request", req_json.clone()) .part("fileContent", file_part); - self.inner + client .post(UPLOAD_URL) - .header("x-api-key", &api_key) + .header("x-api-key", &self.api_key) .multipart(form) }) .await?; - let status = res.status(); let body = res.text().await?; - if status.is_success() { - let operation: WebAssetOperation = serde_json::from_str(&body)?; + let operation: Operation = serde_json::from_str(&body)?; - match self.poll_operation(operation.operation_id, &api_key).await { - Ok(Some(id)) => Ok(id), - Ok(None) => bail!("Failed to get asset ID"), - Err(e) => Err(e), - } - } else { - bail!("Failed to upload asset: {} - {}", status, body) - } + let id = self + .poll_operation(operation.operation_id, &self.api_key) + .await + .context("Failed to poll operation")?; + + Ok(id) } - async fn poll_operation(&self, id: String, api_key: &str) -> anyhow::Result> { + async fn poll_operation(&self, id: String, api_key: &str) -> anyhow::Result { let mut delay = Duration::from_secs(1); const MAX_POLLS: u32 = 10; for attempt in 0..MAX_POLLS { let res = self - .send_with_retry(|| { - self.inner + .send_with_retry(|client| { + client .get(format!("{OPERATION_URL}/{id}")) .header("x-api-key", api_key) }) .await?; - let status = res.status(); let text = res.text().await?; - if !status.is_success() { - bail!("Failed to poll operation: {} - {}", status, text); - } - - let operation: WebAssetOperation = serde_json::from_str(&text)?; + let operation: Operation = serde_json::from_str(&text)?; if operation.done { if let Some(response) = operation.response { - return Ok(Some(response.asset_id.parse()?)); + return Ok(response.asset_id.parse()?); } else { - bail!("Operation completed but no response provided") + bail!("Operation completed but no response provided"); } } @@ -140,35 +137,64 @@ impl WebApiClient { async fn send_with_retry(&self, make_req: F) -> anyhow::Result where - F: Fn() -> RequestBuilder, + F: Fn(&reqwest::Client) -> RequestBuilder, { + if self.fatally_failed.load(Ordering::SeqCst) { + bail!("A previous request failed due to a fatal error"); + } + const MAX: u8 = 5; let mut attempt = 0; loop { - let res = make_req().send().await?; + { + let reset = self.rate_limit_reset.lock().await; + if let Some(reset_at) = *reset { + let now = Instant::now(); + if reset_at > now { + let wait = reset_at - now; + drop(reset); + debug!("Waiting {:.2}ms for rate limit reset", wait.as_secs_f64()); + tokio::time::sleep(wait).await; + } + } + } - match res.status() { + let res = make_req(&self.inner).send().await?; + let status = res.status(); + + match status { StatusCode::TOO_MANY_REQUESTS if attempt < MAX => { let wait = res .headers() - .get(header::RETRY_AFTER) + .get(RATELIMIT_RESET_HEADER) .and_then(|h| h.to_str().ok()) .and_then(|s| s.parse::().ok()) .map(Duration::from_secs) .unwrap_or_else(|| Duration::from_secs(1 << attempt)); - tokio::time::sleep(wait).await; - attempt += 1; + let reset_at = Instant::now() + wait; + { + let mut reset = self.rate_limit_reset.lock().await; + *reset = Some(reset_at); + } warn!( - "Rate limited, retrying in {} seconds", - wait.as_millis() / 1000 + "Rate limited, retrying in {:.2} seconds", + wait.as_secs_f64() ); + tokio::time::sleep(wait).await; + attempt += 1; + continue; } - _ => return Ok(res), + StatusCode::OK => return Ok(res), + _ => { + let body = res.text().await?; + self.fatally_failed.store(true, Ordering::SeqCst); + bail!("Request failed with status {status}:\n{body}"); + } } } } @@ -176,34 +202,34 @@ impl WebApiClient { #[derive(Serialize)] #[serde(rename_all = "camelCase")] -struct WebAssetRequest { +struct Request { asset_type: AssetType, display_name: String, description: &'static str, - creation_context: WebAssetRequestCreationContext, + creation_context: CreationContext, } #[derive(Serialize)] #[serde(rename_all = "camelCase")] -struct WebAssetRequestCreationContext { - creator: WebAssetCreator, +struct CreationContext { + creator: Creator, expected_price: Option, } #[derive(Serialize)] #[serde(untagged)] -enum WebAssetCreator { - User(WebAssetUserCreator), - Group(WebAssetGroupCreator), +enum Creator { + User(UserCreator), + Group(GroupCreator), } -impl From for WebAssetCreator { - fn from(value: Creator) -> Self { +impl From for Creator { + fn from(value: config::Creator) -> Self { match value.ty { - CreatorType::User => WebAssetCreator::User(WebAssetUserCreator { + config::CreatorType::User => Creator::User(UserCreator { user_id: value.id.to_string(), }), - CreatorType::Group => WebAssetCreator::Group(WebAssetGroupCreator { + config::CreatorType::Group => Creator::Group(GroupCreator { group_id: value.id.to_string(), }), } @@ -212,27 +238,27 @@ impl From for WebAssetCreator { #[derive(Serialize)] #[serde(rename_all = "camelCase")] -struct WebAssetUserCreator { +struct UserCreator { user_id: String, } #[derive(Serialize)] #[serde(rename_all = "camelCase")] -struct WebAssetGroupCreator { +struct GroupCreator { group_id: String, } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] -struct WebAssetOperation { +struct Operation { done: bool, operation_id: String, - response: Option, + response: Option, } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] -struct WebAssetOperationResponse { +struct OperationResponse { asset_id: String, } diff --git a/tests/assets/README.md b/tests/assets/README.md new file mode 100644 index 0000000..54c6c9f --- /dev/null +++ b/tests/assets/README.md @@ -0,0 +1 @@ +Assets are sourced from https://samplelib.com/ diff --git a/tests/assets/test1.png b/tests/assets/test1.png new file mode 100644 index 0000000..785c315 Binary files /dev/null and b/tests/assets/test1.png differ diff --git a/tests/assets/test2.jpg b/tests/assets/test2.jpg new file mode 100644 index 0000000..ab911bb Binary files /dev/null and b/tests/assets/test2.jpg differ diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 0000000..bc5f911 --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,48 @@ +use assert_cmd::cargo::cargo_bin_cmd; +use assert_fs::{TempDir, fixture::ChildPath, prelude::*}; +use std::{fs, path::Path}; + +pub struct Project { + pub dir: TempDir, +} + +impl Project { + pub fn new() -> Self { + Self { + dir: TempDir::new().unwrap(), + } + } + + pub fn write_config(&self, contents: toml::Table) { + self.dir + .child("asphalt.toml") + .write_str(&contents.to_string()) + .unwrap(); + } + + pub fn write_lockfile(&self, contents: toml::Table) { + self.dir + .child("asphalt.lock.toml") + .write_str(&contents.to_string()) + .unwrap(); + } + + fn read_test_asset(&self, file_name: &str) -> Vec { + let path = Path::new("tests").join("assets").join(file_name); + fs::read(&path).unwrap() + } + + pub fn add_file(&self, file_name: &str) -> ChildPath { + let file = self.dir.child("input").child(file_name); + file.write_binary(&self.read_test_asset(file_name)).unwrap(); + file + } + + pub fn run(&self) -> assert_cmd::Command { + let mut cmd = cargo_bin_cmd!(); + cmd.env("ASPHALT_TEST", "true"); + cmd.env("ASPHALT_API_KEY", "test"); + cmd.current_dir(self.dir.path()); + cmd + } +} diff --git a/tests/sync.rs b/tests/sync.rs new file mode 100644 index 0000000..b0e454f --- /dev/null +++ b/tests/sync.rs @@ -0,0 +1,256 @@ +use assert_fs::{fixture::ChildPath, prelude::*}; +use common::Project; +use predicates::{Predicate, prelude::predicate, str::contains}; +use std::{fs, path::Path}; +use toml::toml; + +mod common; + +fn hash(path: &ChildPath) -> String { + let mut hasher = blake3::Hasher::new(); + hasher.update(&fs::read(path).unwrap()); + hasher.finalize().to_string() +} + +fn toml_eq(expected: toml::Value) -> impl Predicate { + predicate::function(move |path: &Path| { + let contents = fs::read_to_string(path).unwrap(); + let actual: toml::Value = toml::from_str(&contents).unwrap(); + actual == expected + }) +} + +#[test] +fn missing_config_fails() { + Project::new() + .run() + .args(["sync", "--target", "debug"]) + .assert() + .failure(); +} + +#[test] +fn debug_creates_output() { + let project = Project::new(); + project.write_config(toml! { + [creator] + type = "user" + id = 1234 + + [inputs.assets] + path = "input/**/*" + output_path = "output" + bleed = false + }); + let test_file = project.add_file("test1.png"); + + project.run().args(["sync", "debug"]).assert().success(); + + project + .dir + .child(".asphalt-debug/test1.png") + .assert(predicate::path::eq_file(test_file.path())); +} + +#[test] +fn debug_web_assets() { + let project = Project::new(); + project.write_config(toml! { + [creator] + type = "user" + id = 12345 + + [inputs.assets] + path = "input/**/*" + output_path = "output" + + [inputs.assets.web] + "existing.png" = { id = 1234 } + }); + + project.run().args(["sync", "debug"]).assert().success(); + + project + .dir + .child("output/assets.luau") + .assert(contains("existing.png")) + .assert(contains("1234")); +} + +#[test] +fn cloud_output_and_lockfile() { + let project = Project::new(); + project.write_config(toml! { + [creator] + type = "user" + id = 12345 + + [inputs.assets] + path = "input/**/*" + output_path = "output" + }); + let test_file = project.add_file("test1.png"); + + project + .run() + .args(["sync", "--api-key", "test"]) + .assert() + .success(); + + project.dir.child("asphalt.lock.toml").assert(toml_eq({ + let mut table = toml::Table::new(); + table.insert("version".into(), 2.into()); + + table.insert("inputs".into(), { + let mut inputs = toml::Table::new(); + inputs.insert("assets".into(), { + let mut assets = toml::Table::new(); + assets.insert(hash(&test_file), { + let mut entry = toml::Table::new(); + entry.insert("asset_id".into(), 1337.into()); + entry.into() + }); + assets.into() + }); + inputs.into() + }); + + table.into() + })); +} + +#[test] +fn dry_run_none() { + let project = Project::new(); + project.write_config(toml! { + [creator] + type = "user" + id = 12345 + + [inputs.assets] + path = "input/**/*" + output_path = "output" + }); + + project + .run() + .args(["sync", "cloud", "--dry-run"]) + .assert() + .success() + .stderr(contains("No new assets")); +} + +#[test] +fn dry_run_1_new() { + let project = Project::new(); + project.write_config(toml! { + [creator] + type = "user" + id = 12345 + + [inputs.assets] + path = "input/**/*" + output_path = "output" + }); + project.add_file("test1.png"); + + project + .run() + .args(["sync", "cloud", "--dry-run"]) + .assert() + .failure() + .stderr(contains("1 new assets")); +} + +#[test] +fn dry_run_1_new_1_old() { + let project = Project::new(); + project.write_config(toml! { + [creator] + type = "user" + id = 12345 + + [inputs.assets] + path = "input/**/*" + output_path = "output" + }); + let old_file = project.add_file("test1.png"); + project.add_file("test2.jpg"); + + project.write_lockfile({ + let mut table = toml::Table::new(); + table.insert("version".into(), 2.into()); + + table.insert("inputs".into(), { + let mut inputs = toml::Table::new(); + inputs.insert("assets".into(), { + let mut assets = toml::Table::new(); + assets.insert(hash(&old_file), { + let mut entry = toml::Table::new(); + entry.insert("asset_id".into(), toml::Value::Integer(1)); + entry.into() + }); + assets.into() + }); + inputs.into() + }); + + table + }); + + project + .run() + .args(["sync", "cloud", "--dry-run"]) + .assert() + .failure() + .stderr(contains("1 new assets")); +} + +#[test] +fn dry_run_2_old() { + let project = Project::new(); + project.write_config(toml! { + [creator] + type = "user" + id = 12345 + + [inputs.assets] + path = "input/**/*" + output_path = "output" + }); + let old_file_1 = project.add_file("test1.png"); + let old_file_2 = project.add_file("test2.jpg"); + + project.write_lockfile({ + let mut table = toml::Table::new(); + table.insert("version".into(), 2.into()); + + table.insert("inputs".into(), { + let mut inputs = toml::Table::new(); + inputs.insert("assets".into(), { + let mut assets = toml::Table::new(); + assets.insert(hash(&old_file_1), { + let mut entry = toml::Table::new(); + entry.insert("asset_id".into(), toml::Value::Integer(1)); + entry.into() + }); + assets.insert(hash(&old_file_2), { + let mut entry = toml::Table::new(); + entry.insert("asset_id".into(), toml::Value::Integer(1)); + entry.into() + }); + assets.into() + }); + inputs.into() + }); + + table + }); + + project + .run() + .args(["sync", "cloud", "--dry-run"]) + .assert() + .success() + .stderr(contains("No new assets")); +}