diff --git a/Cargo.lock b/Cargo.lock index 213e7bba..60f2e297 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -39,9 +39,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] @@ -63,18 +63,18 @@ dependencies = [ [[package]] name = "android_system_properties" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" dependencies = [ "libc", ] [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "arbitrary" @@ -106,6 +106,18 @@ dependencies = [ "x11rb", ] +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures 0.2.17", + "password-hash 0.5.0", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -199,7 +211,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -228,13 +240,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.89" +version = "0.1.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.6", ] [[package]] @@ -336,6 +348,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "base64ct" version = "1.8.3" @@ -374,13 +392,22 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.13.0" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" dependencies = [ "serde_core", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -401,9 +428,9 @@ dependencies = [ [[package]] name = "blocking" -version = "1.6.2" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa" dependencies = [ "async-channel", "async-task", @@ -450,9 +477,9 @@ checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytemuck" -version = "1.25.0" +version = "1.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" [[package]] name = "byteorder" @@ -468,9 +495,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" dependencies = [ "serde", ] @@ -501,7 +528,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", "cairo-sys-rs", "glib", "libc", @@ -522,9 +549,9 @@ dependencies = [ [[package]] name = "camino" -version = "1.2.3" +version = "1.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ce8d3bd5823c7504d3f579f13e7b2f3da252fcb938c594d5680ee508bf846f" +checksum = "bbbad30e4b4c14a39e3cc8aed085a12a327257c316619c93581e017bc52be591" dependencies = [ "serde_core", ] @@ -549,7 +576,7 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] @@ -564,9 +591,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.65" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +checksum = "a3eb0f42d6c360dc3f8a821f6bf2fdea7f72bfd36b3076eb0e6d1e9e0752fff4" dependencies = [ "find-msvc-tools", "jobserver", @@ -603,18 +630,18 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" [[package]] name = "chacha20" -version = "0.10.0" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", + "cpufeatures 0.3.1", "rand_core 0.10.1", ] @@ -657,19 +684,27 @@ dependencies = [ name = "citadel-opds" version = "0.1.0" dependencies = [ + "argon2", "axum", + "base64 0.22.1", "bytes", "chrono", "diesel", "futures-util", + "hmac", "libcalibre", "netdev", "quick-xml 0.38.4", + "rand_core 0.6.4", "reqwest 0.12.28", "serde", + "serde_json", + "sha2", "specta", + "subtle", "tempfile", "tokio", + "tower", "urlencoding", "uuid", ] @@ -687,6 +722,7 @@ dependencies = [ "libcalibre", "log", "mobi", + "netdev", "quick-xml 0.38.4", "regex", "reqwest 0.12.28", @@ -708,6 +744,7 @@ dependencies = [ "tauri-plugin-webdriver-automation", "tauri-specta", "tempfile", + "tokio", "urlencoding", "uuid", "zip 0.6.6", @@ -724,9 +761,9 @@ dependencies = [ [[package]] name = "combine" -version = "4.6.7" +version = "4.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" dependencies = [ "bytes", "memchr", @@ -743,9 +780,9 @@ dependencies = [ [[package]] name = "console" -version = "0.16.3" +version = "0.16.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" +checksum = "e96a4956774c13c126a8b5af4daa79384f4d826534c95a02d76afb39e2ab64e3" dependencies = [ "encode_unicode", "libc", @@ -760,9 +797,9 @@ checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" [[package]] name = "cookie" -version = "0.18.1" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" dependencies = [ "time", "version_check", @@ -800,7 +837,7 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", "core-foundation 0.10.1", "core-graphics-types", "foreign-types 0.5.0", @@ -813,7 +850,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", "core-foundation 0.10.1", "core-graphics-types", "foreign-types 0.5.0", @@ -826,11 +863,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", "core-foundation 0.10.1", "libc", ] +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -842,36 +885,36 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" dependencies = [ "libc", ] [[package]] name = "crc32fast" -version = "1.5.0" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-channel" -version = "0.5.15" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" [[package]] name = "crunchy" @@ -909,7 +952,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -940,12 +983,12 @@ dependencies = [ [[package]] name = "darling" -version = "0.23.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" dependencies = [ - "darling_core 0.23.0", - "darling_macro 0.23.0", + "darling_core 0.24.1", + "darling_macro 0.24.1", ] [[package]] @@ -959,20 +1002,20 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "darling_core" -version = "0.23.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" dependencies = [ "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.118", + "syn 3.0.6", ] [[package]] @@ -983,31 +1026,62 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core 0.21.3", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "darling_macro" -version = "0.23.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" dependencies = [ - "darling_core 0.23.0", + "darling_core 0.24.1", "quote", - "syn 2.0.118", + "syn 3.0.6", ] [[package]] name = "dbus" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b942602992bb7acfd1f51c49811c58a610ef9181b6e66f3e519d79b540a3bf73" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" dependencies = [ "libc", "libdbus-sys", "windows-sys 0.61.2", ] +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.20", +] + [[package]] name = "deranged" version = "0.5.8" @@ -1025,7 +1099,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1046,14 +1120,14 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "diesel" -version = "2.3.10" +version = "2.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29fe29a87fb84c631ffb3ba21798c4b1f3a964701ba78f0dce4bf8668562ec88" +checksum = "e3b934ddbdcb2abb9f9fc9c30bd47bcc5618b615eea1d334cda5fdf8ff9b072a" dependencies = [ "chrono", "diesel_derives", @@ -1066,15 +1140,15 @@ dependencies = [ [[package]] name = "diesel_derives" -version = "2.3.9" +version = "2.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1817b7f4279b947fc4cafddec12b0e5f8727141706561ce3ac94a60bddd1cf5" +checksum = "ecbd51fb6c020672543641167efa4e6417ff7ad76849ed556ace3595e72de03a" dependencies = [ "diesel_table_macro_syntax", "dsl_auto_type", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1094,7 +1168,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe2444076b48641147115697648dc743c2c00b61adade0f01ce67133c7babe8c" dependencies = [ - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1135,7 +1209,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", "block2", "libc", "objc2", @@ -1143,13 +1217,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.6", ] [[package]] @@ -1172,7 +1246,7 @@ checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1228,7 +1302,7 @@ dependencies = [ "objc2-foundation", "raw-window-handle", "serde", - "thiserror 2.0.18", + "thiserror 2.0.20", "windows 0.52.0", "windows-core 0.58.0", ] @@ -1244,7 +1318,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1291,20 +1365,20 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "either" -version = "1.16.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" [[package]] name = "embed-resource" -version = "3.0.9" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31a88c8d26de40ed18fe748c547845aa39de1db3afd958f8cb91579f3644bcb" +checksum = "fbfdaacccebec3b28e4866b8973543c7647797db5ada1bdab552e48fe665fbbd" dependencies = [ "cc", "memchr", "rustc_version", - "toml 1.1.2+spec-1.1.0", + "toml 1.1.6+spec-1.1.0", "vswhom", "winreg", ] @@ -1387,11 +1461,17 @@ checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" [[package]] name = "encoding_rs" -version = "0.8.35" +version = "0.8.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +checksum = "7b5ef0006ac9ab233c38522f5ae99cae3625151de8f706cacee1cba4b8e2832a" dependencies = [ "cfg-if", + "core_detect", + "multiversion", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", ] [[package]] @@ -1418,7 +1498,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1429,7 +1509,7 @@ checksum = "95518004c0a638e03a17589d2d336b7c936d92184d81bf1e66d3b1555de89f2d" dependencies = [ "percent-encoding", "regex", - "thiserror 2.0.18", + "thiserror 2.0.20", "xml-rs", "zip 3.0.0", ] @@ -1463,17 +1543,16 @@ dependencies = [ [[package]] name = "error-code" -version = "3.3.2" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" +checksum = "0b5343afd4a8365a643ac588dab4cf234a190c7f6c88c9f6dd6ffe00837661b7" [[package]] name = "event-listener" -version = "5.4.1" +version = "5.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" dependencies = [ - "concurrent-queue", "parking", "pin-project-lite", ] @@ -1502,9 +1581,9 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fastrand" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "fax" @@ -1543,9 +1622,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.9" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" [[package]] name = "fixedbitset" @@ -1555,12 +1634,12 @@ checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flate2" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.9.1", "zlib-rs", ] @@ -1603,13 +1682,13 @@ dependencies = [ [[package]] name = "foreign-types-macros" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.6", ] [[package]] @@ -1635,24 +1714,24 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" dependencies = [ "futures-core", ] [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] name = "futures-executor" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" dependencies = [ "futures-core", "futures-task", @@ -1661,9 +1740,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" [[package]] name = "futures-lite" @@ -1680,32 +1759,32 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.6", ] [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-core", "futures-io", @@ -1909,7 +1988,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", "futures-channel", "futures-core", "futures-executor", @@ -1937,7 +2016,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1952,9 +2031,9 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" [[package]] name = "gobject-sys" @@ -2016,14 +2095,14 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "h2" -version = "0.4.15" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16" dependencies = [ "atomic-waker", "bytes", @@ -2031,7 +2110,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.14.0", + "indexmap 2.14.2", "slab", "tokio", "tokio-util", @@ -2111,9 +2190,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" [[package]] name = "hex" @@ -2142,9 +2221,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ "bytes", "itoa", @@ -2152,9 +2231,9 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" dependencies = [ "bytes", "http", @@ -2162,9 +2241,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.3" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" dependencies = [ "bytes", "futures-core", @@ -2193,9 +2272,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.10.1" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" dependencies = [ "atomic-waker", "bytes", @@ -2305,9 +2384,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" dependencies = [ "displaydoc", "potential_utf", @@ -2319,9 +2398,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" dependencies = [ "displaydoc", "litemap", @@ -2332,9 +2411,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -2346,16 +2425,17 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" [[package]] name = "icu_properties" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" dependencies = [ + "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", @@ -2366,15 +2446,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" [[package]] name = "icu_provider" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" dependencies = [ "displaydoc", "icu_locale_core", @@ -2441,9 +2521,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.14.0" +version = "2.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" dependencies = [ "equivalent", "hashbrown 0.17.1", @@ -2484,9 +2564,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.12.0" +version = "2.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" [[package]] name = "is-docker" @@ -2536,6 +2616,60 @@ dependencies = [ "system-deps", ] +[[package]] +name = "jiff" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1baf72f08796de0260609515130699b890ac25f30e610ad894bc5856cafdb" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link 0.2.1", +] + +[[package]] +name = "jiff-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c" +dependencies = [ + "defmt", + "log", +] + +[[package]] +name = "jiff-static" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "378268a1116ad67ae6228701118ac9f491d78fda38a40a1f1a9e1348de6f7212" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + [[package]] name = "jni" version = "0.21.1" @@ -2564,7 +2698,7 @@ dependencies = [ "jni-sys 0.4.1", "log", "simd_cesu8", - "thiserror 2.0.18", + "thiserror 2.0.20", "walkdir", "windows-link 0.2.1", ] @@ -2579,7 +2713,7 @@ dependencies = [ "quote", "rustc_version", "simd_cesu8", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -2607,24 +2741,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "jobserver" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.3", "libc", ] [[package]] name = "js-sys" -version = "0.3.102" +version = "0.3.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" dependencies = [ "cfg-if", "futures-util", @@ -2659,7 +2793,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", "serde", "unicode-segmentation", ] @@ -2690,9 +2824,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.186" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libcalibre" @@ -2739,9 +2873,9 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.17" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +checksum = "6480ccc157a1389bb2e4891b24751b0f798ba640d22386f23143fbcc89da195a" dependencies = [ "libc", ] @@ -2764,9 +2898,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" [[package]] name = "lock_api" @@ -2779,9 +2913,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.33" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "mac-addr" @@ -2808,9 +2942,9 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "memchr" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "memoffset" @@ -2864,11 +2998,21 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" -version = "1.2.1" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" dependencies = [ "libc", "wasi", @@ -2913,10 +3057,37 @@ dependencies = [ "once_cell", "png 0.18.1", "serde", - "thiserror 2.0.18", + "thiserror 2.0.20", "windows-sys 0.61.2", ] +[[package]] +name = "multiversion" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ca4bea16ffc3f443cf7d866912118196bfef4c6a1556ca00f9f9b00bb43f7c" +dependencies = [ + "multiversion-macros", +] + +[[package]] +name = "multiversion-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d416831a7317ef4b08bee00b69cbbb9c8763da7959a7026244d6266869f9c83" +dependencies = [ + "proc-macro2", + "quote", + "rustversion", + "syn 3.0.6", +] + +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + [[package]] name = "native-tls" version = "0.2.18" @@ -2940,7 +3111,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", "jni-sys 0.3.1", "log", "ndk-sys", @@ -2998,7 +3169,7 @@ version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2288fcb784eb3defd5fb16f4c4160d5f477de192eac730f43e1d11c24d9a007" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", "libc", "log", "netlink-packet-core", @@ -3064,7 +3235,7 @@ dependencies = [ "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3083,7 +3254,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", "block2", "libc", "objc2", @@ -3104,7 +3275,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", "objc2", "objc2-foundation", ] @@ -3115,7 +3286,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", "objc2", "objc2-foundation", ] @@ -3126,7 +3297,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", "block2", "dispatch2", "libc", @@ -3139,7 +3310,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", "dispatch2", "objc2", "objc2-core-foundation", @@ -3172,7 +3343,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", "objc2", "objc2-core-foundation", "objc2-core-graphics", @@ -3184,7 +3355,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", "objc2", "objc2-core-foundation", "objc2-core-graphics", @@ -3197,7 +3368,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c71e34919aba0d701380d911702455038a8a3587467fe0141d6a71501e7ffe48" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", "objc2", "objc2-core-foundation", "objc2-foundation", @@ -3226,7 +3397,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", "block2", "libc", "objc2", @@ -3239,7 +3410,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", "objc2", "objc2-core-foundation", ] @@ -3250,7 +3421,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", "objc2", "objc2-app-kit", "objc2-foundation", @@ -3262,7 +3433,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", "objc2", "objc2-core-foundation", "objc2-foundation", @@ -3303,7 +3474,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", "block2", "objc2", "objc2-cloud-kit", @@ -3334,7 +3505,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", "block2", "objc2", "objc2-app-kit", @@ -3350,14 +3521,13 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "open" -version = "5.3.5" +version = "5.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fbaa89d2ddc8473c78a3adf69eea8cffa28c483b8e02a971ef31527cd0fc92c" +checksum = "aa576c76302b7b808eecc68061e67336c47833ef9d22caa74dda10fa9675eebc" dependencies = [ "dunce", "is-wsl", "libc", - "pathdiff", ] [[package]] @@ -3366,7 +3536,7 @@ version = "0.10.81" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", "cfg-if", "foreign-types 0.3.2", "libc", @@ -3382,7 +3552,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3440,7 +3610,7 @@ dependencies = [ "objc2-osa-kit", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] @@ -3509,16 +3679,21 @@ dependencies = [ ] [[package]] -name = "paste" -version = "1.0.15" +name = "password-hash" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] [[package]] -name = "pathdiff" -version = "0.2.3" +name = "paste" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pbkdf2" @@ -3528,7 +3703,7 @@ checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" dependencies = [ "digest", "hmac", - "password-hash", + "password-hash 0.4.2", "sha2", ] @@ -3546,7 +3721,7 @@ checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" dependencies = [ "fixedbitset", "hashbrown 0.15.5", - "indexmap 2.14.0", + "indexmap 2.14.2", ] [[package]] @@ -3590,7 +3765,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3621,19 +3796,19 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" [[package]] name = "plist" -version = "1.9.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" +checksum = "2896bade328c13f7042a297ea5ac5b0951f6cf989dea5f32c2fd98da398195cb" dependencies = [ - "base64 0.22.1", - "indexmap 2.14.0", - "quick-xml 0.39.4", + "base64 0.23.1", + "indexmap 2.14.2", + "quick-xml 0.42.0", "serde", "time", ] @@ -3648,7 +3823,7 @@ dependencies = [ "crc32fast", "fdeflate", "flate2", - "miniz_oxide", + "miniz_oxide 0.8.9", ] [[package]] @@ -3657,11 +3832,11 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", "crc32fast", "fdeflate", "flate2", - "miniz_oxide", + "miniz_oxide 0.8.9", ] [[package]] @@ -3678,11 +3853,26 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + [[package]] name = "potential_utf" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" dependencies = [ "zerovec", ] @@ -3734,7 +3924,7 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.25.12+spec-1.1.0", + "toml_edit 0.25.15+spec-1.1.0", ] [[package]] @@ -3763,9 +3953,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] @@ -3778,9 +3968,9 @@ checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.13.0", + "bitflags 2.13.2", "num-traits", - "rand 0.9.4", + "rand 0.9.5", "rand_chacha", "rand_xorshift", "regex-syntax", @@ -3791,9 +3981,9 @@ dependencies = [ [[package]] name = "pxfm" -version = "0.1.29" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" [[package]] name = "quick-error" @@ -3818,18 +4008,27 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.39.4" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quick-xml" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b" dependencies = [ "memchr", ] [[package]] name = "quote" -version = "1.0.46" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -3859,9 +4058,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ "rand_chacha", "rand_core 0.9.5", @@ -3869,9 +4068,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ "chacha20", "getrandom 0.4.3", @@ -3893,6 +4092,9 @@ name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] [[package]] name = "rand_core" @@ -3930,45 +4132,44 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", ] [[package]] name = "redox_users" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0" dependencies = [ - "getrandom 0.2.17", "libredox", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] name = "ref-cast" -version = "1.0.25" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.25" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.6", ] [[package]] name = "regex" -version = "1.12.4" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -3978,9 +4179,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.14" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", @@ -4035,11 +4236,11 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.13.4" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "bytes", "futures-core", "futures-util", @@ -4117,7 +4318,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" dependencies = [ "hashbrown 0.16.1", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] @@ -4126,7 +4327,7 @@ version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -4136,9 +4337,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustc_version" @@ -4151,11 +4352,11 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", "errno", "libc", "linux-raw-sys", @@ -4164,9 +4365,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.41" +version = "0.23.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" dependencies = [ "once_cell", "ring", @@ -4190,9 +4391,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.1" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ "zeroize", ] @@ -4226,9 +4427,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.13" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ "ring", "rustls-pki-types", @@ -4237,9 +4438,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "rusty-fork" @@ -4321,9 +4522,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" dependencies = [ "dyn-clone", "ref-cast", @@ -4340,7 +4541,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4355,7 +4556,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -4378,7 +4579,7 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", "cssparser", "derive_more", "log", @@ -4403,9 +4604,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -4425,22 +4626,22 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.6", ] [[package]] @@ -4451,14 +4652,14 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "serde_json" -version = "1.0.150" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -4480,13 +4681,13 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.6", ] [[package]] @@ -4521,18 +4722,19 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.21.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +checksum = "935177bb8c0cd8ca1a4e6d1a2ac8988bea69cab4f9d3a31311e012ad27868ea4" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "bs58", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.14.0", + "indexmap 2.14.2", + "jiff", "schemars 0.9.0", - "schemars 1.2.1", + "schemars 1.2.2", "serde_core", "serde_json", "serde_with_macros", @@ -4541,14 +4743,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.21.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +checksum = "1d607aa01a3cb0ad757d6fd216136910db3c97b102fe686585689615a02dbcdc" dependencies = [ - "darling 0.23.0", + "darling 0.24.1", "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.6", ] [[package]] @@ -4557,7 +4759,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.14.0", + "indexmap 2.14.2", "itoa", "ryu", "serde", @@ -4583,7 +4785,7 @@ checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4597,9 +4799,9 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" dependencies = [ "cfg-if", "cpufeatures 0.2.17", @@ -4619,13 +4821,13 @@ dependencies = [ [[package]] name = "shared_child" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7" +checksum = "607549934f6cc26b89cfecfdc46fa90f1e5d1536a68349b0c3a4f9d1c0d37959" dependencies = [ "libc", "sigchld", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4636,9 +4838,9 @@ checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "sigchld" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1" +checksum = "24f2b37f04360cd465089b87a9c3869c08220a2f3458463f0adf8badf5e77f2c" dependencies = [ "libc", "os_pipe", @@ -4647,9 +4849,9 @@ dependencies = [ [[package]] name = "signal-hook" -version = "0.3.18" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +checksum = "b2a0c28ca5908dbdbcd52e6fdaa00358ab88637f8ab33e1f188dd510eb44b53d" dependencies = [ "libc", "signal-hook-registry", @@ -4667,15 +4869,15 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" [[package]] name = "simd_cesu8" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" dependencies = [ "rustc_version", "simdutf8", @@ -4707,15 +4909,15 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.2" +version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" [[package]] name = "socket2" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", "windows-sys 0.61.2", @@ -4790,7 +4992,7 @@ dependencies = [ "Inflector", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4870,9 +5072,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "swift-rs" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +checksum = "e45c444e496845d3f2a351146bff59aae4975b2280238df1dfaa0c7d1846f38e" dependencies = [ "base64 0.21.7", "serde", @@ -4891,9 +5093,20 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.118" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" dependencies = [ "proc-macro2", "quote", @@ -4911,13 +5124,13 @@ dependencies = [ [[package]] name = "synstructure" -version = "0.13.2" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.6", ] [[package]] @@ -4926,7 +5139,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -4960,7 +5173,7 @@ version = "0.35.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", "block2", "core-foundation 0.10.1", "core-graphics 0.25.0", @@ -4996,13 +5209,13 @@ dependencies = [ [[package]] name = "tao-macros" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +checksum = "5f7eeb6d99155545da6150a1795945f16ac9c178deb2a5f2e74d776107bd5849" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -5024,9 +5237,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.11.3" +version = "2.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2616f96cb644bf2c5c456d9de4d5d5100e592d7424c74d8b55c5cb96e359e93" +checksum = "667b20e2726d572dea2de7370da16e188eb06008faf9a92fab7cdc46791190b5" dependencies = [ "anyhow", "bytes", @@ -5053,7 +5266,7 @@ dependencies = [ "percent-encoding", "plist", "raw-window-handle", - "reqwest 0.13.4", + "reqwest 0.13.5", "serde", "serde_json", "serde_repr", @@ -5065,7 +5278,7 @@ dependencies = [ "tauri-runtime", "tauri-runtime-wry", "tauri-utils", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "tray-icon", "url", @@ -5114,9 +5327,9 @@ dependencies = [ "serde", "serde_json", "sha2", - "syn 2.0.118", + "syn 2.0.119", "tauri-utils", - "thiserror 2.0.18", + "thiserror 2.0.20", "time", "url", "uuid", @@ -5132,7 +5345,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "tauri-codegen", "tauri-utils", ] @@ -5155,9 +5368,9 @@ dependencies = [ [[package]] name = "tauri-plugin-clipboard-manager" -version = "2.3.2" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "206dc20af4ed210748ba945c2774e60fd0acd52b9a73a028402caf809e9b6ecf" +checksum = "4136fb69d967753d000423d7e5f863f89bf949efbdfbecb43a580426a01a0194" dependencies = [ "arboard", "log", @@ -5165,14 +5378,14 @@ dependencies = [ "serde_json", "tauri", "tauri-plugin", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] name = "tauri-plugin-dialog" -version = "2.7.1" +version = "2.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884" +checksum = "61854a36651aa48381e5e209f69a01273b77f3f9f91f0c430b1b98d33bd47229" dependencies = [ "log", "raw-window-handle", @@ -5182,7 +5395,7 @@ dependencies = [ "tauri", "tauri-plugin", "tauri-plugin-fs", - "thiserror 2.0.18", + "thiserror 2.0.20", "url", ] @@ -5198,14 +5411,14 @@ dependencies = [ "serde_json", "tauri", "tauri-plugin", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] name = "tauri-plugin-fs" -version = "2.5.1" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371" +checksum = "de22eef34fd78c0da050e748710edd50bf127e651d02ea1b2bfada1523cc5c51" dependencies = [ "anyhow", "dunce", @@ -5220,16 +5433,16 @@ dependencies = [ "tauri", "tauri-plugin", "tauri-utils", - "thiserror 2.0.18", - "toml 1.1.2+spec-1.1.0", + "thiserror 2.0.20", + "toml 1.1.6+spec-1.1.0", "url", ] [[package]] name = "tauri-plugin-opener" -version = "2.5.4" +version = "2.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17e1bea14edce6b793a04e2417e3fd924b9bc4faae83cdee7d714156cceeed29" +checksum = "60d60366174b745b4ef5824b8bbc1c457fd08f0ce101ff643c0a49181a9f4e91" dependencies = [ "dunce", "glob", @@ -5241,7 +5454,7 @@ dependencies = [ "serde_json", "tauri", "tauri-plugin", - "thiserror 2.0.18", + "thiserror 2.0.20", "url", "windows 0.61.3", "zbus", @@ -5249,9 +5462,9 @@ dependencies = [ [[package]] name = "tauri-plugin-persisted-scope" -version = "2.3.7" +version = "2.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b560a5962bf975d38fb4ec98a0e64e52929992ec708acb812add9c1ab8d186d" +checksum = "3455c9b9d86158aa9acf596b2385fd99f3963e16fce0f5a613020b4388a36770" dependencies = [ "aho-corasick", "bincode", @@ -5260,14 +5473,14 @@ dependencies = [ "serde_json", "tauri", "tauri-plugin-fs", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] name = "tauri-plugin-shell" -version = "2.3.5" +version = "2.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8457dbf9e2bab1edd8df22bb2c20857a59a9868e79cb3eac5ed639eec4d0c73b" +checksum = "8548af174c5516e4f71f142acea4d02e00316296ea9aafa58798851481003e3c" dependencies = [ "encoding_rs", "log", @@ -5280,31 +5493,31 @@ dependencies = [ "shared_child", "tauri", "tauri-plugin", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", ] [[package]] name = "tauri-plugin-store" -version = "2.4.3" +version = "2.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c72dda16786eb4a3f903e43a17b64d8d78dc0f00fe2aa4b757c28f617a8630b" +checksum = "f2c7c771d3ce796b48c6e52b2fac38087d479a3c58e0424d3850cc21db6b4a69" dependencies = [ "dunce", "serde", "serde_json", "tauri", "tauri-plugin", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "tracing", ] [[package]] name = "tauri-plugin-updater" -version = "2.10.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806d9dac662c2e4594ff03c647a552f2c9bd544e7d0f683ec58f872f952ce4af" +checksum = "b28d8cabdeb0564f03ae261963de4bc3d98321cd3d213e76a81b7d344e5df606" dependencies = [ "base64 0.22.1", "dirs", @@ -5316,7 +5529,7 @@ dependencies = [ "minisign-verify", "osakit", "percent-encoding", - "reqwest 0.13.4", + "reqwest 0.13.5", "rustls", "semver", "serde", @@ -5325,7 +5538,7 @@ dependencies = [ "tauri", "tauri-plugin", "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.20", "time", "tokio", "url", @@ -5367,7 +5580,7 @@ dependencies = [ "serde", "serde_json", "tauri-utils", - "thiserror 2.0.18", + "thiserror 2.0.20", "url", "webkit2gtk", "webview2-com", @@ -5376,9 +5589,9 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.11.3" +version = "2.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe41e015bf8fc4d6477ff4926a0ef769dc64ff34c7b0038b6f7cacae892acb5c" +checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f" dependencies = [ "gtk", "http", @@ -5413,7 +5626,7 @@ dependencies = [ "specta-typescript", "tauri", "tauri-specta-macros", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] @@ -5425,7 +5638,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -5458,8 +5671,8 @@ dependencies = [ "serde_json", "serde_with", "swift-rs", - "thiserror 2.0.18", - "toml 1.1.2+spec-1.1.0", + "thiserror 2.0.20", + "toml 1.1.6+spec-1.1.0", "url", "urlpattern", "uuid", @@ -5474,7 +5687,7 @@ checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" dependencies = [ "dunce", "embed-resource", - "toml 1.1.2+spec-1.1.0", + "toml 1.1.6+spec-1.1.0", ] [[package]] @@ -5492,12 +5705,11 @@ dependencies = [ [[package]] name = "tendril" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" dependencies = [ "new_debug_unreachable", - "utf-8", ] [[package]] @@ -5511,11 +5723,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ - "thiserror-impl 2.0.18", + "thiserror-impl 2.0.20", ] [[package]] @@ -5526,18 +5738,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.6", ] [[package]] @@ -5556,9 +5768,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.51" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" dependencies = [ "deranged", "num-conv", @@ -5576,9 +5788,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.30" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" dependencies = [ "num-conv", "time-core", @@ -5586,9 +5798,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" dependencies = [ "displaydoc", "zerovec", @@ -5596,24 +5808,15 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" [[package]] name = "tokio" -version = "1.52.3" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", "libc", @@ -5628,13 +5831,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.0" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.6", ] [[package]] @@ -5649,9 +5852,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.4" +version = "0.26.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" dependencies = [ "rustls", "tokio", @@ -5659,13 +5862,14 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.18" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" dependencies = [ "bytes", "futures-core", "futures-sink", + "libc", "pin-project-lite", "tokio", ] @@ -5688,7 +5892,7 @@ version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ - "indexmap 2.14.0", + "indexmap 2.14.2", "serde_core", "serde_spanned 1.1.1", "toml_datetime 0.7.5+spec-1.1.0", @@ -5699,17 +5903,17 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.2+spec-1.1.0" +version = "1.1.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a" dependencies = [ - "indexmap 2.14.0", + "indexmap 2.14.2", "serde_core", "serde_spanned 1.1.1", "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "toml_writer", - "winnow 1.0.3", + "winnow 1.0.4", ] [[package]] @@ -5745,7 +5949,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.14.0", + "indexmap 2.14.2", "toml_datetime 0.6.3", "winnow 0.5.40", ] @@ -5756,7 +5960,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ - "indexmap 2.14.0", + "indexmap 2.14.2", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.3", @@ -5765,30 +5969,30 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.12+spec-1.1.0" +version = "0.25.15+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614" dependencies = [ - "indexmap 2.14.0", + "indexmap 2.14.2", "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow 1.0.3", + "winnow 1.0.4", ] [[package]] name = "toml_parser" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ - "winnow 1.0.3", + "winnow 1.0.4", ] [[package]] name = "toml_writer" -version = "1.1.1+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] name = "tower" @@ -5812,7 +6016,7 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", "bytes", "futures-util", "http", @@ -5856,7 +6060,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -5870,9 +6074,9 @@ dependencies = [ [[package]] name = "tray-icon" -version = "0.24.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65ba1e5f6b9ef9fd87e21b9c6f351554dbd717960089168fcfdef854686961dc" +checksum = "045979e3f037cd18ad1cb2a419dfda133c5c29c9f3453370079f2255d46c257e" dependencies = [ "crossbeam-channel", "dirs", @@ -5886,7 +6090,7 @@ dependencies = [ "once_cell", "png 0.18.1", "serde", - "thiserror 2.0.18", + "thiserror 2.0.20", "windows-sys 0.61.2", ] @@ -5979,9 +6183,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.24" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" [[package]] name = "unicode-segmentation" @@ -6032,12 +6236,6 @@ dependencies = [ "url", ] -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -6046,13 +6244,13 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.23.3" +version = "1.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" dependencies = [ "getrandom 0.4.3", "js-sys", - "rand 0.10.1", + "rand 0.10.2", "serde_core", "wasm-bindgen", ] @@ -6140,9 +6338,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.125" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" dependencies = [ "cfg-if", "once_cell", @@ -6153,9 +6351,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.75" +version = "0.4.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280" +checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" dependencies = [ "js-sys", "wasm-bindgen", @@ -6163,9 +6361,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.125" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6173,22 +6371,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.125" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.6", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.125" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" dependencies = [ "unicode-ident", ] @@ -6208,9 +6406,9 @@ dependencies = [ [[package]] name = "wayland-backend" -version = "0.3.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" +checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078" dependencies = [ "cc", "downcast-rs 1.2.1", @@ -6221,11 +6419,11 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.14" +version = "0.31.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" +checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", "rustix", "wayland-backend", "wayland-scanner", @@ -6237,7 +6435,7 @@ version = "0.32.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", "wayland-backend", "wayland-client", "wayland-scanner", @@ -6249,7 +6447,7 @@ version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", "wayland-backend", "wayland-client", "wayland-protocols", @@ -6258,12 +6456,12 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.10" +version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" +checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" dependencies = [ "proc-macro2", - "quick-xml 0.39.4", + "quick-xml 0.41.0", "quote", ] @@ -6278,9 +6476,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.102" +version = "0.3.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d" +checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" dependencies = [ "js-sys", "wasm-bindgen", @@ -6288,9 +6486,9 @@ dependencies = [ [[package]] name = "web_atoms" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297" +checksum = "ba8b815c1b593dc0baf78dd0f4fc8fdb2de53198fb1163738093e9a311c33fb3" dependencies = [ "phf", "phf_codegen", @@ -6344,9 +6542,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" dependencies = [ "rustls-pki-types", ] @@ -6373,7 +6571,7 @@ checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -6382,7 +6580,7 @@ version = "0.38.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" dependencies = [ - "thiserror 2.0.18", + "thiserror 2.0.20", "windows 0.61.3", "windows-core 0.61.2", ] @@ -6540,7 +6738,7 @@ checksum = "12168c33176773b86799be25e2a2ba07c7aab9968b37541f1094dbd7a60c8946" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -6551,7 +6749,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -6562,7 +6760,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -6573,7 +6771,7 @@ checksum = "9d8dc32e0095a7eeccebd0e3f09e9509365ecb3fc6ac4d6f5f14a3f6392942d1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -6584,7 +6782,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -6595,7 +6793,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -6952,9 +7150,9 @@ checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" [[package]] name = "winnow" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" dependencies = [ "memchr", ] @@ -6985,7 +7183,7 @@ dependencies = [ "log", "os_pipe", "rustix", - "thiserror 2.0.18", + "thiserror 2.0.20", "tree_magic_mini", "wayland-backend", "wayland-client", @@ -6995,9 +7193,9 @@ dependencies = [ [[package]] name = "writeable" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" [[package]] name = "wry" @@ -7032,7 +7230,7 @@ dependencies = [ "sha2", "soup3", "tao-macros", - "thiserror 2.0.18", + "thiserror 2.0.20", "url", "webkit2gtk", "webkit2gtk-sys", @@ -7093,9 +7291,9 @@ dependencies = [ [[package]] name = "xml" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "636f85e5ca6488e96401b61eb7de54f4e44755c988af0f52cf90230c312a1a89" +checksum = "2f45bb2c13fec6a6cb4c0f76a7e94839e110a14ec803ec2940777a94c347bc52" [[package]] name = "xml-rs" @@ -7119,21 +7317,21 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.6", "synstructure", ] [[package]] name = "zbus" -version = "5.16.0" +version = "5.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285" +checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907" dependencies = [ "async-broadcast", "async-executor", @@ -7158,7 +7356,7 @@ dependencies = [ "uds_windows", "uuid", "windows-sys 0.61.2", - "winnow 1.0.3", + "winnow 1.0.4", "zbus_macros", "zbus_names", "zvariant", @@ -7166,14 +7364,14 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.16.0" +version = "5.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6" +checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40" dependencies = [ "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.6", "zbus_names", "zvariant", "zvariant_utils", @@ -7181,33 +7379,42 @@ dependencies = [ [[package]] name = "zbus_names" -version = "4.3.2" +version = "4.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" +checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e" dependencies = [ "serde", - "winnow 1.0.3", + "winnow 1.0.4", "zvariant", ] +[[package]] +name = "zcheapstr" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473" +dependencies = [ + "serde", +] + [[package]] name = "zerocopy" -version = "0.8.52" +version = "0.8.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +checksum = "d35102a9f36d089ccae9e4c6802bc118be4487b80aaffc0ab4e0cf5ce92d2873" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.52" +version = "0.8.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +checksum = "146c01f5ab44258da43cf276c74a2763db2ff3969c9c652c3f2de07041d0b2bc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -7221,13 +7428,13 @@ dependencies = [ [[package]] name = "zerofrom-derive" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.6", "synstructure", ] @@ -7239,9 +7446,9 @@ checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zerotrie" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" dependencies = [ "displaydoc", "yoke", @@ -7250,9 +7457,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.6" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" dependencies = [ "yoke", "zerofrom", @@ -7261,13 +7468,13 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.3" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.6", ] [[package]] @@ -7299,7 +7506,7 @@ dependencies = [ "arbitrary", "crc32fast", "flate2", - "indexmap 2.14.0", + "indexmap 2.14.2", "memchr", "zopfli", ] @@ -7312,21 +7519,21 @@ checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" dependencies = [ "arbitrary", "crc32fast", - "indexmap 2.14.0", + "indexmap 2.14.2", "memchr", ] [[package]] name = "zlib-rs" -version = "0.6.4" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "977347db8caa080403f6b6b7c1cda9479a8e869316f7e13a59b19076a40f94e3" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" [[package]] name = "zopfli" @@ -7361,9 +7568,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" +version = "2.1.0+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +checksum = "0ef0a8027ec3ee71300ab3bcbcd0393f434aa72b91ca6d635a39941deae8eea0" dependencies = [ "cc", "pkg-config", @@ -7371,9 +7578,9 @@ dependencies = [ [[package]] name = "zune-core" -version = "0.5.1" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" +checksum = "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b" [[package]] name = "zune-jpeg" @@ -7386,40 +7593,41 @@ dependencies = [ [[package]] name = "zvariant" -version = "5.12.0" +version = "5.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0" +checksum = "c1d34c27cc6cdd1f458427519dd6b8612f7b7e3f7b9a0b2355d041dda9869147" dependencies = [ "endi", "enumflags2", "serde", - "winnow 1.0.3", + "winnow 1.0.4", + "zcheapstr", "zvariant_derive", "zvariant_utils", ] [[package]] name = "zvariant_derive" -version = "5.12.0" +version = "5.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737" +checksum = "864155e69b4352db0c7f374917bf45d1e0c8d17659c8b3dbf9795f3673f8c497" dependencies = [ "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.6", "zvariant_utils", ] [[package]] name = "zvariant_utils" -version = "3.4.0" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6" +checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3" dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.118", - "winnow 1.0.3", + "syn 3.0.6", + "winnow 1.0.4", ] diff --git a/crates/citadel-opds/Cargo.toml b/crates/citadel-opds/Cargo.toml index bad92709..7d47f855 100644 --- a/crates/citadel-opds/Cargo.toml +++ b/crates/citadel-opds/Cargo.toml @@ -6,14 +6,21 @@ rust-version.workspace = true description = "OPDS catalog, authentication, and networking runtime for Citadel" [dependencies] +argon2 = "0.5" axum = "0.8.9" +base64 = "0.22" bytes = "1" chrono = { version = "0.4.31", features = ["serde"] } futures-util = "0.3" +hmac = "0.12" +rand_core = { version = "0.6", features = ["getrandom"] } +sha2 = "0.10" +subtle = "2.6" libcalibre = { path = "../libcalibre" } netdev = { version = "=0.45.0", default-features = false } quick-xml = "0.38" serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" specta = { version = "=2.0.0-rc.22", features = ["chrono", "derive"] } tokio = { version = "1.52.3", features = ["macros", "net", "rt-multi-thread", "sync", "time"] } urlencoding = "2.1.3" @@ -21,5 +28,6 @@ uuid = { version = "1.6.1", features = ["v4", "fast-rng"] } [dev-dependencies] diesel = { version = "2.2.4", features = ["sqlite"] } -reqwest = "0.12" +reqwest = { version = "0.12", features = ["json"] } tempfile = "3.8" +tower = "0.5" diff --git a/crates/citadel-opds/src/auth.rs b/crates/citadel-opds/src/auth.rs new file mode 100644 index 00000000..2ef14866 --- /dev/null +++ b/crates/citadel-opds/src/auth.rs @@ -0,0 +1,510 @@ +use std::{ + sync::{Arc, Mutex}, + time::{Duration, Instant}, +}; + +use argon2::{ + password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, + Algorithm, Argon2, Params, Version, +}; +use axum::{ + extract::{Request, State}, + http::{header::AUTHORIZATION, header::WWW_AUTHENTICATE, HeaderValue, StatusCode}, + middleware::Next, + response::{IntoResponse, Response}, +}; +use base64::{engine::general_purpose::STANDARD, Engine}; +use hmac::{Hmac, Mac}; +use rand_core::{OsRng, RngCore}; +use sha2::Sha256; +use subtle::ConstantTimeEq; + +const DEFAULT_CACHE_CAPACITY: usize = 256; +const DEFAULT_CACHE_TTL: Duration = Duration::from_secs(30); +const DEFAULT_TARGET_DURATION: Duration = Duration::from_millis(250); +const BASIC_CHALLENGE: &str = "Basic realm=\"Citadel\""; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct OpdsAuthCredentials { + pub username: String, + pub verifier: String, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum OpdsAuthError { + InvalidVerifier, + PasswordHashingFailed, +} + +impl std::fmt::Display for OpdsAuthError { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::InvalidVerifier => { + formatter.write_str("The OPDS credential verifier is invalid.") + } + Self::PasswordHashingFailed => { + formatter.write_str("Could not create OPDS credentials.") + } + } + } +} + +impl std::error::Error for OpdsAuthError {} + +/// Creates credentials which store only a username and an Argon2id PHC verifier. +pub(crate) fn create_credentials( + username: String, + password: &[u8], +) -> Result { + let salt = SaltString::generate(&mut OsRng); + let verifier = argon2id() + .hash_password(password, &salt) + .map_err(|_| OpdsAuthError::PasswordHashingFailed)? + .to_string(); + + Ok(OpdsAuthCredentials { username, verifier }) +} + +#[derive(Clone)] +pub struct OpdsBasicAuth { + enabled: Option>, +} + +struct EnabledAuth { + credentials: OpdsAuthCredentials, + cache_key: [u8; 32], + cache: Mutex, + target_duration: Mutex, +} + +#[derive(Clone, Copy)] +enum CachedOutcome { + Authorized, + Rejected, +} + +struct CacheEntry { + tag: [u8; 32], + outcome: CachedOutcome, + target_duration: Duration, + expires_at: Instant, +} + +struct AuthCache { + entries: Vec, + capacity: usize, + ttl: Duration, +} + +impl AuthCache { + fn new(capacity: usize, ttl: Duration) -> Self { + Self { + entries: Vec::with_capacity(capacity), + capacity, + ttl, + } + } + + fn get(&mut self, tag: &[u8; 32]) -> Option<(CachedOutcome, Duration)> { + let now = Instant::now(); + self.entries.retain(|entry| entry.expires_at > now); + self.entries + .iter() + .find(|entry| bool::from(entry.tag.ct_eq(tag))) + .map(|entry| (entry.outcome, entry.target_duration)) + } + + fn insert(&mut self, tag: [u8; 32], outcome: CachedOutcome, target_duration: Duration) { + if self.capacity == 0 { + return; + } + if self.entries.len() == self.capacity { + self.entries.remove(0); + } + self.entries.push(CacheEntry { + tag, + outcome, + target_duration, + expires_at: Instant::now() + self.ttl, + }); + } +} + +impl OpdsBasicAuth { + pub(crate) fn disabled() -> Self { + Self { enabled: None } + } + + pub(crate) fn enabled(credentials: OpdsAuthCredentials) -> Result { + Self::enabled_with_settings( + credentials, + DEFAULT_CACHE_CAPACITY, + DEFAULT_CACHE_TTL, + DEFAULT_TARGET_DURATION, + ) + } + + fn enabled_with_settings( + credentials: OpdsAuthCredentials, + cache_capacity: usize, + cache_ttl: Duration, + target_duration: Duration, + ) -> Result { + let password_hash = + PasswordHash::new(&credentials.verifier).map_err(|_| OpdsAuthError::InvalidVerifier)?; + if password_hash.algorithm.as_str() != "argon2id" { + return Err(OpdsAuthError::InvalidVerifier); + } + + let mut cache_key = [0; 32]; + OsRng.fill_bytes(&mut cache_key); + Ok(Self { + enabled: Some(Arc::new(EnabledAuth { + credentials, + cache_key, + cache: Mutex::new(AuthCache::new(cache_capacity, cache_ttl)), + target_duration: Mutex::new(target_duration), + })), + }) + } + + fn is_enabled(&self) -> bool { + self.enabled.is_some() + } + + /// Authorizes complete Authorization header bytes. Disabled authentication does not parse them. + async fn authorize(&self, authorization: Option<&[u8]>) -> bool { + let Some(enabled) = &self.enabled else { + return true; + }; + + let started_at = Instant::now(); + let authorization = authorization.unwrap_or_default(); + let tag = opaque_tag(&enabled.cache_key, authorization); + if let Some((outcome, cached_duration)) = enabled + .cache + .lock() + .ok() + .and_then(|mut cache| cache.get(&tag)) + { + let target_duration = current_target_duration(enabled).max(cached_duration); + pad_to_target(started_at, target_duration).await; + return matches!(outcome, CachedOutcome::Authorized); + } + + let parsed = parse_basic_credentials(authorization); + let authorized = match parsed { + Some(credentials) if credentials.username == enabled.credentials.username => { + let verifier = enabled.credentials.verifier.clone(); + tokio::task::spawn_blocking(move || { + verify_password(&verifier, &credentials.password) + }) + .await + .unwrap_or(false) + } + _ => false, + }; + let target_duration = target_duration(enabled, started_at.elapsed()); + let outcome = if authorized { + CachedOutcome::Authorized + } else { + CachedOutcome::Rejected + }; + if let Ok(mut cache) = enabled.cache.lock() { + cache.insert(tag, outcome, target_duration); + } + pad_to_target(started_at, target_duration).await; + authorized + } +} + +/// Axum 0.8 middleware. Apply it as the final layer around the OPDS router. +pub(crate) async fn require_basic_auth( + State(auth): State, + request: Request, + next: Next, +) -> Response { + if !auth.is_enabled() { + return next.run(request).await; + } + + let authorization = request + .headers() + .get(AUTHORIZATION) + .map(|value| value.as_bytes().to_vec()); + if auth.authorize(authorization.as_deref()).await { + next.run(request).await + } else { + unauthorized_response() + } +} + +fn current_target_duration(enabled: &EnabledAuth) -> Duration { + enabled + .target_duration + .lock() + .map(|duration| *duration) + .unwrap_or(DEFAULT_TARGET_DURATION) +} + +struct BasicCredentials { + username: String, + password: Vec, +} + +fn parse_basic_credentials(authorization: &[u8]) -> Option { + let authorization = std::str::from_utf8(authorization).ok()?; + let (scheme, encoded) = authorization.split_once(' ')?; + if !scheme.eq_ignore_ascii_case("basic") || encoded.is_empty() || encoded.contains(' ') { + return None; + } + let decoded = STANDARD.decode(encoded).ok()?; + let separator = decoded.iter().position(|byte| *byte == b':')?; + let username = std::str::from_utf8(&decoded[..separator]).ok()?.to_string(); + Some(BasicCredentials { + username, + password: decoded[separator + 1..].to_vec(), + }) +} + +fn verify_password(verifier: &str, password: &[u8]) -> bool { + let Ok(password_hash) = PasswordHash::new(verifier) else { + return false; + }; + password_hash.algorithm.as_str() == "argon2id" + && argon2id().verify_password(password, &password_hash).is_ok() +} + +fn argon2id() -> Argon2<'static> { + Argon2::new(Algorithm::Argon2id, Version::V0x13, Params::default()) +} + +fn opaque_tag(cache_key: &[u8; 32], authorization: &[u8]) -> [u8; 32] { + let mut mac = Hmac::::new_from_slice(cache_key).expect("HMAC accepts a fixed-size key"); + mac.update(authorization); + mac.finalize().into_bytes().into() +} + +fn target_duration(enabled: &EnabledAuth, elapsed: Duration) -> Duration { + let Ok(mut target_duration) = enabled.target_duration.lock() else { + return elapsed.max(DEFAULT_TARGET_DURATION); + }; + *target_duration = (*target_duration).max(elapsed); + *target_duration +} + +async fn pad_to_target(started_at: Instant, target_duration: Duration) { + if let Some(remaining) = target_duration.checked_sub(started_at.elapsed()) { + tokio::time::sleep(remaining).await; + } +} + +fn unauthorized_response() -> Response { + ( + StatusCode::UNAUTHORIZED, + [(WWW_AUTHENTICATE, HeaderValue::from_static(BASIC_CHALLENGE))], + ) + .into_response() +} + +#[cfg(test)] +mod tests { + use axum::{ + body::Body, + http::{header::AUTHORIZATION, Request}, + middleware, + routing::get, + Router, + }; + use tower::ServiceExt; + + use super::*; + + fn basic_header(username: &str, password: &[u8]) -> Vec { + let mut credentials = username.as_bytes().to_vec(); + credentials.push(b':'); + credentials.extend(password); + format!("Basic {}", STANDARD.encode(credentials)).into_bytes() + } + + fn enabled_auth(username: &str, password: &[u8], target_duration: Duration) -> OpdsBasicAuth { + OpdsBasicAuth::enabled_with_settings( + create_credentials(username.to_string(), password).unwrap(), + 2, + Duration::from_secs(1), + target_duration, + ) + .unwrap() + } + + fn app(auth: OpdsBasicAuth) -> Router { + Router::new() + .route("/opds", get(|| async { "catalog" })) + .layer(middleware::from_fn_with_state(auth, require_basic_auth)) + } + + async fn response(auth: OpdsBasicAuth, authorization: Option>) -> Response { + let mut request = Request::builder().uri("/opds"); + if let Some(authorization) = authorization { + request = request.header(AUTHORIZATION, authorization); + } + app(auth) + .oneshot(request.body(Body::empty()).unwrap()) + .await + .unwrap() + } + + #[test] + fn credentials_store_a_random_argon2id_phc_verifier() { + let first = create_credentials("reader".to_string(), b"correct horse").unwrap(); + let second = create_credentials("reader".to_string(), b"correct horse").unwrap(); + + assert_eq!(first.username, "reader"); + assert!(first.verifier.starts_with("$argon2id$")); + assert_ne!(first.verifier, second.verifier); + let password_hash = PasswordHash::new(&first.verifier).unwrap(); + assert!(argon2id() + .verify_password(b"correct horse", &password_hash) + .is_ok()); + } + + #[tokio::test] + async fn disabled_auth_bypasses_even_malformed_authorization() { + let auth = OpdsBasicAuth::disabled(); + + assert!(auth.authorize(Some(b"not even close to Basic")).await); + assert_eq!( + response(auth, Some(b"not even close to Basic".to_vec())) + .await + .status(), + StatusCode::OK + ); + } + + #[tokio::test] + async fn enabled_auth_accepts_only_the_configured_basic_credentials() { + let auth = enabled_auth("reader", b"correct horse", Duration::ZERO); + + assert!( + auth.authorize(Some(&basic_header("reader", b"correct horse"))) + .await + ); + assert!( + !auth + .authorize(Some(&basic_header("reader", b"wrong password"))) + .await + ); + assert!( + !auth + .authorize(Some(&basic_header("someone-else", b"correct horse"))) + .await + ); + assert!(!auth.authorize(None).await); + } + + #[tokio::test] + async fn middleware_challenges_missing_and_wrong_credentials_and_allows_correct_ones() { + let auth = enabled_auth("reader", b"correct horse", Duration::ZERO); + + let missing = response(auth.clone(), None).await; + let wrong = response( + auth.clone(), + Some(basic_header("reader", b"wrong password")), + ) + .await; + let correct = response(auth, Some(basic_header("reader", b"correct horse"))).await; + + for rejected in [missing, wrong] { + assert_eq!(rejected.status(), StatusCode::UNAUTHORIZED); + assert_eq!( + rejected.headers().get(WWW_AUTHENTICATE).unwrap(), + BASIC_CHALLENGE + ); + } + assert_eq!(correct.status(), StatusCode::OK); + } + + #[tokio::test] + async fn cache_records_and_reuses_the_authorization_outcome_without_raw_header_bytes() { + let auth = enabled_auth("reader", b"correct horse", Duration::ZERO); + let header = basic_header("reader", b"correct horse"); + + assert!(auth.authorize(Some(&header)).await); + let enabled = auth.enabled.as_ref().unwrap(); + let cache = enabled.cache.lock().unwrap(); + assert_eq!(cache.entries.len(), 1); + assert_eq!( + cache.entries[0].tag, + opaque_tag(&enabled.cache_key, &header) + ); + assert!(matches!( + cache.entries[0].outcome, + CachedOutcome::Authorized + )); + drop(cache); + + assert!(auth.authorize(Some(&header)).await); + assert_eq!(enabled.cache.lock().unwrap().entries.len(), 1); + } + + #[tokio::test] + async fn unknown_usernames_and_cached_rejections_are_padded_to_the_target_duration() { + let target_duration = Duration::from_millis(20); + let auth = enabled_auth("reader", b"correct horse", target_duration); + let unknown_header = basic_header("unknown", b"correct horse"); + + let first_started_at = Instant::now(); + assert!(!auth.authorize(Some(&unknown_header)).await); + let first_elapsed = first_started_at.elapsed(); + + let cached_started_at = Instant::now(); + assert!(!auth.authorize(Some(&unknown_header)).await); + let cached_elapsed = cached_started_at.elapsed(); + + let minimum_padded_duration = target_duration - Duration::from_millis(2); + assert!(first_elapsed >= minimum_padded_duration); + assert!(cached_elapsed >= minimum_padded_duration); + } + + #[test] + fn basic_parser_preserves_password_bytes_after_the_first_colon() { + let header = basic_header("reader", b"pa:ss:word"); + let credentials = parse_basic_credentials(&header).unwrap(); + + assert_eq!(credentials.username, "reader"); + assert_eq!(credentials.password, b"pa:ss:word"); + } + + #[test] + fn cache_uses_only_opaque_complete_header_tags_and_is_bounded() { + let key = [7; 32]; + let first = opaque_tag(&key, b"Basic cmVhZGVyOmZpcnN0"); + let second = opaque_tag(&key, b"Basic cmVhZGVyOnNlY29uZA=="); + let differently_cased = opaque_tag(&key, b"basic cmVhZGVyOmZpcnN0"); + assert_ne!(first, second); + assert_ne!(first, differently_cased); + + let mut cache = AuthCache::new(1, Duration::from_secs(1)); + cache.insert(first, CachedOutcome::Authorized, Duration::from_millis(10)); + cache.insert(second, CachedOutcome::Rejected, Duration::from_millis(20)); + + assert!(cache.get(&first).is_none()); + assert_eq!( + cache + .get(&second) + .map(|(_, target_duration)| target_duration), + Some(Duration::from_millis(20)) + ); + } + + #[test] + fn unauthorized_response_uses_the_koreader_basic_challenge() { + let response = unauthorized_response(); + + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + assert_eq!( + response.headers().get(WWW_AUTHENTICATE).unwrap(), + "Basic realm=\"Citadel\"" + ); + } +} diff --git a/crates/citadel-opds/src/catalog.rs b/crates/citadel-opds/src/catalog.rs index 0cb07e9d..bb0c7f06 100644 --- a/crates/citadel-opds/src/catalog.rs +++ b/crates/citadel-opds/src/catalog.rs @@ -13,7 +13,10 @@ use chrono::{NaiveDateTime, SecondsFormat}; use libcalibre::{BookId, BookPage, CalibreError, ResolvedBookAsset}; use serde::Deserialize; -use super::assets::{self, AssetMethod, AssetResponseError}; +use super::{ + assets::{self, AssetMethod, AssetResponseError}, + auth::{require_basic_auth, OpdsBasicAuth}, +}; use crate::identity::{book_identity, library_identity}; const PAGE_SIZE: u64 = 50; @@ -74,7 +77,7 @@ pub(crate) struct FeedEntry { pub(crate) content: Option, } -pub fn router(source: Arc) -> Router { +pub fn router(source: Arc, auth: OpdsBasicAuth) -> Router { Router::new() .route("/opds", get(root_feed)) .route("/opds/all", get(all_books_feed)) @@ -87,6 +90,10 @@ pub fn router(source: Arc) -> Router { get(book_cover).head(book_cover_head), ) .with_state(CatalogState { source }) + .layer(axum::middleware::from_fn_with_state( + auth, + require_basic_auth, + )) } async fn root_feed(state: State, query: Query) -> Response { @@ -536,7 +543,10 @@ mod tests { impl CatalogSource for MemorySource { fn active_library_id(&self) -> Result { - Ok("550e8400-e29b-41d4-a716-446655440000".to_string()) + self.books + .first() + .map(|_| "memory-library".to_string()) + .ok_or(CalibreError::LibraryNotInitialized) } fn book_page( @@ -680,7 +690,9 @@ mod tests { let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let address = listener.local_addr().unwrap(); let task = tokio::spawn(async move { - axum::serve(listener, router(source)).await.unwrap(); + axum::serve(listener, router(source, OpdsBasicAuth::disabled())) + .await + .unwrap(); }); (format!("http://{address}"), task) } diff --git a/crates/citadel-opds/src/credentials.rs b/crates/citadel-opds/src/credentials.rs new file mode 100644 index 00000000..a1d17737 --- /dev/null +++ b/crates/citadel-opds/src/credentials.rs @@ -0,0 +1,212 @@ +use std::{ + fs::{self, OpenOptions}, + io::{self, Write}, + path::{Path, PathBuf}, + sync::{Arc, RwLock}, +}; + +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct StoredOpdsCredentials { + pub username: String, + pub password_verifier: String, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, specta::Type)] +#[serde(rename_all = "camelCase")] +pub struct OpdsCredentialStatus { + pub configured: bool, + pub username: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, specta::Type)] +#[serde(rename_all = "camelCase")] +pub struct GeneratedOpdsCredentials { + pub username: String, + pub password: String, +} + +#[derive(Clone)] +pub(crate) struct OpdsCredentialStore { + inner: Arc, +} + +struct CredentialStoreInner { + path: Option, + credentials: RwLock>, +} + +impl OpdsCredentialStore { + pub fn load(path: PathBuf) -> io::Result { + let credentials = match fs::read(&path) { + Ok(contents) => Some(serde_json::from_slice(&contents).map_err(io::Error::other)?), + Err(error) if error.kind() == io::ErrorKind::NotFound => None, + Err(error) => return Err(error), + }; + Ok(Self { + inner: Arc::new(CredentialStoreInner { + path: Some(path), + credentials: RwLock::new(credentials), + }), + }) + } + + #[cfg(test)] + pub fn in_memory() -> Self { + Self { + inner: Arc::new(CredentialStoreInner { + path: None, + credentials: RwLock::new(None), + }), + } + } + + pub fn status(&self) -> OpdsCredentialStatus { + let credentials = self + .inner + .credentials + .read() + .expect("OPDS credential store poisoned"); + OpdsCredentialStatus { + configured: credentials.is_some(), + username: credentials + .as_ref() + .map(|credentials| credentials.username.clone()), + } + } + + pub fn get(&self) -> Option { + self.inner + .credentials + .read() + .expect("OPDS credential store poisoned") + .clone() + } + + pub fn set(&self, credentials: StoredOpdsCredentials) -> io::Result<()> { + if let Some(path) = &self.inner.path { + persist(path, &credentials)?; + } + *self + .inner + .credentials + .write() + .expect("OPDS credential store poisoned") = Some(credentials); + Ok(()) + } + + pub fn clear(&self) -> io::Result<()> { + if let Some(path) = &self.inner.path { + match fs::remove_file(path) { + Ok(()) => {} + Err(error) if error.kind() == io::ErrorKind::NotFound => {} + Err(error) => return Err(error), + } + } + *self + .inner + .credentials + .write() + .expect("OPDS credential store poisoned") = None; + Ok(()) + } +} + +fn persist(path: &Path, credentials: &StoredOpdsCredentials) -> io::Result<()> { + let parent = path + .parent() + .ok_or_else(|| io::Error::other("credential path has no parent"))?; + fs::create_dir_all(parent)?; + let temporary = parent.join(format!(".opds-credentials-{}.tmp", uuid::Uuid::new_v4())); + let bytes = serde_json::to_vec(credentials).map_err(io::Error::other)?; + + let mut options = OpenOptions::new(); + options.create_new(true).write(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + options.mode(0o600); + } + let mut file = options.open(&temporary)?; + if let Err(error) = file.write_all(&bytes).and_then(|_| file.sync_all()) { + let _ = fs::remove_file(&temporary); + return Err(error); + } + fs::rename(&temporary, path)?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + fs::set_permissions(path, fs::Permissions::from_mode(0o600))?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn persists_only_username_and_verifier_with_private_permissions() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("opds-credentials.json"); + let store = OpdsCredentialStore::load(path.clone()).unwrap(); + store + .set(StoredOpdsCredentials { + username: "reader".to_string(), + password_verifier: "$argon2id$verifier".to_string(), + }) + .unwrap(); + + let contents = fs::read_to_string(&path).unwrap(); + assert!(contents.contains("reader")); + assert!(contents.contains("$argon2id$verifier")); + assert!(!contents.contains("password\":")); + assert_eq!( + OpdsCredentialStore::load(path) + .unwrap() + .status() + .username + .as_deref(), + Some("reader") + ); + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + assert_eq!( + fs::metadata(directory.path().join("opds-credentials.json")) + .unwrap() + .permissions() + .mode() + & 0o777, + 0o600 + ); + } + } + + #[test] + fn clear_is_idempotent() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("opds-credentials.json"); + let store = OpdsCredentialStore::load(path.clone()).unwrap(); + store + .set(StoredOpdsCredentials { + username: "reader".to_string(), + password_verifier: "verifier".to_string(), + }) + .unwrap(); + + store.clear().unwrap(); + store.clear().unwrap(); + assert!(!path.exists()); + assert_eq!( + store.status(), + OpdsCredentialStatus { + configured: false, + username: None + } + ); + } +} diff --git a/crates/citadel-opds/src/lib.rs b/crates/citadel-opds/src/lib.rs index 1ef9e953..1e9cd67b 100644 --- a/crates/citadel-opds/src/lib.rs +++ b/crates/citadel-opds/src/lib.rs @@ -1,9 +1,19 @@ //! Framework-free OPDS catalog, authentication, networking, and service lifecycle for Citadel. pub mod assets; +mod auth; pub mod catalog; +pub mod credentials; mod identity; pub mod network; +pub mod service; mod xml; +pub use auth::OpdsBasicAuth; pub use catalog::{router, CatalogSource}; +pub use credentials::{GeneratedOpdsCredentials, OpdsCredentialStatus}; +pub use network::{OpdsInterfaceKind, OpdsInterfaceState, OpdsNetworkInterface}; +pub use service::{ + OpdsBindTarget, OpdsErrorCode, OpdsLifecycleState, OpdsService, OpdsServiceStatus, + OpdsStartConfig, OpdsStatusError, +}; diff --git a/crates/citadel-opds/src/service.rs b/crates/citadel-opds/src/service.rs new file mode 100644 index 00000000..14f5f9ce --- /dev/null +++ b/crates/citadel-opds/src/service.rs @@ -0,0 +1,1532 @@ +use std::{ + collections::{BTreeMap, BTreeSet}, + io, + net::{SocketAddr, TcpListener as StdTcpListener}, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, Mutex, + }, + time::Duration, +}; + +use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; +use futures_util::future::join_all; +use rand_core::{OsRng, RngCore}; +use serde::{Deserialize, Serialize}; +use tokio::{ + sync::{oneshot, Notify}, + task::JoinHandle, +}; + +use super::{ + auth::{create_credentials, OpdsAuthCredentials, OpdsBasicAuth}, + credentials::{ + GeneratedOpdsCredentials, OpdsCredentialStatus, OpdsCredentialStore, StoredOpdsCredentials, + }, + network::{ + advertised_url, plan_bindings, InterfaceProvider, InterfaceSnapshot, + NetdevInterfaceProvider, OpdsNetworkInterface, WaitingReason, + }, + router, CatalogSource, +}; + +const MONITOR_INTERVAL: Duration = Duration::from_secs(2); +const LISTENER_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5); +const WORKER_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(6); + +pub use crate::network::OpdsBindTarget; + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, specta::Type)] +#[serde(rename_all = "camelCase")] +pub struct OpdsStartConfig { + pub target: OpdsBindTarget, + pub port: u32, + pub authentication_enabled: bool, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, specta::Type)] +#[serde(rename_all = "camelCase")] +pub enum OpdsLifecycleState { + Stopped, + Starting, + Running, + WaitingForInterface, + Error, + Stopping, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, specta::Type)] +#[serde(rename_all = "camelCase")] +pub enum OpdsErrorCode { + InvalidPort, + LibraryNotReady, + ConfigurationConflict, + InterfaceUnavailable, + InterfaceEnumerationFailed, + PortUnavailable, + ListenerFailed, + InvalidCredentials, + CredentialsRequired, + CredentialStorageFailed, + Unexpected, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, specta::Type)] +#[serde(rename_all = "camelCase")] +pub struct OpdsStatusError { + pub code: OpdsErrorCode, + pub message: String, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, specta::Type)] +#[serde(rename_all = "camelCase")] +pub struct OpdsServiceStatus { + pub state: OpdsLifecycleState, + pub target: Option, + pub port: Option, + pub active_library_id: Option, + pub urls: Vec, + pub error: Option, +} + +impl Default for OpdsServiceStatus { + fn default() -> Self { + Self { + state: OpdsLifecycleState::Stopped, + target: None, + port: None, + active_library_id: None, + urls: Vec::new(), + error: None, + } + } +} + +trait InterfaceSnapshots: Send + Sync { + fn snapshot(&self) -> io::Result>; +} + +struct NetdevInterfaceSnapshots; + +impl InterfaceSnapshots for NetdevInterfaceSnapshots { + fn snapshot(&self) -> io::Result> { + let mut provider = NetdevInterfaceProvider; + provider.snapshot() + } +} + +trait ListenerFactory: Send + Sync { + fn start( + &self, + address: SocketAddr, + source: Arc, + auth: OpdsBasicAuth, + tracker: Arc, + ) -> io::Result; +} + +struct TcpListenerFactory; + +impl ListenerFactory for TcpListenerFactory { + fn start( + &self, + address: SocketAddr, + source: Arc, + auth: OpdsBasicAuth, + tracker: Arc, + ) -> io::Result { + let listener = StdTcpListener::bind(address)?; + listener.set_nonblocking(true)?; + let listener = tokio::net::TcpListener::from_std(listener)?; + let (shutdown, shutdown_receiver) = oneshot::channel(); + let app = router(source, auth); + let completion = tracker.register(); + let task = tokio::spawn(async move { + let _completion = completion; + axum::serve(listener, app) + .with_graceful_shutdown(async move { + let _ = shutdown_receiver.await; + }) + .await + }); + Ok(ServerTask { + shutdown: Some(shutdown), + task: Some(task), + }) + } +} + +struct ServiceDependencies { + interfaces: Arc, + listeners: Arc, + monitor_interval: Duration, + worker_shutdown_timeout: Duration, +} + +impl Default for ServiceDependencies { + fn default() -> Self { + Self { + interfaces: Arc::new(NetdevInterfaceSnapshots), + listeners: Arc::new(TcpListenerFactory), + monitor_interval: MONITOR_INTERVAL, + worker_shutdown_timeout: WORKER_SHUTDOWN_TIMEOUT, + } + } +} + +struct RunningController { + config: OpdsStartConfig, + stop: oneshot::Sender<()>, + worker: JoinHandle<()>, +} + +struct ServiceInner { + status: Arc>, + controller: tokio::sync::Mutex>, + source: Arc, + dependencies: ServiceDependencies, + listener_tracker: Arc, + credentials: OpdsCredentialStore, +} + +#[derive(Clone)] +pub struct OpdsService { + inner: Arc, +} + +impl OpdsService { + pub fn new( + source: Arc, + credential_path: std::path::PathBuf, + ) -> io::Result { + Ok(Self::with_dependencies_and_credentials( + source, + ServiceDependencies::default(), + OpdsCredentialStore::load(credential_path)?, + )) + } + + #[cfg(test)] + fn with_dependencies( + source: Arc, + dependencies: ServiceDependencies, + ) -> Self { + Self::with_dependencies_and_credentials( + source, + dependencies, + OpdsCredentialStore::in_memory(), + ) + } + + fn with_dependencies_and_credentials( + source: Arc, + dependencies: ServiceDependencies, + credentials: OpdsCredentialStore, + ) -> Self { + Self { + inner: Arc::new(ServiceInner { + status: Arc::new(Mutex::new(OpdsServiceStatus::default())), + controller: tokio::sync::Mutex::new(None), + source, + dependencies, + listener_tracker: Arc::new(ListenerTracker::default()), + credentials, + }), + } + } + + pub fn credential_status(&self) -> OpdsCredentialStatus { + self.inner.credentials.status() + } + + pub async fn configure_credentials( + &self, + username: String, + password: String, + ) -> Result { + let mut controller = self.inner.controller.lock().await; + reap_finished_controller(&mut controller, &self.inner.status).await; + ensure_credentials_editable(&controller)?; + let username = normalize_username(username)?; + if password.is_empty() { + return Err(invalid_credentials("Enter a password.")); + } + let credentials = + tokio::task::spawn_blocking(move || create_credentials(username, password.as_bytes())) + .await + .map_err(|_| credential_storage_error())? + .map_err(|_| credential_storage_error())?; + self.inner + .credentials + .set(StoredOpdsCredentials { + username: credentials.username, + password_verifier: credentials.verifier, + }) + .map_err(|_| credential_storage_error())?; + drop(controller); + Ok(self.credential_status()) + } + + pub async fn clear_credentials(&self) -> Result { + let mut controller = self.inner.controller.lock().await; + reap_finished_controller(&mut controller, &self.inner.status).await; + ensure_credentials_editable(&controller)?; + self.inner + .credentials + .clear() + .map_err(|_| credential_storage_error())?; + drop(controller); + Ok(self.credential_status()) + } + + pub async fn generate_credentials( + &self, + username: String, + ) -> Result { + let username = normalize_username(username)?; + let mut bytes = [0_u8; 18]; + OsRng.fill_bytes(&mut bytes); + let password = URL_SAFE_NO_PAD.encode(bytes); + self.configure_credentials(username.clone(), password.clone()) + .await?; + Ok(GeneratedOpdsCredentials { username, password }) + } + + pub async fn status(&self) -> OpdsServiceStatus { + if let Ok(mut controller) = self.inner.controller.try_lock() { + reap_finished_controller(&mut controller, &self.inner.status).await; + } + + let mut status = status_snapshot(&self.inner.status); + if status.state == OpdsLifecycleState::Stopped { + status.active_library_id = None; + return status; + } + status.active_library_id = active_library_id(self.inner.source.clone()).await; + status + } + + pub async fn list_interfaces(&self) -> Result, OpdsStatusError> { + snapshot_interfaces(self.inner.dependencies.interfaces.clone()) + .await + .map(|interfaces| { + interfaces + .into_iter() + .map(|interface| interface.public()) + .collect() + }) + .map_err(|_| OpdsStatusError { + code: OpdsErrorCode::InterfaceEnumerationFailed, + message: "Citadel could not list network interfaces.".to_string(), + }) + } + + pub async fn start( + &self, + config: OpdsStartConfig, + ) -> Result { + let mut controller = self.inner.controller.lock().await; + reap_finished_controller(&mut controller, &self.inner.status).await; + if let Some(running) = controller.as_ref() { + if running.config == config { + drop(controller); + return Ok(self.status().await); + } + return Err(OpdsStatusError { + code: OpdsErrorCode::ConfigurationConflict, + message: "Sharing is already active with different network settings. Stop it before changing the interface or port.".to_string(), + }); + } + + let auth = if config.authentication_enabled { + let Some(credentials) = self.inner.credentials.get() else { + return Err(OpdsStatusError { + code: OpdsErrorCode::CredentialsRequired, + message: "Set a username and password before enabling authentication." + .to_string(), + }); + }; + OpdsBasicAuth::enabled(OpdsAuthCredentials { + username: credentials.username, + verifier: credentials.password_verifier, + }) + .map_err(|_| credential_storage_error())? + } else { + OpdsBasicAuth::disabled() + }; + + set_configured_status( + &self.inner.status, + &config, + OpdsLifecycleState::Starting, + None, + Vec::new(), + None, + ); + let Ok(port) = u16::try_from(config.port) + .ok() + .filter(|port| *port != 0) + .ok_or(()) + else { + set_configured_status( + &self.inner.status, + &config, + OpdsLifecycleState::Error, + Some(OpdsStatusError { + code: OpdsErrorCode::InvalidPort, + message: "Choose a port between 1 and 65535.".to_string(), + }), + Vec::new(), + None, + ); + drop(controller); + return Ok(self.status().await); + }; + + let library_id = active_library_id(self.inner.source.clone()).await; + let Some(library_id) = library_id else { + set_configured_status( + &self.inner.status, + &config, + OpdsLifecycleState::Error, + Some(OpdsStatusError { + code: OpdsErrorCode::LibraryNotReady, + message: "Open a library before starting sharing.".to_string(), + }), + Vec::new(), + None, + ); + drop(controller); + return Ok(self.status().await); + }; + + let mut servers = BTreeMap::new(); + match snapshot_interfaces(self.inner.dependencies.interfaces.clone()).await { + Ok(interfaces) => { + apply_plan( + &interfaces, + &config, + port, + &mut servers, + self.inner.dependencies.listeners.as_ref(), + self.inner.listener_tracker.clone(), + self.inner.source.clone(), + auth.clone(), + &self.inner.status, + Some(library_id.clone()), + ) + .await; + } + Err(_) => { + set_configured_status( + &self.inner.status, + &config, + OpdsLifecycleState::Error, + Some(interface_enumeration_error(false)), + Vec::new(), + Some(library_id), + ); + } + } + + let (stop, stop_receiver) = oneshot::channel(); + let worker = tokio::spawn(monitor_service( + self.inner.dependencies.interfaces.clone(), + self.inner.dependencies.listeners.clone(), + self.inner.listener_tracker.clone(), + self.inner.source.clone(), + auth, + self.inner.status.clone(), + config.clone(), + port, + servers, + stop_receiver, + self.inner.dependencies.monitor_interval, + )); + *controller = Some(RunningController { + config, + stop, + worker, + }); + drop(controller); + Ok(self.status().await) + } + + pub async fn stop(&self) -> OpdsServiceStatus { + let mut controller = self.inner.controller.lock().await; + let Some(running) = controller.take() else { + set_stopped_status(&self.inner.status); + drop(controller); + return self.status().await; + }; + set_simple_status( + &self.inner.status, + OpdsLifecycleState::Stopping, + None, + Vec::new(), + ); + let stopped = stop_controller( + running, + self.inner.listener_tracker.clone(), + self.inner.dependencies.worker_shutdown_timeout, + ) + .await; + if stopped { + set_stopped_status(&self.inner.status); + } else { + set_simple_status( + &self.inner.status, + OpdsLifecycleState::Error, + Some(OpdsStatusError { + code: OpdsErrorCode::ListenerFailed, + message: "Citadel could not confirm that every OPDS listener stopped." + .to_string(), + }), + Vec::new(), + ); + } + drop(controller); + self.status().await + } +} + +fn ensure_credentials_editable( + controller: &Option, +) -> Result<(), OpdsStatusError> { + if controller.is_some() { + return Err(OpdsStatusError { + code: OpdsErrorCode::ConfigurationConflict, + message: "Stop sharing before changing credentials.".to_string(), + }); + } + Ok(()) +} + +fn normalize_username(username: String) -> Result { + let username = username.trim().to_string(); + if username.is_empty() { + return Err(invalid_credentials("Enter a username.")); + } + if username.contains(':') || username.chars().any(char::is_control) { + return Err(invalid_credentials( + "The username cannot contain a colon or control characters.", + )); + } + Ok(username) +} + +fn invalid_credentials(message: &str) -> OpdsStatusError { + OpdsStatusError { + code: OpdsErrorCode::InvalidCredentials, + message: message.to_string(), + } +} + +fn credential_storage_error() -> OpdsStatusError { + OpdsStatusError { + code: OpdsErrorCode::CredentialStorageFailed, + message: "Citadel could not securely save the sharing credentials.".to_string(), + } +} + +struct ServerTask { + shutdown: Option>, + task: Option>>, +} + +#[derive(Default)] +struct ListenerTracker { + active: AtomicUsize, + empty: Notify, +} + +impl ListenerTracker { + fn register(self: &Arc) -> ListenerCompletion { + self.active.fetch_add(1, Ordering::AcqRel); + ListenerCompletion { + tracker: self.clone(), + } + } + + async fn wait_until_empty(&self) { + loop { + let notified = self.empty.notified(); + if self.active.load(Ordering::Acquire) == 0 { + return; + } + notified.await; + } + } +} + +struct ListenerCompletion { + tracker: Arc, +} + +impl Drop for ListenerCompletion { + fn drop(&mut self) { + if self.tracker.active.fetch_sub(1, Ordering::AcqRel) == 1 { + self.tracker.empty.notify_waiters(); + } + } +} + +impl Drop for ServerTask { + fn drop(&mut self) { + if let Some(task) = self.task.as_ref() { + task.abort(); + } + } +} + +async fn snapshot_interfaces( + interfaces: Arc, +) -> io::Result> { + tokio::task::spawn_blocking(move || interfaces.snapshot()) + .await + .map_err(|_| io::Error::other("interface snapshot task failed"))? +} + +async fn active_library_id(source: Arc) -> Option { + tokio::task::spawn_blocking(move || source.active_library_id().ok()) + .await + .ok() + .flatten() +} + +async fn apply_plan( + interfaces: &[InterfaceSnapshot], + config: &OpdsStartConfig, + port: u16, + servers: &mut BTreeMap, + listeners: &dyn ListenerFactory, + tracker: Arc, + source: Arc, + auth: OpdsBasicAuth, + status: &Mutex, + library_id: Option, +) { + match plan_bindings(interfaces, &config.target, port) { + super::network::BindPlan::Wait(reason) => { + shutdown_servers(servers).await; + set_configured_status( + status, + config, + OpdsLifecycleState::WaitingForInterface, + Some(waiting_error(&reason)), + Vec::new(), + library_id, + ); + } + super::network::BindPlan::Listen(desired) => { + let stale = servers + .keys() + .filter(|address| !desired.contains(address)) + .copied() + .collect::>(); + let mut stale_servers = BTreeMap::new(); + for address in stale { + if let Some(server) = servers.remove(&address) { + stale_servers.insert(address, server); + } + } + shutdown_servers(&mut stale_servers).await; + + if let Err(error) = + start_missing_servers(servers, &desired, listeners, tracker, source, auth) + { + shutdown_servers(servers).await; + set_configured_status( + status, + config, + OpdsLifecycleState::Error, + Some(bind_error(error)), + Vec::new(), + library_id, + ); + return; + } + set_configured_status( + status, + config, + OpdsLifecycleState::Running, + None, + urls(&desired), + library_id, + ); + } + } +} + +fn start_missing_servers( + servers: &mut BTreeMap, + desired: &BTreeSet, + listeners: &dyn ListenerFactory, + tracker: Arc, + source: Arc, + auth: OpdsBasicAuth, +) -> io::Result<()> { + for address in desired { + if address.ip().is_unspecified() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "wildcard listeners are forbidden", + )); + } + if !servers.contains_key(address) { + servers.insert( + *address, + listeners.start(*address, source.clone(), auth.clone(), tracker.clone())?, + ); + } + } + Ok(()) +} + +async fn monitor_service( + interfaces: Arc, + listeners: Arc, + tracker: Arc, + source: Arc, + auth: OpdsBasicAuth, + status: Arc>, + config: OpdsStartConfig, + port: u16, + mut servers: BTreeMap, + mut stop: oneshot::Receiver<()>, + interval: Duration, +) { + let mut ticker = tokio::time::interval(interval); + ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + ticker.tick().await; + loop { + tokio::select! { + _ = &mut stop => break, + _ = ticker.tick() => {} + } + + if servers + .values() + .any(|server| server.task.as_ref().is_some_and(JoinHandle::is_finished)) + { + shutdown_servers(&mut servers).await; + set_configured_status( + &status, + &config, + OpdsLifecycleState::Error, + Some(OpdsStatusError { + code: OpdsErrorCode::ListenerFailed, + message: "An OPDS listener stopped unexpectedly; Citadel will retry." + .to_string(), + }), + Vec::new(), + None, + ); + continue; + } + + let snapshot = match snapshot_interfaces(interfaces.clone()).await { + Ok(snapshot) => snapshot, + Err(_) => { + shutdown_servers(&mut servers).await; + set_configured_status( + &status, + &config, + OpdsLifecycleState::Error, + Some(interface_enumeration_error(true)), + Vec::new(), + None, + ); + continue; + } + }; + let library_id = active_library_id(source.clone()).await; + apply_plan( + &snapshot, + &config, + port, + &mut servers, + listeners.as_ref(), + tracker.clone(), + source.clone(), + auth.clone(), + &status, + library_id, + ) + .await; + } + shutdown_servers(&mut servers).await; +} + +async fn reap_finished_controller( + controller: &mut Option, + status: &Mutex, +) { + if !controller + .as_ref() + .is_some_and(|running| running.worker.is_finished()) + { + return; + } + let running = controller.take().expect("finished controller disappeared"); + let _ = running.worker.await; + set_simple_status( + status, + OpdsLifecycleState::Error, + Some(OpdsStatusError { + code: OpdsErrorCode::ListenerFailed, + message: "The OPDS service stopped unexpectedly. Start sharing again to retry." + .to_string(), + }), + Vec::new(), + ); +} + +async fn stop_controller( + controller: RunningController, + tracker: Arc, + worker_shutdown_timeout: Duration, +) -> bool { + let _ = controller.stop.send(()); + let mut worker = controller.worker; + if tokio::time::timeout(worker_shutdown_timeout, &mut worker) + .await + .is_err() + { + worker.abort(); + let _ = worker.await; + } + tokio::time::timeout(LISTENER_SHUTDOWN_TIMEOUT, tracker.wait_until_empty()) + .await + .is_ok() +} + +async fn shutdown_servers(servers: &mut BTreeMap) { + let mut servers = std::mem::take(servers).into_values().collect::>(); + for server in &mut servers { + if let Some(shutdown) = server.shutdown.take() { + let _ = shutdown.send(()); + } + } + let mut tasks = servers + .iter_mut() + .filter_map(|server| server.task.take()) + .collect::>(); + if tokio::time::timeout(LISTENER_SHUTDOWN_TIMEOUT, join_all(tasks.iter_mut())) + .await + .is_err() + { + for task in &tasks { + task.abort(); + } + let _ = join_all(tasks).await; + } +} + +fn status_snapshot(status: &Mutex) -> OpdsServiceStatus { + status.lock().expect("OPDS status mutex poisoned").clone() +} + +fn set_stopped_status(status: &Mutex) { + *status.lock().expect("OPDS status mutex poisoned") = OpdsServiceStatus::default(); +} + +fn set_simple_status( + status: &Mutex, + state: OpdsLifecycleState, + error: Option, + urls: Vec, +) { + let mut status = status.lock().expect("OPDS status mutex poisoned"); + status.state = state; + status.error = error; + status.urls = urls; +} + +fn set_configured_status( + status: &Mutex, + config: &OpdsStartConfig, + state: OpdsLifecycleState, + error: Option, + urls: Vec, + library_id: Option, +) { + let mut status = status.lock().expect("OPDS status mutex poisoned"); + status.state = state; + status.target = Some(config.target.clone()); + status.port = Some(config.port); + if library_id.is_some() { + status.active_library_id = library_id; + } + status.error = error; + status.urls = urls; +} + +fn urls(addresses: &BTreeSet) -> Vec { + addresses.iter().copied().map(advertised_url).collect() +} + +fn waiting_error(reason: &WaitingReason) -> OpdsStatusError { + OpdsStatusError { + code: OpdsErrorCode::InterfaceUnavailable, + message: reason.message(), + } +} + +fn interface_enumeration_error(retrying: bool) -> OpdsStatusError { + OpdsStatusError { + code: OpdsErrorCode::InterfaceEnumerationFailed, + message: if retrying { + "Citadel could not inspect network interfaces; it will retry." + } else { + "Citadel could not inspect network interfaces." + } + .to_string(), + } +} + +fn bind_error(error: io::Error) -> OpdsStatusError { + if error.kind() == io::ErrorKind::AddrInUse { + OpdsStatusError { + code: OpdsErrorCode::PortUnavailable, + message: "That port is already in use on the selected network interface.".to_string(), + } + } else { + OpdsStatusError { + code: OpdsErrorCode::Unexpected, + message: "Citadel could not open the requested OPDS listener.".to_string(), + } + } +} + +#[cfg(test)] +mod tests { + use std::{ + net::{IpAddr, Ipv4Addr}, + sync::atomic::{AtomicBool, Ordering}, + }; + + use super::*; + use crate::network::{AddressScope, InterfaceAddress, OpdsInterfaceKind, OpdsInterfaceState}; + + struct TestSource { + library_id: Option, + } + + impl CatalogSource for TestSource { + fn active_library_id(&self) -> Result { + self.library_id + .clone() + .ok_or(libcalibre::CalibreError::LibraryNotInitialized) + } + + fn book_page( + &self, + _limit: i64, + _offset: i64, + ) -> Result< + (String, Option, libcalibre::BookPage), + libcalibre::CalibreError, + > { + Ok(( + self.active_library_id()?, + None, + libcalibre::BookPage { + items: Vec::new(), + total: 0, + }, + )) + } + + fn book_file( + &self, + book_id: libcalibre::BookId, + format: &str, + ) -> Result { + Err(libcalibre::CalibreError::BookFileNotFound( + book_id, + format.to_string(), + )) + } + + fn book_cover( + &self, + book_id: libcalibre::BookId, + ) -> Result { + Err(libcalibre::CalibreError::BookCoverNotFound(book_id)) + } + } + + fn test_source() -> Arc { + Arc::new(TestSource { + library_id: Some("test-library".to_string()), + }) + } + + fn missing_source() -> Arc { + Arc::new(TestSource { library_id: None }) + } + + struct FakeInterfaces { + current: Mutex, io::ErrorKind>>, + } + + struct BlockingInterfaces { + snapshot: Vec, + entered: AtomicBool, + release: AtomicBool, + } + + struct HangingMonitorInterfaces { + snapshot: Vec, + calls: AtomicUsize, + release: AtomicBool, + } + + impl InterfaceSnapshots for HangingMonitorInterfaces { + fn snapshot(&self) -> io::Result> { + if self.calls.fetch_add(1, Ordering::AcqRel) != 0 { + while !self.release.load(Ordering::Acquire) { + std::thread::sleep(Duration::from_millis(1)); + } + } + Ok(self.snapshot.clone()) + } + } + + impl InterfaceSnapshots for BlockingInterfaces { + fn snapshot(&self) -> io::Result> { + self.entered.store(true, Ordering::Release); + while !self.release.load(Ordering::Acquire) { + std::thread::sleep(Duration::from_millis(1)); + } + Ok(self.snapshot.clone()) + } + } + + impl FakeInterfaces { + fn new(snapshot: Vec) -> Self { + Self { + current: Mutex::new(Ok(snapshot)), + } + } + + fn set(&self, snapshot: Vec) { + *self.current.lock().unwrap() = Ok(snapshot); + } + + fn set_error(&self, kind: io::ErrorKind) { + *self.current.lock().unwrap() = Err(kind); + } + } + + impl InterfaceSnapshots for FakeInterfaces { + fn snapshot(&self) -> io::Result> { + match &*self.current.lock().unwrap() { + Ok(snapshot) => Ok(snapshot.clone()), + Err(kind) => Err(io::Error::from(*kind)), + } + } + } + + #[derive(Default)] + struct FakeListeners { + active: Arc>>, + fail_bind: AtomicBool, + fail_task: AtomicBool, + hold_shutdown: Arc, + } + + impl ListenerFactory for FakeListeners { + fn start( + &self, + address: SocketAddr, + _source: Arc, + _auth: OpdsBasicAuth, + tracker: Arc, + ) -> io::Result { + if self.fail_bind.swap(false, Ordering::AcqRel) { + return Err(io::Error::from(io::ErrorKind::AddrInUse)); + } + self.active.lock().unwrap().insert(address); + let active = self.active.clone(); + let self_hold_shutdown = self.hold_shutdown.clone(); + let task_should_fail = self.fail_task.swap(false, Ordering::AcqRel); + let completion = tracker.register(); + let (shutdown, shutdown_receiver) = oneshot::channel(); + let task = tokio::spawn(async move { + let _completion = completion; + let _active = FakeActiveListener { active, address }; + let result = if task_should_fail { + Err(io::Error::from(io::ErrorKind::BrokenPipe)) + } else { + let _ = shutdown_receiver.await; + while self_hold_shutdown.load(Ordering::Acquire) { + tokio::time::sleep(Duration::from_millis(1)).await; + } + Ok(()) + }; + result + }); + Ok(ServerTask { + shutdown: Some(shutdown), + task: Some(task), + }) + } + } + + struct FakeActiveListener { + active: Arc>>, + address: SocketAddr, + } + + impl Drop for FakeActiveListener { + fn drop(&mut self) { + self.active.lock().unwrap().remove(&self.address); + } + } + + fn lan(state: OpdsInterfaceState, address: [u8; 4]) -> InterfaceSnapshot { + InterfaceSnapshot { + id: "en0".to_string(), + label: "Ethernet".to_string(), + state, + kind: OpdsInterfaceKind::Lan, + addresses: vec![InterfaceAddress { + address: IpAddr::V4(Ipv4Addr::from(address)), + scope: AddressScope::Private, + deprecated: false, + tentative: false, + duplicated: false, + }], + } + } + + fn config(port: u32) -> OpdsStartConfig { + OpdsStartConfig { + target: OpdsBindTarget::Interface { + id: "en0".to_string(), + }, + port, + authentication_enabled: false, + } + } + + fn service_with( + source: Arc, + interfaces: Arc, + listeners: Arc, + interval: Duration, + ) -> OpdsService { + OpdsService::with_dependencies( + source, + ServiceDependencies { + interfaces, + listeners, + monitor_interval: interval, + worker_shutdown_timeout: Duration::from_millis(100), + }, + ) + } + + async fn wait_for_state( + service: &OpdsService, + expected: OpdsLifecycleState, + ) -> OpdsServiceStatus { + for _ in 0..100 { + let status = service.status().await; + if status.state == expected { + return status; + } + tokio::time::sleep(Duration::from_millis(5)).await; + } + panic!( + "timed out waiting for {expected:?}; current status: {:?}", + service.status().await + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn selected_interface_loss_waits_without_broadening_and_recovers() { + let source = test_source(); + let interfaces = Arc::new(FakeInterfaces::new(vec![lan( + OpdsInterfaceState::Up, + [192, 168, 1, 5], + )])); + let listeners = Arc::new(FakeListeners::default()); + let service = service_with( + source, + interfaces.clone(), + listeners.clone(), + Duration::from_millis(10), + ); + + let running = service.start(config(8080)).await.unwrap(); + assert_eq!(running.state, OpdsLifecycleState::Running); + assert_eq!( + *listeners.active.lock().unwrap(), + BTreeSet::from(["192.168.1.5:8080".parse().unwrap()]) + ); + + interfaces.set(vec![lan(OpdsInterfaceState::Down, [192, 168, 1, 5])]); + let waiting = wait_for_state(&service, OpdsLifecycleState::WaitingForInterface).await; + assert_eq!( + waiting.error.unwrap().code, + OpdsErrorCode::InterfaceUnavailable + ); + assert!(listeners.active.lock().unwrap().is_empty()); + + interfaces.set(vec![lan(OpdsInterfaceState::Up, [192, 168, 1, 9])]); + let recovered = wait_for_state(&service, OpdsLifecycleState::Running).await; + assert_eq!(recovered.urls, vec!["http://192.168.1.9:8080/opds"]); + assert_eq!( + *listeners.active.lock().unwrap(), + BTreeSet::from(["192.168.1.9:8080".parse().unwrap()]) + ); + + assert_eq!(service.stop().await.state, OpdsLifecycleState::Stopped); + assert!(listeners.active.lock().unwrap().is_empty()); + } + + #[tokio::test(flavor = "multi_thread")] + async fn concurrent_same_config_starts_are_idempotent_and_conflicts_are_typed() { + let source = test_source(); + let interfaces = Arc::new(FakeInterfaces::new(vec![lan( + OpdsInterfaceState::Up, + [192, 168, 1, 5], + )])); + let listeners = Arc::new(FakeListeners::default()); + let service = service_with( + source, + interfaces, + listeners.clone(), + Duration::from_millis(10), + ); + + let requested = config(8080); + let (first, second) = tokio::join!( + service.start(requested.clone()), + service.start(requested.clone()) + ); + assert_eq!(first.unwrap().state, OpdsLifecycleState::Running); + assert_eq!(second.unwrap().state, OpdsLifecycleState::Running); + assert_eq!(listeners.active.lock().unwrap().len(), 1); + + let conflict = service.start(config(8081)).await.unwrap_err(); + assert_eq!(conflict.code, OpdsErrorCode::ConfigurationConflict); + assert_eq!(service.status().await.port, Some(8080)); + service.stop().await; + assert_eq!(service.stop().await.state, OpdsLifecycleState::Stopped); + } + + #[tokio::test(flavor = "multi_thread")] + async fn credentials_are_backend_owned_and_cannot_change_while_running() { + let source = test_source(); + let interfaces = Arc::new(FakeInterfaces::new(vec![lan( + OpdsInterfaceState::Up, + [192, 168, 1, 5], + )])); + let service = service_with( + source, + interfaces, + Arc::new(FakeListeners::default()), + Duration::from_secs(1), + ); + + let status = service + .configure_credentials(" reader ".to_string(), "secret".to_string()) + .await + .unwrap(); + assert_eq!(status.username.as_deref(), Some("reader")); + assert!(status.configured); + assert!(service + .inner + .credentials + .get() + .unwrap() + .password_verifier + .starts_with("$argon2id$")); + + let mut authenticated = config(8080); + authenticated.authentication_enabled = true; + assert_eq!( + service.start(authenticated).await.unwrap().state, + OpdsLifecycleState::Running + ); + assert_eq!( + service.clear_credentials().await.unwrap_err().code, + OpdsErrorCode::ConfigurationConflict + ); + service.stop().await; + assert!(!service.clear_credentials().await.unwrap().configured); + } + + #[tokio::test(flavor = "multi_thread")] + async fn authentication_requires_configured_credentials() { + let source = test_source(); + let service = service_with( + source, + Arc::new(FakeInterfaces::new(vec![lan( + OpdsInterfaceState::Up, + [192, 168, 1, 5], + )])), + Arc::new(FakeListeners::default()), + Duration::from_secs(1), + ); + let mut authenticated = config(8080); + authenticated.authentication_enabled = true; + + assert_eq!( + service.start(authenticated).await.unwrap_err().code, + OpdsErrorCode::CredentialsRequired + ); + assert_eq!(service.status().await.state, OpdsLifecycleState::Stopped); + } + + #[tokio::test(flavor = "multi_thread")] + async fn credential_validation_and_generation_have_defined_whitespace_behavior() { + let service = service_with( + missing_source(), + Arc::new(FakeInterfaces::new(Vec::new())), + Arc::new(FakeListeners::default()), + Duration::from_secs(1), + ); + + for username in ["", " ", "read:er", "read\ner"] { + assert_eq!( + service + .configure_credentials(username.to_string(), "secret".to_string()) + .await + .unwrap_err() + .code, + OpdsErrorCode::InvalidCredentials + ); + } + assert_eq!( + service + .configure_credentials("reader".to_string(), String::new()) + .await + .unwrap_err() + .code, + OpdsErrorCode::InvalidCredentials + ); + + service + .configure_credentials(" reader ".to_string(), " ".to_string()) + .await + .unwrap(); + assert_eq!( + service.credential_status().username.as_deref(), + Some("reader") + ); + + let generated = service + .generate_credentials("reader".to_string()) + .await + .unwrap(); + assert_eq!(generated.password.len(), 24); + assert!(!service + .inner + .credentials + .get() + .unwrap() + .password_verifier + .contains(&generated.password)); + } + + #[tokio::test(flavor = "multi_thread")] + async fn invalid_port_missing_library_and_occupied_port_are_actionable() { + let interfaces = Arc::new(FakeInterfaces::new(vec![lan( + OpdsInterfaceState::Up, + [192, 168, 1, 5], + )])); + let listeners = Arc::new(FakeListeners::default()); + let missing = service_with( + missing_source(), + interfaces.clone(), + listeners.clone(), + Duration::from_secs(1), + ); + let status = missing.start(config(8080)).await.unwrap(); + assert_eq!(status.error.unwrap().code, OpdsErrorCode::LibraryNotReady); + + let source = test_source(); + let service = service_with( + source, + interfaces, + listeners.clone(), + Duration::from_secs(1), + ); + let invalid = service.start(config(70_000)).await.unwrap(); + assert_eq!(invalid.error.unwrap().code, OpdsErrorCode::InvalidPort); + + listeners.fail_bind.store(true, Ordering::Release); + let occupied = service.start(config(8080)).await.unwrap(); + assert_eq!(occupied.error.unwrap().code, OpdsErrorCode::PortUnavailable); + service.stop().await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn listener_task_failure_is_observable_and_then_retried() { + let source = test_source(); + let interfaces = Arc::new(FakeInterfaces::new(vec![lan( + OpdsInterfaceState::Up, + [192, 168, 1, 5], + )])); + let listeners = Arc::new(FakeListeners::default()); + listeners.fail_task.store(true, Ordering::Release); + let service = service_with( + source, + interfaces, + listeners.clone(), + Duration::from_millis(50), + ); + + service.start(config(8080)).await.unwrap(); + let failed = wait_for_state(&service, OpdsLifecycleState::Error).await; + assert_eq!(failed.error.unwrap().code, OpdsErrorCode::ListenerFailed); + wait_for_state(&service, OpdsLifecycleState::Running).await; + assert_eq!(listeners.active.lock().unwrap().len(), 1); + service.stop().await; + assert!(listeners.active.lock().unwrap().is_empty()); + } + + #[tokio::test(flavor = "multi_thread")] + async fn interface_enumeration_failure_is_actionable_and_retried() { + let source = test_source(); + let interfaces = Arc::new(FakeInterfaces::new(Vec::new())); + interfaces.set_error(io::ErrorKind::Other); + let listeners = Arc::new(FakeListeners::default()); + let service = service_with( + source, + interfaces.clone(), + listeners, + Duration::from_millis(10), + ); + + let failed = service.start(config(8080)).await.unwrap(); + assert_eq!( + failed.error.unwrap().code, + OpdsErrorCode::InterfaceEnumerationFailed + ); + interfaces.set(vec![lan(OpdsInterfaceState::Up, [192, 168, 1, 5])]); + wait_for_state(&service, OpdsLifecycleState::Running).await; + service.stop().await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn starting_and_stopping_are_observable_during_blocked_dependencies() { + let source = test_source(); + let interfaces = Arc::new(BlockingInterfaces { + snapshot: vec![lan(OpdsInterfaceState::Up, [192, 168, 1, 5])], + entered: AtomicBool::new(false), + release: AtomicBool::new(false), + }); + let listeners = Arc::new(FakeListeners::default()); + let service = service_with( + source, + interfaces.clone(), + listeners.clone(), + Duration::from_secs(1), + ); + + let starting_service = service.clone(); + let start = tokio::spawn(async move { starting_service.start(config(8080)).await }); + while !interfaces.entered.load(Ordering::Acquire) { + tokio::task::yield_now().await; + } + assert_eq!(service.status().await.state, OpdsLifecycleState::Starting); + interfaces.release.store(true, Ordering::Release); + assert_eq!( + start.await.unwrap().unwrap().state, + OpdsLifecycleState::Running + ); + + listeners.hold_shutdown.store(true, Ordering::Release); + let stopping_service = service.clone(); + let stop = tokio::spawn(async move { stopping_service.stop().await }); + wait_for_state(&service, OpdsLifecycleState::Stopping).await; + listeners.hold_shutdown.store(false, Ordering::Release); + assert_eq!(stop.await.unwrap().state, OpdsLifecycleState::Stopped); + } + + #[tokio::test(flavor = "multi_thread")] + async fn real_listener_rejects_occupied_port_and_releases_it_on_stop() { + let occupied = StdTcpListener::bind((Ipv4Addr::LOCALHOST, 0)).unwrap(); + let address = occupied.local_addr().unwrap(); + let factory = TcpListenerFactory; + let tracker = Arc::new(ListenerTracker::default()); + let error = match factory.start( + address, + missing_source(), + OpdsBasicAuth::disabled(), + tracker.clone(), + ) { + Ok(_) => panic!("occupied listener unexpectedly bound"), + Err(error) => error, + }; + assert_eq!(error.kind(), io::ErrorKind::AddrInUse); + drop(occupied); + + let mut servers = BTreeMap::from([( + address, + factory + .start( + address, + missing_source(), + OpdsBasicAuth::disabled(), + tracker.clone(), + ) + .unwrap(), + )]); + shutdown_servers(&mut servers).await; + tracker.wait_until_empty().await; + assert!(StdTcpListener::bind(address).is_ok()); + } + + #[tokio::test(flavor = "multi_thread")] + async fn forced_worker_abort_awaits_listener_cancellation_and_releases_port() { + let probe = StdTcpListener::bind((Ipv4Addr::LOCALHOST, 0)).unwrap(); + let port = probe.local_addr().unwrap().port(); + drop(probe); + let interfaces = Arc::new(HangingMonitorInterfaces { + snapshot: vec![lan(OpdsInterfaceState::Up, [127, 0, 0, 1])], + calls: AtomicUsize::new(0), + release: AtomicBool::new(false), + }); + let source = test_source(); + let service = service_with( + source, + interfaces.clone(), + Arc::new(TcpListenerFactory), + Duration::from_millis(5), + ); + assert_eq!( + service.start(config(u32::from(port))).await.unwrap().state, + OpdsLifecycleState::Running + ); + while interfaces.calls.load(Ordering::Acquire) < 2 { + tokio::task::yield_now().await; + } + + assert_eq!(service.stop().await.state, OpdsLifecycleState::Stopped); + assert!(StdTcpListener::bind((Ipv4Addr::LOCALHOST, port)).is_ok()); + interfaces.release.store(true, Ordering::Release); + } + + #[tokio::test(flavor = "multi_thread")] + async fn listener_completion_cannot_be_lost_between_count_check_and_wait() { + for _ in 0..1_000 { + let tracker = Arc::new(ListenerTracker::default()); + let completion = tracker.register(); + let waiter_tracker = tracker.clone(); + let waiter = tokio::spawn(async move { waiter_tracker.wait_until_empty().await }); + tokio::task::yield_now().await; + drop(completion); + tokio::time::timeout(Duration::from_millis(100), waiter) + .await + .expect("listener completion notification was lost") + .unwrap(); + } + } +} diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 1bc24d6b..48023ad5 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -19,6 +19,7 @@ chrono = { version = "0.4.31", features = ["serde"] } diesel = { version = "2.1.0", features = ["sqlite", "chrono", "returning_clauses_for_sqlite_3_35"] } epub = "2.1.1" mobi = "0.8.0" +netdev = { version = "=0.45.0", default-features = false } citadel-core = { path = "../crates/citadel-core" } citadel-opds = { path = "../crates/citadel-opds" } libcalibre = { path = "../crates/libcalibre" } @@ -46,6 +47,7 @@ tauri-plugin-opener = "2" tauri-plugin-updater = "2" tauri-plugin-webdriver-automation = "0.1.3" image = { version = "0.25.6", default-features = false, features = ["jpeg", "png"] } +tokio = { version = "1.52.3", features = ["macros", "net", "rt-multi-thread", "sync", "time"] } [dev-dependencies] tempfile = "3.8" diff --git a/src-tauri/src/app_updates.rs b/src-tauri/src/app_updates.rs index 745a83dc..997cea44 100644 --- a/src-tauri/src/app_updates.rs +++ b/src-tauri/src/app_updates.rs @@ -1,5 +1,7 @@ use tauri_plugin_updater::UpdaterExt; +use citadel_opds::OpdsService; + #[derive(serde::Serialize, specta::Type)] pub struct UpdateCheckResult { pub has_update: bool, @@ -28,7 +30,10 @@ pub async fn clb_cmd_check_for_updates(app: tauri::AppHandle) -> Result Result { +pub async fn clb_cmd_install_update_if_available( + app: tauri::AppHandle, + opds: tauri::State<'_, OpdsService>, +) -> Result { let updater = app .updater() .map_err(|err| format!("Updater initialization failed: {err}"))?; @@ -42,6 +47,7 @@ pub async fn clb_cmd_install_update_if_available(app: tauri::AppHandle) -> Resul .await .map_err(|err| format!("Failed to download/install update: {err}"))?; + opds.stop().await; app.restart(); } Ok(None) => Ok("no-update".to_string()), diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index d741b232..597085dd 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,6 +1,11 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +use std::sync::{ + atomic::{AtomicU8, Ordering}, + Arc, +}; + use libs::calibre; #[cfg(debug_assertions)] use specta_typescript::Typescript; @@ -16,6 +21,7 @@ pub mod libs { mod book; mod menu; mod metadata; +pub mod opds; mod state; fn run_tauri_backend() -> std::io::Result<()> { @@ -64,6 +70,15 @@ fn run_tauri_backend() -> std::io::Result<()> { metadata::commands::clb_query_metadata_by_isbn, app_updates::clb_cmd_check_for_updates, app_updates::clb_cmd_install_update_if_available, + // OPDS sharing commands + opds::commands::clb_query_opds_interfaces, + opds::commands::clb_cmd_start_opds, + opds::commands::clb_cmd_stop_opds, + opds::commands::clb_query_opds_status, + opds::commands::clb_query_opds_credential_status, + opds::commands::clb_cmd_configure_opds_credentials, + opds::commands::clb_cmd_generate_opds_credentials, + opds::commands::clb_cmd_clear_opds_credentials, // Window commands menu::clb_cmd_open_settings, ]); @@ -86,14 +101,21 @@ fn run_tauri_backend() -> std::io::Result<()> { tauri_builder = tauri_builder.plugin(tauri_plugin_webdriver_automation::init()); } - tauri_builder + let state = state::CitadelState::new(); + let opds_state = state.clone(); + let app = tauri_builder .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_updater::Builder::new().build()) - .manage(state::CitadelState::new()) + .manage(state) .invoke_handler(builder.invoke_handler()) .plugin(tauri_plugin_store::Builder::new().build()) .setup(move |app| { builder.mount_events(app); + let credential_path = app.path().app_data_dir()?.join("opds-credentials.json"); + app.manage(citadel_opds::OpdsService::new( + Arc::new(opds_state.clone()), + credential_path, + )?); // Native macOS menu bar: app menu with Settings…, File > Add // Book…, and the standard Edit/View/Window items. @@ -140,8 +162,39 @@ fn run_tauri_backend() -> std::io::Result<()> { .plugin(tauri_plugin_drag::init()) .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_clipboard_manager::init()) - .run(tauri::generate_context!()) - .expect("error while running tauri application"); + .build(tauri::generate_context!()) + .expect("error while building tauri application"); + + const RUNNING: u8 = 0; + const STOPPING: u8 = 1; + const EXITING: u8 = 2; + let exit_phase = Arc::new(AtomicU8::new(RUNNING)); + app.run(move |app_handle, event| { + if let tauri::RunEvent::ExitRequested { code, api, .. } = event { + match exit_phase.compare_exchange( + RUNNING, + STOPPING, + Ordering::AcqRel, + Ordering::Acquire, + ) { + Ok(_) => { + api.prevent_exit(); + let app_handle = app_handle.clone(); + let opds_service = app_handle.state::().inner().clone(); + let exit_phase = exit_phase.clone(); + let exit_code = code.unwrap_or(0); + tauri::async_runtime::spawn(async move { + opds_service.stop().await; + exit_phase.store(EXITING, Ordering::Release); + app_handle.exit(exit_code); + }); + } + Err(STOPPING) => api.prevent_exit(), + Err(EXITING) => {} + Err(_) => unreachable!(), + } + } + }); Ok(()) } diff --git a/src-tauri/src/opds/commands.rs b/src-tauri/src/opds/commands.rs new file mode 100644 index 00000000..f40cecc3 --- /dev/null +++ b/src-tauri/src/opds/commands.rs @@ -0,0 +1,74 @@ +use citadel_opds::{ + credentials::{GeneratedOpdsCredentials, OpdsCredentialStatus}, + network::OpdsNetworkInterface, + service::{OpdsServiceStatus, OpdsStartConfig, OpdsStatusError}, + OpdsService, +}; + +#[tauri::command] +#[specta::specta] +pub fn clb_query_opds_credential_status( + service: tauri::State<'_, OpdsService>, +) -> Result { + Ok(service.credential_status()) +} + +#[tauri::command] +#[specta::specta] +pub async fn clb_cmd_configure_opds_credentials( + service: tauri::State<'_, OpdsService>, + username: String, + password: String, +) -> Result { + service.configure_credentials(username, password).await +} + +#[tauri::command] +#[specta::specta] +pub async fn clb_cmd_generate_opds_credentials( + service: tauri::State<'_, OpdsService>, + username: String, +) -> Result { + service.generate_credentials(username).await +} + +#[tauri::command] +#[specta::specta] +pub async fn clb_cmd_clear_opds_credentials( + service: tauri::State<'_, OpdsService>, +) -> Result { + service.clear_credentials().await +} + +#[tauri::command] +#[specta::specta] +pub async fn clb_query_opds_interfaces( + service: tauri::State<'_, OpdsService>, +) -> Result, OpdsStatusError> { + service.list_interfaces().await +} + +#[tauri::command] +#[specta::specta] +pub async fn clb_cmd_start_opds( + service: tauri::State<'_, OpdsService>, + config: OpdsStartConfig, +) -> Result { + service.start(config).await +} + +#[tauri::command] +#[specta::specta] +pub async fn clb_cmd_stop_opds( + service: tauri::State<'_, OpdsService>, +) -> Result { + Ok(service.stop().await) +} + +#[tauri::command] +#[specta::specta] +pub async fn clb_query_opds_status( + service: tauri::State<'_, OpdsService>, +) -> Result { + Ok(service.status().await) +} diff --git a/src-tauri/src/opds/mod.rs b/src-tauri/src/opds/mod.rs new file mode 100644 index 00000000..a0da9712 --- /dev/null +++ b/src-tauri/src/opds/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod commands; + +pub use citadel_opds::*; diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index 954aba8d..5f20151f 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -1,37 +1,71 @@ -use std::sync::Mutex; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, Mutex, +}; use chrono::NaiveDateTime; use libcalibre::{BookId, BookPage, CalibreError, Library, ResolvedBookAsset}; use citadel_opds::CatalogSource; +#[derive(Clone)] pub struct CitadelState { + inner: Arc, +} + +struct CitadelStateInner { library: Mutex>, current_library_path: Mutex>, + library_switch: Mutex<()>, + library_transitioning: AtomicBool, } impl CitadelState { pub fn new() -> Self { Self { - library: Mutex::new(None), - current_library_path: Mutex::new(None), + inner: Arc::new(CitadelStateInner { + library: Mutex::new(None), + current_library_path: Mutex::new(None), + library_switch: Mutex::new(()), + library_transitioning: AtomicBool::new(false), + }), } } /// Initialize or switch to a library pub fn init_library(&self, library_path: String) -> Result<(), String> { - let db_path = libcalibre::util::get_db_path(&library_path) - .ok_or_else(|| format!("Invalid library path: {}", library_path))?; - - let lib = Library::new(db_path).map_err(|e| format!("Failed to open library: {}", e))?; - - *self.library.lock().expect("Library mutex poisoned") = Some(lib); - *self - .current_library_path + let _switch = self + .inner + .library_switch .lock() - .expect("Library path mutex poisoned") = Some(library_path); + .expect("Library switch mutex poisoned"); + { + let _library = self.inner.library.lock().expect("Library mutex poisoned"); + self.inner + .library_transitioning + .store(true, Ordering::Release); + } + let result = (|| { + let db_path = libcalibre::util::get_db_path(&library_path) + .ok_or_else(|| format!("Invalid library path: {}", library_path))?; + let library = Library::new(db_path) + .map_err(|error| format!("Failed to open library: {error}"))?; - Ok(()) + *self.inner.library.lock().expect("Library mutex poisoned") = Some(library); + *self + .inner + .current_library_path + .lock() + .expect("Library path mutex poisoned") = Some(library_path); + Ok(()) + })(); + { + let _library = self.inner.library.lock().expect("Library mutex poisoned"); + self.inner + .library_transitioning + .store(false, Ordering::Release); + } + result } /// Execute a function with mutable access to the library @@ -39,7 +73,7 @@ impl CitadelState { where F: FnOnce(&mut Library) -> R, { - let mut lib_guard = self.library.lock().expect("Library mutex poisoned"); + let mut lib_guard = self.inner.library.lock().expect("Library mutex poisoned"); match lib_guard.as_mut() { Some(lib) => Ok(f(lib)), None => Err("No library initialized. Please load a library first.".to_string()), @@ -48,7 +82,8 @@ impl CitadelState { /// Get the current library path pub fn get_library_path(&self) -> Option { - self.current_library_path + self.inner + .current_library_path .lock() .expect("Library path mutex poisoned") .clone() @@ -56,10 +91,23 @@ impl CitadelState { /// Check if a library is currently loaded pub fn is_initialized(&self) -> bool { - self.library - .lock() - .expect("Library mutex poisoned") - .is_some() + let library = self.inner.library.lock().expect("Library mutex poisoned"); + !self.is_library_transitioning() && library.is_some() + } + + pub fn is_library_transitioning(&self) -> bool { + self.inner.library_transitioning.load(Ordering::Acquire) + } + + pub fn active_library_id(&self) -> Result { + let mut library = self.inner.library.lock().expect("Library mutex poisoned"); + if self.is_library_transitioning() { + return Err(CalibreError::LibraryNotInitialized); + } + library + .as_mut() + .ok_or(CalibreError::LibraryNotInitialized)? + .library_uuid() } /// Resolve under the library mutex and return only owned data. Opening and @@ -69,7 +117,10 @@ impl CitadelState { book_id: BookId, format: &str, ) -> Result { - let mut library = self.library.lock().expect("Library mutex poisoned"); + let mut library = self.inner.library.lock().expect("Library mutex poisoned"); + if self.is_library_transitioning() { + return Err(CalibreError::LibraryNotInitialized); + } library .as_mut() .ok_or(CalibreError::LibraryNotInitialized)? @@ -77,7 +128,10 @@ impl CitadelState { } pub fn resolve_book_cover(&self, book_id: BookId) -> Result { - let mut library = self.library.lock().expect("Library mutex poisoned"); + let mut library = self.inner.library.lock().expect("Library mutex poisoned"); + if self.is_library_transitioning() { + return Err(CalibreError::LibraryNotInitialized); + } library .as_mut() .ok_or(CalibreError::LibraryNotInitialized)? @@ -89,7 +143,10 @@ impl CitadelState { limit: i64, offset: i64, ) -> Result<(String, Option, BookPage), CalibreError> { - let mut library = self.library.lock().expect("Library mutex poisoned"); + let mut library = self.inner.library.lock().expect("Library mutex poisoned"); + if self.is_library_transitioning() { + return Err(CalibreError::LibraryNotInitialized); + } let library = library .as_mut() .ok_or(CalibreError::LibraryNotInitialized)?; @@ -121,3 +178,141 @@ impl CatalogSource for CitadelState { self.resolve_book_cover(book_id) } } + +#[cfg(test)] +mod tests { + use std::{collections::HashMap, path::Path, time::Duration}; + + use axum::http::StatusCode; + + use super::*; + + fn extract_empty_library() -> tempfile::TempDir { + let directory = tempfile::tempdir().unwrap(); + let zip_path = + Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/empty_7_2_calibre_lib.zip"); + let file = std::fs::File::open(zip_path).unwrap(); + zip::ZipArchive::new(file) + .unwrap() + .extract(directory.path()) + .unwrap(); + directory + } + + fn add_asset(directory: &Path, byte: u8, length: usize) -> BookId { + let root = directory.to_string_lossy().into_owned(); + let database = libcalibre::util::get_db_path(&root).unwrap(); + let mut library = libcalibre::Library::new(database).unwrap(); + let source = directory.join(format!("source-{byte}.epub")); + std::fs::write(&source, vec![byte; length]).unwrap(); + let book = library + .add_book(libcalibre::BookAdd { + title: format!("Library {byte}"), + author_names: vec!["Citadel Test".to_string()], + tags: None, + series: None, + series_index: None, + publisher: None, + publication_date: None, + rating: None, + comments: None, + identifiers: HashMap::new(), + language: Some("eng".to_string()), + file_paths: vec![source], + }) + .unwrap(); + book.id + } + + #[tokio::test(flavor = "multi_thread")] + async fn running_router_switches_library_at_same_url_and_is_unavailable_during_transition() { + let first = extract_empty_library(); + let second = extract_empty_library(); + let first_book = add_asset(first.path(), b'A', 2 * 1024 * 1024); + let second_book = add_asset(second.path(), b'B', 2 * 1024 * 1024); + assert_eq!(first_book, second_book); + let second_path = second.path().to_string_lossy().into_owned(); + let second_db = libcalibre::util::get_db_path(&second_path).unwrap(); + libcalibre::Library::new(second_db) + .unwrap() + .randomize_library_uuid() + .unwrap(); + + let state = CitadelState::new(); + state + .init_library(first.path().to_string_lossy().into_owned()) + .unwrap(); + let first_id = state.active_library_id().unwrap(); + let listener = tokio::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, 0)) + .await + .unwrap(); + let base = format!("http://{}", listener.local_addr().unwrap()); + let server_state = state.clone(); + let server = tokio::spawn(async move { + axum::serve( + listener, + citadel_opds::router( + Arc::new(server_state), + citadel_opds::OpdsBasicAuth::disabled(), + ), + ) + .await + .unwrap(); + }); + let client = reqwest::Client::new(); + + let first_response = client.get(format!("{base}/opds")).send().await.unwrap(); + assert_eq!(first_response.status(), StatusCode::OK); + assert!(first_response.text().await.unwrap().contains(&first_id)); + + let asset_url = format!( + "{base}/opds/books/{}/files/EPUB/book.epub", + first_book.as_i32() + ); + let old_download = client.get(&asset_url).send().await.unwrap(); + assert_eq!(old_download.status(), StatusCode::OK); + let responsive_feed = tokio::time::timeout( + Duration::from_secs(1), + client.get(format!("{base}/opds")).send(), + ) + .await + .expect("a stalled large download must not block feed requests") + .unwrap(); + assert_eq!(responsive_feed.status(), StatusCode::OK); + + state + .inner + .library_transitioning + .store(true, Ordering::Release); + let transition_response = reqwest::get(format!("{base}/opds")).await.unwrap(); + assert_eq!( + transition_response.status(), + StatusCode::SERVICE_UNAVAILABLE + ); + state + .inner + .library_transitioning + .store(false, Ordering::Release); + + state.init_library(second_path).unwrap(); + let second_id = state.active_library_id().unwrap(); + assert_ne!(first_id, second_id); + let second_response = client.get(format!("{base}/opds")).send().await.unwrap(); + assert_eq!(second_response.status(), StatusCode::OK); + assert!(second_response.text().await.unwrap().contains(&second_id)); + let new_bytes = client + .get(&asset_url) + .send() + .await + .unwrap() + .bytes() + .await + .unwrap(); + assert_eq!(new_bytes.len(), 2 * 1024 * 1024); + assert!(new_bytes.iter().all(|byte| *byte == b'B')); + let old_bytes = old_download.bytes().await.unwrap(); + assert_eq!(old_bytes.len(), 2 * 1024 * 1024); + assert!(old_bytes.iter().all(|byte| *byte == b'A')); + server.abort(); + } +} diff --git a/src/bindings.ts b/src/bindings.ts index 7d82df85..052387af 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -289,6 +289,38 @@ async clbCmdInstallUpdateIfAvailable() : Promise> { else return { status: "error", error: e as any }; } }, +async clbQueryOpdsInterfaces() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("clb_query_opds_interfaces") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async clbCmdStartOpds(config: OpdsStartConfig) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("clb_cmd_start_opds", { config }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async clbCmdStopOpds() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("clb_cmd_stop_opds") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async clbQueryOpdsStatus() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("clb_query_opds_status") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, async clbCmdOpenSettings() : Promise { await TAURI_INVOKE("clb_cmd_open_settings"); } @@ -490,6 +522,15 @@ export type LocalOrRemoteUrl = { kind: LocalOrRemote; url: string; local_path: s */ export type MetadataProvider = "hardcover" | "loc" | "dnb" | "k10plus" | "openlibrary" export type NewAuthor = { name: string; sortable_name: string | null } +export type OpdsBindTarget = { type: "allLocalNetworks" } | { type: "interface"; id: string } +export type OpdsErrorCode = "invalidPort" | "libraryNotReady" | "configurationConflict" | "interfaceUnavailable" | "interfaceEnumerationFailed" | "portUnavailable" | "listenerFailed" | "unexpected" +export type OpdsInterfaceKind = "lan" | "vpn" | "loopback" | "other" +export type OpdsInterfaceState = "up" | "down" +export type OpdsLifecycleState = "stopped" | "starting" | "running" | "waitingForInterface" | "error" | "stopping" +export type OpdsNetworkInterface = { id: string; label: string; kind: OpdsInterfaceKind; state: OpdsInterfaceState; addresses: string[]; shareable: boolean } +export type OpdsServiceStatus = { state: OpdsLifecycleState; target: OpdsBindTarget | null; port: number | null; activeLibraryId: string | null; urls: string[]; error: OpdsStatusError | null } +export type OpdsStartConfig = { target: OpdsBindTarget; port: number } +export type OpdsStatusError = { code: OpdsErrorCode; message: string } export type ProviderStatus = { provider: MetadataProvider; is_valid: boolean; message: string } export type RemoteFile = { url: string } /**