diff --git a/.mise/tasks/test b/.mise/tasks/test index 9f59fb4c..848ad35d 100755 --- a/.mise/tasks/test +++ b/.mise/tasks/test @@ -2,4 +2,7 @@ #MISE description="Test project" set -e +: "${TEST_DB:=REFERENCE}" +export TEST_DB + cargo nextest run diff --git a/Cargo.lock b/Cargo.lock index f6816950..958eb695 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -85,6 +85,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + [[package]] name = "aligned-vec" version = "0.6.4" @@ -94,6 +103,21 @@ dependencies = [ "equator", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -122,7 +146,7 @@ dependencies = [ "bytes 1.10.1", "serde", "serde_bytes_ng", - "tokio 1.47.1", + "tokio 1.49.0", ] [[package]] @@ -178,6 +202,15 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "async-attributes" version = "1.1.2" @@ -239,7 +272,7 @@ dependencies = [ "futures-lite 2.6.1", "once_cell", "tokio 0.2.25", - "tokio 1.47.1", + "tokio 1.49.0", ] [[package]] @@ -496,6 +529,26 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.18", + "v_frame", + "y4m", +] + [[package]] name = "av1-grain" version = "0.2.4" @@ -543,7 +596,7 @@ dependencies = [ "http 1.3.1", "ring", "time", - "tokio 1.47.1", + "tokio 1.49.0", "tracing", "url", "zeroize", @@ -746,7 +799,7 @@ checksum = "1e190749ea56f8c42bf15dd76c65e14f8f765233e6df9b0506d9d934ebef867c" dependencies = [ "futures-util", "pin-project-lite 0.2.16", - "tokio 1.47.1", + "tokio 1.49.0", ] [[package]] @@ -825,7 +878,7 @@ dependencies = [ "rustls 0.23.32", "rustls-native-certs 0.8.1", "rustls-pki-types", - "tokio 1.47.1", + "tokio 1.49.0", "tokio-rustls 0.26.3", "tower", "tracing", @@ -879,7 +932,7 @@ dependencies = [ "http-body 1.0.1", "pin-project-lite 0.2.16", "pin-utils", - "tokio 1.47.1", + "tokio 1.49.0", "tracing", ] @@ -895,7 +948,7 @@ dependencies = [ "http 0.2.12", "http 1.3.1", "pin-project-lite 0.2.16", - "tokio 1.47.1", + "tokio 1.49.0", "tracing", "zeroize", ] @@ -922,7 +975,7 @@ dependencies = [ "ryu", "serde", "time", - "tokio 1.47.1", + "tokio 1.49.0", "tokio-util", ] @@ -978,7 +1031,7 @@ dependencies = [ "serde_path_to_error", "serde_urlencoded", "sync_wrapper 1.0.2", - "tokio 1.47.1", + "tokio 1.49.0", "tower", "tower-layer", "tower-service", @@ -1056,7 +1109,7 @@ dependencies = [ "futures-util", "tempfile", "thiserror 1.0.69", - "tokio 1.47.1", + "tokio 1.49.0", "uuid", ] @@ -1214,9 +1267,12 @@ checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" [[package]] name = "bitstream-io" -version = "2.6.0" +version = "4.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" +checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757" +dependencies = [ + "core2", +] [[package]] name = "bitvec" @@ -1263,6 +1319,16 @@ dependencies = [ "piper", ] +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bson" version = "2.15.0" @@ -1288,9 +1354,9 @@ dependencies = [ [[package]] name = "built" -version = "0.7.7" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" +checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" [[package]] name = "bumpalo" @@ -1352,7 +1418,7 @@ dependencies = [ "instant", "once_cell", "thiserror 1.0.69", - "tokio 1.47.1", + "tokio 1.49.0", ] [[package]] @@ -1412,16 +1478,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "cfg-expr" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" -dependencies = [ - "smallvec", - "target-lexicon", -] - [[package]] name = "cfg-if" version = "1.0.3" @@ -1508,7 +1564,7 @@ dependencies = [ "futures-core", "memchr", "pin-project-lite 0.2.16", - "tokio 1.47.1", + "tokio 1.49.0", "tokio-util", ] @@ -1630,6 +1686,15 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +dependencies = [ + "memchr", +] + [[package]] name = "core_maths" version = "0.1.1" @@ -2016,7 +2081,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ff1266be84e4e04a81e2d1cbb998cb271b374fb73ce780245ef96c037c50cd" dependencies = [ "crossbeam-queue", - "tokio 1.47.1", + "tokio 1.49.0", ] [[package]] @@ -2473,9 +2538,9 @@ dependencies = [ [[package]] name = "exr" -version = "1.73.0" +version = "1.74.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" dependencies = [ "bit_field", "half", @@ -2714,7 +2779,7 @@ dependencies = [ "redis-protocol", "semver", "socket2 0.5.10", - "tokio 1.47.1", + "tokio 1.49.0", "tokio-stream", "tokio-util", "url", @@ -2833,7 +2898,7 @@ checksum = "45ec6fe3675af967e67c5536c0b9d44e34e6c52f86bedc4ea49c5317b8e94d06" dependencies = [ "futures-channel", "futures-task", - "tokio 1.47.1", + "tokio 1.49.0", ] [[package]] @@ -3007,6 +3072,16 @@ dependencies = [ "weezl", ] +[[package]] +name = "gif" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gimli" version = "0.31.1" @@ -3080,7 +3155,7 @@ dependencies = [ "http 0.2.12", "indexmap 2.13.0", "slab", - "tokio 1.47.1", + "tokio 1.49.0", "tokio-util", "tracing", ] @@ -3099,7 +3174,7 @@ dependencies = [ "http 1.3.1", "indexmap 2.13.0", "slab", - "tokio 1.47.1", + "tokio 1.49.0", "tokio-util", "tracing", ] @@ -3251,7 +3326,7 @@ dependencies = [ "rand 0.8.5", "thiserror 1.0.69", "tinyvec", - "tokio 1.47.1", + "tokio 1.49.0", "tracing", "url", ] @@ -3273,7 +3348,7 @@ dependencies = [ "resolv-conf", "smallvec", "thiserror 1.0.69", - "tokio 1.47.1", + "tokio 1.49.0", "tracing", ] @@ -3439,7 +3514,7 @@ dependencies = [ "itoa", "pin-project-lite 0.2.16", "socket2 0.5.10", - "tokio 1.47.1", + "tokio 1.49.0", "tower-service", "tracing", "want", @@ -3464,7 +3539,7 @@ dependencies = [ "pin-project-lite 0.2.16", "pin-utils", "smallvec", - "tokio 1.47.1", + "tokio 1.49.0", "want", ] @@ -3480,7 +3555,7 @@ dependencies = [ "log", "rustls 0.21.12", "rustls-native-certs 0.6.3", - "tokio 1.47.1", + "tokio 1.49.0", "tokio-rustls 0.24.1", ] @@ -3496,7 +3571,7 @@ dependencies = [ "hyper-util", "rustls 0.22.4", "rustls-pki-types", - "tokio 1.47.1", + "tokio 1.49.0", "tokio-rustls 0.25.0", "tower-service", "webpki-roots 0.26.11", @@ -3514,7 +3589,7 @@ dependencies = [ "rustls 0.23.32", "rustls-native-certs 0.8.1", "rustls-pki-types", - "tokio 1.47.1", + "tokio 1.49.0", "tokio-rustls 0.26.3", "tower-service", ] @@ -3528,7 +3603,7 @@ dependencies = [ "bytes 1.10.1", "hyper 0.14.32", "native-tls", - "tokio 1.47.1", + "tokio 1.49.0", "tokio-native-tls", ] @@ -3543,7 +3618,7 @@ dependencies = [ "hyper 1.7.0", "hyper-util", "native-tls", - "tokio 1.47.1", + "tokio 1.49.0", "tokio-native-tls", "tower-service", ] @@ -3568,7 +3643,7 @@ dependencies = [ "pin-project-lite 0.2.16", "socket2 0.6.0", "system-configuration 0.6.1", - "tokio 1.47.1", + "tokio 1.49.0", "tower-service", "tracing", "windows-registry", @@ -3750,15 +3825,15 @@ checksum = "cd62e6b5e86ea8eeeb8db1de02880a6abc01a397b2ebb64b5d74ac255318f5cb" [[package]] name = "image" -version = "0.25.8" +version = "0.25.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7" +checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" dependencies = [ "bytemuck", "byteorder-lite", "color_quant", "exr", - "gif", + "gif 0.14.1", "image-webp 0.2.4", "moxcms", "num-traits", @@ -3768,8 +3843,8 @@ dependencies = [ "rayon", "rgb", "tiff", - "zune-core", - "zune-jpeg", + "zune-core 0.5.1", + "zune-jpeg 0.5.11", ] [[package]] @@ -3877,17 +3952,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "io-uring" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" -dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "libc", -] - [[package]] name = "ipconfig" version = "0.3.2" @@ -3995,6 +4059,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -4080,13 +4153,32 @@ dependencies = [ "tracing", ] +[[package]] +name = "jxl-bitstream" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b480e752277e29eb4054f69546887a9b84656fe78c08f54ba5850ced98a378fe" +dependencies = [ + "tracing", +] + [[package]] name = "jxl-coding" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da5b5093904e940bc11ef50e872c7bdf7b6e88653f012b925f8479daf212b5c9" dependencies = [ - "jxl-bitstream", + "jxl-bitstream 0.4.1", + "tracing", +] + +[[package]] +name = "jxl-coding" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd972bcd125e776f1eb241ac50e39f956095a1c2770c64736c968f8946bd9a3c" +dependencies = [ + "jxl-bitstream 1.1.0", "tracing", ] @@ -4096,10 +4188,25 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cb1c31e10054079df585633fc14fb9e4c96565c58c05b983c502e2472b57fa0" dependencies = [ - "jxl-bitstream", - "jxl-coding", - "jxl-grid", - "jxl-threadpool", + "jxl-bitstream 0.4.1", + "jxl-coding 0.4.1", + "jxl-grid 0.4.2", + "jxl-threadpool 0.1.2", + "tracing", +] + +[[package]] +name = "jxl-color" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f316b1358c1711755b3ee8e8cb5c4a1dad12e796233088a7a513440782de80b2" +dependencies = [ + "jxl-bitstream 1.1.0", + "jxl-coding 1.0.1", + "jxl-grid 0.6.1", + "jxl-image 0.13.0", + "jxl-oxide-common", + "jxl-threadpool 1.0.0", "tracing", ] @@ -4109,13 +4216,30 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e35b289aa0f24044167d83a1c29ae2b99210c5082ab7e3e90dacbcae818aa0a2" dependencies = [ - "jxl-bitstream", - "jxl-coding", - "jxl-grid", - "jxl-image", - "jxl-modular", - "jxl-threadpool", - "jxl-vardct", + "jxl-bitstream 0.4.1", + "jxl-coding 0.4.1", + "jxl-grid 0.4.2", + "jxl-image 0.9.0", + "jxl-modular 0.7.1", + "jxl-threadpool 0.1.2", + "jxl-vardct 0.7.0", + "tracing", +] + +[[package]] +name = "jxl-frame" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d967c6fd669c7c01060b5022d8835fa82fd46b06ffc98b549f17600a097c2b3" +dependencies = [ + "jxl-bitstream 1.1.0", + "jxl-coding 1.0.1", + "jxl-grid 0.6.1", + "jxl-image 0.13.0", + "jxl-modular 0.11.2", + "jxl-oxide-common", + "jxl-threadpool 1.0.0", + "jxl-vardct 0.11.1", "tracing", ] @@ -4128,15 +4252,54 @@ dependencies = [ "tracing", ] +[[package]] +name = "jxl-grid" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0e0ef92d5d60e76bf41098e57e985f523185e08fad54268da448637feca6989" +dependencies = [ + "tracing", +] + [[package]] name = "jxl-image" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b31b17ed3bd0e3b65e7b06628f5930e009dda6cd17638cf5159a20a3feedec6" dependencies = [ - "jxl-bitstream", - "jxl-color", - "jxl-grid", + "jxl-bitstream 0.4.1", + "jxl-color 0.7.1", + "jxl-grid 0.4.2", + "tracing", +] + +[[package]] +name = "jxl-image" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f752d62577c702a94dbbce4045caf08cb58639e8a4d56464b40ecf33ffe565" +dependencies = [ + "jxl-bitstream 1.1.0", + "jxl-grid 0.6.1", + "jxl-oxide-common", + "tracing", +] + +[[package]] +name = "jxl-jbr" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35d032bcec660647828527ff42c6f5776d2fd44b8357f9f6d9ac6dc07218e46" +dependencies = [ + "brotli-decompressor", + "jxl-bitstream 1.1.0", + "jxl-frame 0.13.3", + "jxl-grid 0.6.1", + "jxl-image 0.13.0", + "jxl-modular 0.11.2", + "jxl-oxide-common", + "jxl-threadpool 1.0.0", + "jxl-vardct 0.11.1", "tracing", ] @@ -4146,10 +4309,24 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da3b9fb8f46e63a14ecedefbd0f873b04162aaf8a09676b630c31bc8dadc4638" dependencies = [ - "jxl-bitstream", - "jxl-coding", - "jxl-grid", - "jxl-threadpool", + "jxl-bitstream 0.4.1", + "jxl-coding 0.4.1", + "jxl-grid 0.4.2", + "jxl-threadpool 0.1.2", + "tracing", +] + +[[package]] +name = "jxl-modular" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da758b2f989aafd9eeb39489fe43d7be5a3a0d2ad61cf1bad705eb6990a6053c" +dependencies = [ + "jxl-bitstream 1.1.0", + "jxl-coding 1.0.1", + "jxl-grid 0.6.1", + "jxl-oxide-common", + "jxl-threadpool 1.0.0", "tracing", ] @@ -4159,31 +4336,81 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ba1ee3895e6c62b131994807b1ee6d179013613a86c01c203369af8d1e8d2f0" dependencies = [ - "jxl-bitstream", - "jxl-color", - "jxl-frame", - "jxl-grid", - "jxl-image", - "jxl-render", - "jxl-threadpool", + "jxl-bitstream 0.4.1", + "jxl-color 0.7.1", + "jxl-frame 0.9.0", + "jxl-grid 0.4.2", + "jxl-image 0.9.0", + "jxl-render 0.8.2", + "jxl-threadpool 0.1.2", "tracing", ] +[[package]] +name = "jxl-oxide" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8ecd2678ed70c1eda42b811ccb2e25ab836edeb18e7f1178c1f917ed36b772" +dependencies = [ + "brotli-decompressor", + "bytemuck", + "image", + "jxl-bitstream 1.1.0", + "jxl-color 0.11.0", + "jxl-frame 0.13.3", + "jxl-grid 0.6.1", + "jxl-image 0.13.0", + "jxl-jbr", + "jxl-oxide-common", + "jxl-render 0.12.3", + "jxl-threadpool 1.0.0", + "tracing", +] + +[[package]] +name = "jxl-oxide-common" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62394c5021b3a9e7e0dbb2d639d555d019090c9946c39f6d3b09d390db4157b" +dependencies = [ + "jxl-bitstream 1.1.0", +] + [[package]] name = "jxl-render" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "203a79b3025b86f875cc97a6f39e1fcc6314f915b2f6b69f767fca45fd482a11" dependencies = [ - "jxl-bitstream", - "jxl-coding", - "jxl-color", - "jxl-frame", - "jxl-grid", - "jxl-image", - "jxl-modular", - "jxl-threadpool", - "jxl-vardct", + "jxl-bitstream 0.4.1", + "jxl-coding 0.4.1", + "jxl-color 0.7.1", + "jxl-frame 0.9.0", + "jxl-grid 0.4.2", + "jxl-image 0.9.0", + "jxl-modular 0.7.1", + "jxl-threadpool 0.1.2", + "jxl-vardct 0.7.0", + "tracing", +] + +[[package]] +name = "jxl-render" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa0c3100918bd3c41bb0f8ce1f4f1664e48f3032ff8eeab0d6a2cfc3276f462d" +dependencies = [ + "bytemuck", + "jxl-bitstream 1.1.0", + "jxl-coding 1.0.1", + "jxl-color 0.11.0", + "jxl-frame 0.13.3", + "jxl-grid 0.6.1", + "jxl-image 0.13.0", + "jxl-modular 0.11.2", + "jxl-oxide-common", + "jxl-threadpool 1.0.0", + "jxl-vardct 0.11.1", "tracing", ] @@ -4198,17 +4425,43 @@ dependencies = [ "tracing", ] +[[package]] +name = "jxl-threadpool" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25f15eb830aa77a7f21148d72e153562a26bfe570139bd4922eab1908dd499d3" +dependencies = [ + "rayon", + "rayon-core", + "tracing", +] + [[package]] name = "jxl-vardct" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16af82a1ad770887cad720bfd3cc6a6d023faf377036989a24cf2c6538b649e0" dependencies = [ - "jxl-bitstream", - "jxl-coding", - "jxl-grid", - "jxl-modular", - "jxl-threadpool", + "jxl-bitstream 0.4.1", + "jxl-coding 0.4.1", + "jxl-grid 0.4.2", + "jxl-modular 0.7.1", + "jxl-threadpool 0.1.2", + "tracing", +] + +[[package]] +name = "jxl-vardct" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce72a18c6d3a47172ab6c479be2bdb56f22066b5d7092663f03b4490820b4511" +dependencies = [ + "jxl-bitstream 1.1.0", + "jxl-coding 1.0.1", + "jxl-grid 0.6.1", + "jxl-modular 0.11.2", + "jxl-oxide-common", + "jxl-threadpool 1.0.0", "tracing", ] @@ -4290,7 +4543,7 @@ dependencies = [ "once_cell", "quoted_printable", "socket2 0.4.10", - "tokio 1.47.1", + "tokio 1.49.0", ] [[package]] @@ -4418,7 +4671,7 @@ dependencies = [ "log", "parking_lot", "pbjson-types", - "prost", + "prost 0.12.6", "rand 0.9.2", "reqwest 0.11.27", "scopeguard", @@ -4440,11 +4693,11 @@ dependencies = [ "parking_lot", "pbjson", "pbjson-types", - "prost", + "prost 0.12.6", "prost-types", "serde", "thiserror 1.0.69", - "tokio 1.47.1", + "tokio 1.49.0", ] [[package]] @@ -4453,7 +4706,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ae53eb874eb86e96e8ccffc31b5a00926d46174cbcb1c24ce4e57b1fcc5c8f6" dependencies = [ - "tokio 1.47.1", + "tokio 1.49.0", "tokio-stream", ] @@ -4463,7 +4716,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "532e84c6cdc5fe774f2b5d9912597b5f3bea561927a48296d03e24549d21c3f6" dependencies = [ - "tokio 1.47.1", + "tokio 1.49.0", "tokio-stream", ] @@ -4796,7 +5049,7 @@ dependencies = [ "log", "metrics", "thiserror 1.0.69", - "tokio 1.47.1", + "tokio 1.49.0", "tracing", "tracing-subscriber", ] @@ -4893,7 +5146,7 @@ dependencies = [ "strsim 0.11.1", "take_mut", "thiserror 1.0.69", - "tokio 1.47.1", + "tokio 1.49.0", "tokio-rustls 0.26.3", "tokio-util", "typed-builder", @@ -4937,7 +5190,7 @@ dependencies = [ "memchr", "mime", "spin", - "tokio 1.47.1", + "tokio 1.49.0", "tokio-util", "version_check", ] @@ -5231,6 +5484,90 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "opentelemetry" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "pin-project-lite 0.2.16", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "opentelemetry-appender-tracing" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef6a1ac5ca3accf562b8c306fa8483c85f4390f768185ab775f242f7fe8fdcc2" +dependencies = [ + "opentelemetry", + "tracing", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "opentelemetry-http" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d" +dependencies = [ + "async-trait", + "bytes 1.10.1", + "http 1.3.1", + "opentelemetry", + "reqwest 0.12.23", +] + +[[package]] +name = "opentelemetry-otlp" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2366db2dca4d2ad033cad11e6ee42844fd727007af5ad04a1730f4cb8163bf" +dependencies = [ + "http 1.3.1", + "opentelemetry", + "opentelemetry-http", + "opentelemetry-proto", + "opentelemetry_sdk", + "prost 0.14.3", + "reqwest 0.12.23", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "opentelemetry-proto" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f" +dependencies = [ + "opentelemetry", + "opentelemetry_sdk", + "prost 0.14.3", + "tonic", + "tonic-prost", +] + +[[package]] +name = "opentelemetry_sdk" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ae4f5991976fd48df6d843de219ca6d31b01daaab2dad5af2badeded372bd" +dependencies = [ + "futures-channel", + "futures-executor", + "futures-util", + "opentelemetry", + "percent-encoding", + "rand 0.9.2", + "thiserror 2.0.18", +] + [[package]] name = "ordered-multimap" version = "0.4.3" @@ -5329,6 +5666,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + [[package]] name = "pathdiff" version = "0.2.3" @@ -5353,7 +5696,7 @@ checksum = "2580e33f2292d34be285c5bc3dba5259542b083cfad6037b6d70345f24dcb735" dependencies = [ "heck 0.4.1", "itertools 0.11.0", - "prost", + "prost 0.12.6", "prost-types", ] @@ -5367,7 +5710,7 @@ dependencies = [ "chrono", "pbjson", "pbjson-build", - "prost", + "prost 0.12.6", "prost-build", "serde", ] @@ -5456,7 +5799,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21e0a3a33733faeaf8651dfee72dd0f388f0c8e5ad496a3478fa5a922f49cfa8" dependencies = [ "memchr", - "thiserror 2.0.16", + "thiserror 2.0.18", "ucd-trie", ] @@ -5950,7 +6293,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" dependencies = [ "bytes 1.10.1", - "prost-derive", + "prost-derive 0.12.6", +] + +[[package]] +name = "prost" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" +dependencies = [ + "bytes 1.10.1", + "prost-derive 0.14.3", ] [[package]] @@ -5967,7 +6320,7 @@ dependencies = [ "once_cell", "petgraph", "prettyplease", - "prost", + "prost 0.12.6", "prost-types", "regex", "syn 2.0.106", @@ -5987,13 +6340,26 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "prost-derive" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote 1.0.40", + "syn 2.0.106", +] + [[package]] name = "prost-types" version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" dependencies = [ - "prost", + "prost 0.12.6", ] [[package]] @@ -6242,19 +6608,21 @@ dependencies = [ [[package]] name = "rav1e" -version = "0.7.1" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" dependencies = [ + "aligned-vec", "arbitrary", "arg_enum_proc_macro", "arrayvec", + "av-scenechange", "av1-grain", "bitstream-io", "built", "cfg-if", "interpolate_name", - "itertools 0.12.1", + "itertools 0.14.0", "libc", "libfuzzer-sys", "log", @@ -6263,23 +6631,21 @@ dependencies = [ "noop_proc_macro", "num-derive", "num-traits", - "once_cell", "paste", "profiling", - "rand 0.8.5", - "rand_chacha 0.3.1", + "rand 0.9.2", + "rand_chacha 0.9.0", "simd_helpers", - "system-deps", - "thiserror 1.0.69", + "thiserror 2.0.18", "v_frame", "wasm-bindgen", ] [[package]] name = "ravif" -version = "0.11.20" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5825c26fddd16ab9f515930d49028a630efec172e903483c94796cfe31893e6b" +checksum = "ef69c1990ceef18a116855938e74793a5f7496ee907562bd0857b6ac734ab285" dependencies = [ "avif-serialize", "imgref", @@ -6335,7 +6701,7 @@ dependencies = [ "ryu", "sha1_smol", "socket2 0.4.10", - "tokio 1.47.1", + "tokio 1.49.0", "tokio-util", "url", ] @@ -6464,7 +6830,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper 0.1.2", "system-configuration 0.5.1", - "tokio 1.47.1", + "tokio 1.49.0", "tokio-native-tls", "tower-service", "url", @@ -6483,7 +6849,9 @@ dependencies = [ "base64 0.22.1", "bytes 1.10.1", "encoding_rs", + "futures-channel", "futures-core", + "futures-util", "h2 0.4.12", "http 1.3.1", "http-body 1.0.1", @@ -6503,7 +6871,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "sync_wrapper 1.0.2", - "tokio 1.47.1", + "tokio 1.49.0", "tokio-native-tls", "tower", "tower-http 0.6.6", @@ -6526,7 +6894,7 @@ version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a325d5e8d1cebddd070b13f44cec8071594ab67d1012797c121f27a669b7958" dependencies = [ - "gif", + "gif 0.13.3", "image-webp 0.1.3", "log", "pico-args", @@ -6534,7 +6902,7 @@ dependencies = [ "svgtypes", "tiny-skia", "usvg", - "zune-jpeg", + "zune-jpeg 0.4.21", ] [[package]] @@ -6548,11 +6916,15 @@ dependencies = [ "image", "imagesize", "infer", - "jxl-oxide", + "jxl-oxide 0.8.1", "kamadak-exif", "lazy_static", "moka", "nanoid", + "opentelemetry", + "opentelemetry-appender-tracing", + "opentelemetry-otlp", + "opentelemetry_sdk", "revolt-config", "revolt-database", "revolt-files", @@ -6565,7 +6937,7 @@ dependencies = [ "simdutf8", "strum_macros", "tempfile", - "tokio 1.47.1", + "tokio 1.49.0", "tower-http 0.5.2", "tracing", "tracing-subscriber", @@ -6612,7 +6984,7 @@ version = "0.9.4" dependencies = [ "indexmap 2.13.0", "lru 0.16.3", - "tokio 1.47.1", + "tokio 1.49.0", ] [[package]] @@ -6641,7 +7013,7 @@ dependencies = [ "revolt-database", "revolt-files", "revolt-result", - "tokio 1.47.1", + "tokio 1.49.0", ] [[package]] @@ -6749,21 +7121,26 @@ name = "revolt-files" version = "0.9.4" dependencies = [ "aes-gcm", + "anyhow", + "async-trait", "aws-config", "aws-sdk-s3", "base64 0.22.1", "ffprobe", "image", "imagesize", - "jxl-oxide", + "jxl-oxide 0.12.5", "resvg", "revolt-config", "revolt-result", "tempfile", + "thiserror 2.0.18", "tiny-skia", + "tokio 1.49.0", "tracing", "typenum", "usvg", + "uuid", "webp", ] @@ -6783,7 +7160,7 @@ dependencies = [ "revolt-result", "serde", "serde_json", - "tokio 1.47.1", + "tokio 1.49.0", "tower-http 0.5.2", "tracing", "utoipa", @@ -6811,7 +7188,7 @@ dependencies = [ "serde", "serde_json", "tempfile", - "tokio 1.47.1", + "tokio 1.49.0", "tracing", "tracing-subscriber", "utoipa", @@ -6894,7 +7271,7 @@ dependencies = [ "revolt_optional_struct", "serde", "serde_json", - "tokio 1.47.1", + "tokio 1.49.0", "ulid 1.2.1", "web-push", ] @@ -6981,7 +7358,7 @@ dependencies = [ "serde", "serde_json", "thiserror 1.0.69", - "tokio 1.47.1", + "tokio 1.49.0", ] [[package]] @@ -7137,7 +7514,7 @@ dependencies = [ "state", "tempfile", "time", - "tokio 1.47.1", + "tokio 1.49.0", "tokio-stream", "tokio-util", "ubyte", @@ -7228,7 +7605,7 @@ dependencies = [ "stable-pattern", "state", "time", - "tokio 1.47.1", + "tokio 1.49.0", "uncased", ] @@ -7719,7 +8096,7 @@ dependencies = [ "sentry-debug-images", "sentry-panic", "sentry-tracing", - "tokio 1.47.1", + "tokio 1.49.0", "ureq", ] @@ -7856,7 +8233,7 @@ dependencies = [ "rand 0.9.2", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.18", "time", "url", "uuid", @@ -8468,19 +8845,6 @@ dependencies = [ "libc", ] -[[package]] -name = "system-deps" -version = "6.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" -dependencies = [ - "cfg-expr", - "heck 0.5.0", - "pkg-config", - "toml 0.8.23", - "version-compare", -] - [[package]] name = "tagptr" version = "0.2.0" @@ -8499,12 +8863,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" -[[package]] -name = "target-lexicon" -version = "0.12.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" - [[package]] name = "tempfile" version = "3.22.0" @@ -8549,11 +8907,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.16" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.16", + "thiserror-impl 2.0.18", ] [[package]] @@ -8569,9 +8927,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.16" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote 1.0.40", @@ -8598,7 +8956,7 @@ dependencies = [ "half", "quick-error 2.0.1", "weezl", - "zune-jpeg", + "zune-jpeg 0.4.21", ] [[package]] @@ -8707,29 +9065,26 @@ dependencies = [ [[package]] name = "tokio" -version = "1.47.1" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ - "backtrace", "bytes 1.10.1", - "io-uring", "libc", "mio", "parking_lot", "pin-project-lite 0.2.16", "signal-hook-registry", - "slab", "socket2 0.6.0", "tokio-macros", - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote 1.0.40", @@ -8743,7 +9098,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" dependencies = [ "native-tls", - "tokio 1.47.1", + "tokio 1.49.0", ] [[package]] @@ -8753,7 +9108,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ "rustls 0.21.12", - "tokio 1.47.1", + "tokio 1.49.0", ] [[package]] @@ -8764,7 +9119,7 @@ checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" dependencies = [ "rustls 0.22.4", "rustls-pki-types", - "tokio 1.47.1", + "tokio 1.49.0", ] [[package]] @@ -8774,7 +9129,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f63835928ca123f1bef57abbcd23bb2ba0ac9ae1235f1e65bda0d06e7786bd" dependencies = [ "rustls 0.23.32", - "tokio 1.47.1", + "tokio 1.49.0", ] [[package]] @@ -8785,7 +9140,7 @@ checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ "futures-core", "pin-project-lite 0.2.16", - "tokio 1.47.1", + "tokio 1.49.0", ] [[package]] @@ -8799,7 +9154,7 @@ dependencies = [ "futures-io", "futures-sink", "pin-project-lite 0.2.16", - "tokio 1.47.1", + "tokio 1.49.0", ] [[package]] @@ -8863,6 +9218,38 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +[[package]] +name = "tonic" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203" +dependencies = [ + "async-trait", + "base64 0.22.1", + "bytes 1.10.1", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "percent-encoding", + "pin-project", + "sync_wrapper 1.0.2", + "tokio-stream", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-prost" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66bd50ad6ce1252d87ef024b3d64fe4c3cf54a86fb9ef4c631fdd0ded7aeaa67" +dependencies = [ + "bytes 1.10.1", + "prost 0.14.3", + "tonic", +] + [[package]] name = "totp-lite" version = "2.0.1" @@ -8885,7 +9272,7 @@ dependencies = [ "futures-util", "pin-project-lite 0.2.16", "sync_wrapper 1.0.2", - "tokio 1.47.1", + "tokio 1.49.0", "tower-layer", "tower-service", "tracing", @@ -8905,6 +9292,7 @@ dependencies = [ "pin-project-lite 0.2.16", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -8939,9 +9327,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite 0.2.16", @@ -8951,9 +9339,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote 1.0.40", @@ -8962,9 +9350,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -8993,9 +9381,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.20" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ "matchers", "nu-ansi-term", @@ -9361,13 +9749,13 @@ dependencies = [ [[package]] name = "uuid" -version = "1.18.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ "getrandom 0.3.3", "js-sys", - "serde", + "serde_core", "wasm-bindgen", ] @@ -9475,12 +9863,6 @@ dependencies = [ "time", ] -[[package]] -name = "version-compare" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" - [[package]] name = "version_check" version = "0.9.5" @@ -10177,6 +10559,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + [[package]] name = "yaml-rust" version = "0.4.5" @@ -10241,7 +10629,7 @@ dependencies = [ "serde", "serde_json", "time", - "tokio 1.47.1", + "tokio 1.49.0", "tower-service", "url", ] @@ -10332,6 +10720,12 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + [[package]] name = "zune-inflate" version = "0.2.54" @@ -10347,5 +10741,14 @@ version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" dependencies = [ - "zune-core", + "zune-core 0.4.12", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2959ca473aae96a14ecedf501d20b3608d2825ba280d5adb57d651721885b0c2" +dependencies = [ + "zune-core 0.5.1", ] diff --git a/Cargo.toml b/Cargo.toml index 5c320e63..10989fb1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,9 +14,37 @@ redis23 = { package = "redis", version = "0.23.3", git = "https://github.com/rev #authifier = { package = "authifier", version = "1.0.10", path = "../authifier/crates/authifier" } #rocket_authifier = { package = "rocket_authifier", version = "1.0.10", path = "../authifier/crates/rocket_authifier" } -# I'm 99% sure this is overloading the GitHub worker -# hence builds have been failing since, let's just -# disable it for now. In the future, we could use this -# if we were rolling our own CI (that is now). [profile.release] lto = true + +[workspace.dependencies] +# Async +async-trait = "0.1.89" +tokio = { version = "1.49.0", features = ["macros", "rt"] } + +# Error Handling +anyhow = "1.0.100" +thiserror = "2.0.18" + +# Other Utilities +uuid = { version = "1.19.0", features = ["v4"] } + +# Axum (HTTP server) +axum-macros = "0.4.1" +axum_typed_multipart = "0.12.1" +axum = { version = "0.7.5", features = ["multipart"] } +tower-http = { version = "0.5.2", features = ["cors", "trace"] } + +# Image Processing +jxl-oxide = { version = "0.12.5", features = ["image"] } +image = "0.25.9" + +# OpenTelemetry +tracing = "0.1.44" +tracing-subscriber = { version = "0.3.22", features = [ + "env-filter", +] } # consider https://crates.io/crates/better-tracing +opentelemetry = { version = "0.31.0", features = ["logs"] } +opentelemetry_sdk = { version = "0.31.0", features = ["logs"] } +opentelemetry-otlp = { version = "0.31.0", features = ["logs"] } +opentelemetry-appender-tracing = { version = "0.31.1" } diff --git a/crates/core/config/src/lib.rs b/crates/core/config/src/lib.rs index 46b08776..35c93b5a 100644 --- a/crates/core/config/src/lib.rs +++ b/crates/core/config/src/lib.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::{collections::HashMap, path::Path}; use cached::proc_macro::cached; use config::{Config, File, FileFormat}; @@ -94,10 +94,19 @@ static CONFIG_BUILDER: Lazy> = Lazy::new(|| { } } - for path in CONFIG_SEARCH_PATHS { - if std::path::Path::new(path).exists() { - builder = builder.add_source(File::new(path, FileFormat::Toml)); + let cwd = std::env::current_dir().unwrap(); + let mut cwd: Option<&Path> = Some(&cwd); + + while let Some(path) = cwd { + for config_path in CONFIG_SEARCH_PATHS { + let config_path = path.join(config_path); + if config_path.exists() { + builder = builder + .add_source(File::new(config_path.to_str().unwrap(), FileFormat::Toml)); + } } + + cwd = path.parent(); } builder.build().unwrap() diff --git a/crates/core/files/Cargo.toml b/crates/core/files/Cargo.toml index f807345d..f34469e8 100644 --- a/crates/core/files/Cargo.toml +++ b/crates/core/files/Cargo.toml @@ -8,7 +8,13 @@ description = "Revolt Backend: S3 and encryption subroutines" repository = "https://github.com/stoatchat/stoatchat" [dependencies] -tracing = "0.1" +anyhow = { workspace = true } +thiserror = { workspace = true } + +tracing = { workspace = true } + +tokio = { workspace = true } +async-trait = { workspace = true } ffprobe = "0.4.0" imagesize = "0.13.0" @@ -27,8 +33,8 @@ revolt-config = { version = "0.9.4", path = "../config", features = [ revolt-result = { version = "0.9.4", path = "../result" } # image processing -jxl-oxide = "0.8.1" -image = { version = "0.25.2" } +jxl-oxide = { workspace = true } +image = { workspace = true } # svg rendering usvg = "0.44.0" @@ -37,3 +43,6 @@ tiny-skia = "0.11.4" # encoding webp = "0.3.0" + +[dev-dependencies] +uuid = { workspace = true } diff --git a/crates/core/files/src/implementation/encryption_impl.rs b/crates/core/files/src/implementation/encryption_impl.rs new file mode 100644 index 00000000..25da9cfd --- /dev/null +++ b/crates/core/files/src/implementation/encryption_impl.rs @@ -0,0 +1,75 @@ +use aes_gcm::{ + aead::{Aead, AeadCore, AeadMutInPlace, OsRng}, + Aes256Gcm, Key, KeyInit, Nonce, +}; +use base64::{prelude::BASE64_STANDARD, Engine}; + +use crate::EncryptionRepository; + +pub struct EncryptionKey { + key: String, +} + +impl EncryptionKey { + pub async fn from_config() -> EncryptionKey { + EncryptionKey::new(revolt_config::config().await.files.encryption_key) + } + + pub fn new(key: String) -> EncryptionKey { + EncryptionKey { key } + } + + fn create_cipher(&self) -> Aes256Gcm { + let key = &BASE64_STANDARD + .decode(self.key.clone()) + .expect("valid base64 string")[..]; + let key: &Key = key.into(); + Aes256Gcm::new(key) + } +} + +impl EncryptionRepository for EncryptionKey { + fn decrypt_buffer(&self, mut buf: Vec, iv: &str) -> anyhow::Result> { + let iv = &BASE64_STANDARD.decode(iv).unwrap()[..]; + let iv: &Nonce = iv.into(); + + self.create_cipher() + .decrypt_in_place(iv, b"", &mut buf) + .map_err(|error| { + tracing::error!("{}", error); + anyhow::anyhow!("EncryptionRepository: decryption failed") + })?; + + Ok(buf) + } + + fn encrypt_buffer(&self, buf: &[u8]) -> anyhow::Result<(Vec, String)> { + let iv = Aes256Gcm::generate_nonce(&mut OsRng); + + let buf = self.create_cipher().encrypt(&iv, buf).map_err(|error| { + tracing::error!("{}", error); + anyhow::anyhow!("EncryptionRepository: encryption failed") + })?; + + Ok((buf, BASE64_STANDARD.encode(iv))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_encrypt_and_decrypt() { + let encryption = + EncryptionKey::new("XkbJ8gBzrouQ+15Ri23xCC81+aZE26Z6+gXzglFxOD4=".to_string()); + + let buf: Vec = vec![67]; + let (ciphertext, iv) = encryption.encrypt_buffer(&buf[..]).unwrap(); + assert_eq!(ciphertext.len(), 17); + + let plaintext = encryption.decrypt_buffer(ciphertext, &iv).unwrap(); + assert_eq!(plaintext.len(), 1); + assert_eq!(plaintext[0], 67); + } +} diff --git a/crates/core/files/src/implementation/media_impl.rs b/crates/core/files/src/implementation/media_impl.rs new file mode 100644 index 00000000..bd39534a --- /dev/null +++ b/crates/core/files/src/implementation/media_impl.rs @@ -0,0 +1,294 @@ +use anyhow::Result; +use image::{DynamicImage, ImageBuffer, ImageReader}; +use jxl_oxide::integration::JxlDecoder; +use revolt_config::report_internal_error; +use std::io::{BufRead, Read, Seek}; +use tempfile::NamedTempFile; +use tiny_skia::Pixmap; + +use crate::{MediaError, MediaRepository}; + +pub struct MediaImpl { + config: revolt_config::Files, +} + +impl MediaImpl { + pub async fn from_config() -> MediaImpl { + MediaImpl { + config: revolt_config::config().await.files, + } + } + + pub fn new(config: revolt_config::Files) -> MediaImpl { + MediaImpl { config } + } +} + +impl MediaRepository for MediaImpl { + fn image_size(&self, f: &NamedTempFile) -> Option<(usize, usize)> { + if let Ok(size) = imagesize::size(f.path()) + .inspect_err(|err| tracing::error!("Failed to generate image size! {err:?}")) + { + Some((size.width, size.height)) + } else { + None + } + } + + fn image_size_vec(&self, v: &[u8], mime: &str) -> Option<(usize, usize)> { + match mime { + "image/svg+xml" => { + let tree = + report_internal_error!(usvg::Tree::from_data(v, &Default::default())).ok()?; + + let size = tree.size(); + Some((size.width() as usize, size.height() as usize)) + } + _ => { + if let Ok(size) = imagesize::blob_size(v) + .inspect_err(|err| tracing::error!("Failed to generate image size! {err:?}")) + { + Some((size.width, size.height)) + } else { + None + } + } + } + } + + fn decode_image( + &self, + reader: &mut R, + mime: &str, + ) -> Result { + match mime { + "image/jxl" => { + let decoder = + JxlDecoder::new(reader).map_err(|e| MediaError::from(anyhow::anyhow!(e)))?; + + DynamicImage::from_decoder(decoder) + .map_err(|e| MediaError::from(anyhow::anyhow!(e))) + } + "image/svg+xml" => { + let mut buf = Vec::new(); + reader + .read_to_end(&mut buf) + .map_err(|e| MediaError::from(anyhow::anyhow!(e)))?; + + let tree: usvg::Tree = usvg::Tree::from_data(&buf, &Default::default()) + .map_err(|e| MediaError::from(anyhow::anyhow!(e)))?; + + let size = tree.size(); + let mut pixmap = Pixmap::new(size.width() as u32, size.height() as u32) + .ok_or_else(|| MediaError::ImageProcessingFailed { + cause: "failed to create Pixmap, likely zero sized".to_string(), + })?; + + let mut pixmap_mut = pixmap.as_mut(); + resvg::render(&tree, Default::default(), &mut pixmap_mut); + + Ok(DynamicImage::ImageRgba8( + ImageBuffer::from_vec( + size.width() as u32, + size.height() as u32, + pixmap.data().to_vec(), + ) + .ok_or_else(|| MediaError::ImageProcessingFailed { + cause: "buffer is not big enough".to_string(), + })?, + )) + } + _ => { + let image: ImageReader<&mut R> = image::ImageReader::new(reader) + .with_guessed_format() + .map_err(|e| MediaError::from(anyhow::anyhow!(e)))?; + + let image: Result = image + .decode() + .map_err(|e| MediaError::from(anyhow::anyhow!(e))); + + image + } + } + } + + fn is_valid_image(&self, reader: &mut R, mime: &str) -> bool { + match mime { + "image/jxl" => jxl_oxide::JxlImage::builder() + .read(reader) + .inspect_err(|err| tracing::error!("Failed to read JXL! {err:?}")) + .is_ok(), + _ => !matches!( + image::ImageReader::new(reader) + .with_guessed_format() + .inspect_err(|err| tracing::error!("Failed to read image! {err:?}")) + .map(|f| f.decode()), + Err(_) | Ok(Err(_)) + ), + } + } + + fn create_thumbnail(&self, image: DynamicImage, tag: &str) -> Vec { + let [w, h] = self.config.preview.get(tag).unwrap(); + + let image = image.thumbnail(image.width().min(*w as u32), image.height().min(*h as u32)); + + let encoder = webp::Encoder::from_image(&image).expect("Could not create encoder."); + if self.config.webp_quality != 100.0 { + encoder.encode(self.config.webp_quality).to_vec() + } else { + encoder.encode_lossless().to_vec() + } + } + + fn video_size(&self, f: &NamedTempFile) -> Option<(i64, i64)> { + if let Ok(data) = ffprobe::ffprobe(f.path()) + .inspect_err(|err| tracing::error!("Failed to ffprobe file! {err:?}")) + { + for stream in data.streams { + if let (Some(w), Some(h)) = (stream.width, stream.height) { + return Some((w, h)); + } + } + + None + } else { + None + } + } +} + +#[cfg(test)] +mod tests { + use std::io::Cursor; + + use crate::{MediaImpl, MediaRepository}; + + #[tokio::test] + async fn asset_test_jpeg() { + let media = MediaImpl::from_config().await; + let buf = include_bytes!("../../tests/assets/test.jpeg"); + assert_eq!(media.image_size_vec(buf, "image/jpeg"), Some((655, 582))); + + let mut reader = Cursor::new(buf); + let image = media.decode_image(&mut reader, "image/jpeg").unwrap(); + media.create_thumbnail(image, "attachments"); + } + + #[tokio::test] + async fn asset_test_jpeg_extra_bytes() { + let media = MediaImpl::from_config().await; + let buf = [ + &include_bytes!("../../tests/assets/test.jpeg")[..], + &[0u8; 16], + ] + .concat(); + assert_eq!(media.image_size_vec(&buf, "image/jpeg"), Some((655, 582))); + + let mut reader = Cursor::new(buf); + let image = media.decode_image(&mut reader, "image/jpeg").unwrap(); + media.create_thumbnail(image, "emojis"); + } + + #[tokio::test] + async fn asset_test_png() { + let media = MediaImpl::from_config().await; + let buf = include_bytes!("../../tests/assets/test.png"); + assert_eq!(media.image_size_vec(buf, "image/png"), Some((900, 900))); + + let mut reader = Cursor::new(buf); + let image = media.decode_image(&mut reader, "image/png").unwrap(); + media.create_thumbnail(image, "emojis"); + } + + #[tokio::test] + async fn asset_test_png_extra_bytes() { + let media = MediaImpl::from_config().await; + let buf = [ + &include_bytes!("../../tests/assets/test.png")[..], + &[0u8; 16], + ] + .concat(); + assert_eq!(media.image_size_vec(&buf, "image/png"), Some((900, 900))); + + let mut reader = Cursor::new(buf); + let image = media.decode_image(&mut reader, "image/png").unwrap(); + media.create_thumbnail(image, "emojis"); + } + + #[tokio::test] + async fn asset_test_corrupted_png() { + let media = MediaImpl::from_config().await; + let buf = include_bytes!("../../tests/assets/corrupted.png"); + assert_eq!(media.image_size_vec(buf, "image/png"), Some((900, 900))); + + let mut reader = Cursor::new(buf); + media.decode_image(&mut reader, "image/png").unwrap_err(); + } + + #[tokio::test] + async fn asset_test_animated_png() { + let media = MediaImpl::from_config().await; + let buf = include_bytes!("../../tests/assets/anim-icos.apng"); + assert_eq!(media.image_size_vec(buf, "image/png"), Some((128, 128))); + + let mut reader = Cursor::new(buf); + let image = media.decode_image(&mut reader, "image/png").unwrap(); + media.create_thumbnail(image, "attachments"); + } + + #[tokio::test] + async fn asset_test_jxl() { + let media = MediaImpl::from_config().await; + let buf = include_bytes!("../../tests/assets/dice.jxl"); + assert_eq!(media.image_size_vec(buf, "image/jxl"), Some((800, 600))); + + let mut reader = Cursor::new(buf); + let image = media.decode_image(&mut reader, "image/jxl").unwrap(); + media.create_thumbnail(image, "attachments"); + } + + #[tokio::test] + async fn asset_test_animated_jxl() { + let media = MediaImpl::from_config().await; + let buf = include_bytes!("../../tests/assets/anim-icos.jxl"); + assert_eq!(media.image_size_vec(buf, "image/jxl"), Some((128, 128))); + + let mut reader = Cursor::new(buf); + let image = media.decode_image(&mut reader, "image/jxl").unwrap(); + media.create_thumbnail(image, "attachments"); + } + + #[tokio::test] + async fn asset_test_webp() { + let media = MediaImpl::from_config().await; + let buf = include_bytes!("../../tests/assets/dice.webp"); + assert_eq!(media.image_size_vec(buf, "image/webp"), Some((800, 600))); + + let mut reader = Cursor::new(buf); + let image = media.decode_image(&mut reader, "image/webp").unwrap(); + media.create_thumbnail(image, "attachments"); + } + + #[tokio::test] + async fn asset_test_animated_webp() { + let media = MediaImpl::from_config().await; + let buf = include_bytes!("../../tests/assets/anim-icos.webp"); + assert_eq!(media.image_size_vec(buf, "image/webp"), Some((128, 128))); + + let mut reader = Cursor::new(buf); + let image = media.decode_image(&mut reader, "image/webp").unwrap(); + media.create_thumbnail(image, "attachments"); + } + + #[tokio::test] + async fn asset_test_animated_gif() { + let media = MediaImpl::from_config().await; + let buf = include_bytes!("../../tests/assets/anim-icos.gif"); + assert_eq!(media.image_size_vec(buf, "image/gif"), Some((128, 128))); + + let mut reader = Cursor::new(buf); + let image = media.decode_image(&mut reader, "image/gif").unwrap(); + media.create_thumbnail(image, "attachments"); + } +} diff --git a/crates/core/files/src/implementation/mod.rs b/crates/core/files/src/implementation/mod.rs new file mode 100644 index 00000000..fa96ac22 --- /dev/null +++ b/crates/core/files/src/implementation/mod.rs @@ -0,0 +1,7 @@ +mod encryption_impl; +mod media_impl; +mod s3_impl; + +pub use encryption_impl::EncryptionKey; +pub use media_impl::MediaImpl; +pub use s3_impl::S3Storage; diff --git a/crates/core/files/src/implementation/s3_impl.rs b/crates/core/files/src/implementation/s3_impl.rs new file mode 100644 index 00000000..c27d6201 --- /dev/null +++ b/crates/core/files/src/implementation/s3_impl.rs @@ -0,0 +1,118 @@ +use std::io::Write; + +use anyhow::Context; +use aws_sdk_s3::{ + config::{Credentials, Region}, + Client, Config, +}; +use revolt_config::FilesS3; + +use crate::{EncryptionRepository, FileStorageRepository}; + +pub struct S3Storage { + client: Client, + encryption: ER, +} + +impl S3Storage { + pub async fn from_config(encryption: ER) -> S3Storage { + S3Storage::new(encryption, revolt_config::config().await.files.s3) + } + + pub fn new(encryption: ER, s3_config: FilesS3) -> S3Storage { + let provider_name = "my-creds"; + let creds = Credentials::new( + s3_config.access_key_id, + s3_config.secret_access_key, + None, + None, + provider_name, + ); + + let config = Config::builder() + .region(Region::new(s3_config.region)) + .endpoint_url(s3_config.endpoint) + .force_path_style(s3_config.path_style_buckets) + .credentials_provider(creds) + .build(); + + S3Storage { + client: Client::from_conf(config), + encryption, + } + } +} + +#[async_trait::async_trait] +impl FileStorageRepository for S3Storage { + async fn create_bucket(&self, bucket_id: &str) -> anyhow::Result<()> { + self.client + .create_bucket() + .bucket(bucket_id) + .send() + .await + .with_context(|| format!("failed to create bucket {bucket_id}"))?; + + Ok(()) + } + + async fn fetch_and_decrypt_file( + &self, + bucket_id: &str, + path: &str, + iv: &str, + ) -> anyhow::Result> { + let mut object = self + .client + .get_object() + .bucket(bucket_id) + .key(path) + .send() + .await + .with_context(|| format!("failed to get object at {path} in {bucket_id}"))?; + + let mut buf = vec![]; + while let Some(bytes) = object.body.next().await { + let data = bytes?; + buf.write_all(&data)?; + } + + if iv.is_empty() { + Ok(buf) + } else { + self.encryption.decrypt_buffer(buf, iv) + } + } + + async fn encrypt_and_upload_file( + &self, + bucket_id: &str, + path: &str, + buf: &[u8], + ) -> anyhow::Result { + let (buf, iv) = self.encryption.encrypt_buffer(buf)?; + + self.client + .put_object() + .bucket(bucket_id) + .key(path) + .body(buf.into()) + .send() + .await + .with_context(|| format!("failed to put object at {path} in {bucket_id}"))?; + + Ok(iv) + } + + async fn delete_file(&self, bucket_id: &str, path: &str) -> anyhow::Result<()> { + self.client + .delete_object() + .bucket(bucket_id) + .key(path) + .send() + .await + .with_context(|| format!("failed to delete object at {path} in {bucket_id}"))?; + + Ok(()) + } +} diff --git a/crates/core/files/src/lib.rs b/crates/core/files/src/lib.rs index f9db7810..b1c0982d 100644 --- a/crates/core/files/src/lib.rs +++ b/crates/core/files/src/lib.rs @@ -1,293 +1,182 @@ -use std::io::{BufRead, Read, Seek, Write}; +mod implementation; +mod repositories; -use aes_gcm::{ - aead::{AeadCore, AeadMutInPlace, OsRng}, - Aes256Gcm, Key, KeyInit, Nonce, -}; -use image::{DynamicImage, ImageBuffer}; -use revolt_config::{config, report_internal_error, FilesS3}; -use revolt_result::{create_error, Result}; +pub use implementation::*; +pub use repositories::*; -use aws_sdk_s3::{ - config::{Credentials, Region}, - Client, Config, -}; +use std::io::{BufRead, Read, Seek}; + +use image::DynamicImage; +use revolt_config::{report_internal_error, Files, FilesLimit, FilesS3}; +use revolt_result::Result; -use base64::prelude::*; use tempfile::NamedTempFile; -use tiny_skia::Pixmap; -/// Size of the authentication tag in the buffer pub const AUTHENTICATION_TAG_SIZE_BYTES: usize = 16; -/// Create an S3 client -pub fn create_client(s3_config: FilesS3) -> Client { - let provider_name = "my-creds"; - let creds = Credentials::new( - s3_config.access_key_id, - s3_config.secret_access_key, - None, - None, - provider_name, - ); - - let config = Config::builder() - .region(Region::new(s3_config.region)) - .endpoint_url(s3_config.endpoint) - .force_path_style(s3_config.path_style_buckets) - .credentials_provider(creds) - .build(); - - Client::from_conf(config) -} - -/// Create an AES-256-GCM cipher -pub fn create_cipher(key: &str) -> Aes256Gcm { - let key = &BASE64_STANDARD.decode(key).expect("valid base64 string")[..]; - let key: &Key = key.into(); - Aes256Gcm::new(key) -} - /// Fetch a file from S3 (and decrypt it) -pub async fn fetch_from_s3(bucket_id: &str, path: &str, nonce: &str) -> Result> { - let config = config().await; - let client = create_client(config.files.s3); - - // Send a request for the file - let mut obj = - report_internal_error!(client.get_object().bucket(bucket_id).key(path).send().await)?; - - // Read the file from remote - let mut buf = vec![]; - while let Some(bytes) = obj.body.next().await { - let data = report_internal_error!(bytes)?; - report_internal_error!(buf.write_all(&data))?; - // is there a more efficient way to do this? - // we just want the Vec - } - - // File is not encrypted - if nonce.is_empty() { - return Ok(buf); - } - - // Recover nonce as bytes - let nonce = &BASE64_STANDARD.decode(nonce).unwrap()[..]; - let nonce: &Nonce = nonce.into(); - - // Decrypt the file - create_cipher(&config.files.encryption_key) - .decrypt_in_place(nonce, b"", &mut buf) - .map_err(|_| create_error!(InternalError))?; - - // Remove the authentication tag bytes that were added during encryption - buf.truncate(buf.len() - AUTHENTICATION_TAG_SIZE_BYTES); - - Ok(buf) +pub async fn fetch_from_s3(bucket_id: &str, path: &str, iv: &str) -> Result> { + let encryption = implementation::EncryptionKey::from_config().await; + let storage = implementation::S3Storage::from_config(encryption).await; + report_internal_error!(storage.fetch_and_decrypt_file(bucket_id, path, iv).await) } /// Encrypt and upload a file to S3 (returning its nonce/IV) pub async fn upload_to_s3(bucket_id: &str, path: &str, buf: &[u8]) -> Result { - let config = config().await; - let client = create_client(config.files.s3); - - // Generate a nonce - let nonce = Aes256Gcm::generate_nonce(&mut OsRng); - - // Extend the buffer for in-place encryption - let mut buf = [buf, &[0; AUTHENTICATION_TAG_SIZE_BYTES]].concat(); - - // Encrypt the file in place - create_cipher(&config.files.encryption_key) - .encrypt_in_place(&nonce, b"", &mut buf) - .map_err(|_| create_error!(InternalError))?; - - // Upload the file to remote - report_internal_error!( - client - .put_object() - .bucket(bucket_id) - .key(path) - .body(buf.into()) - .send() - .await - )?; - - Ok(BASE64_STANDARD.encode(nonce)) + let encryption = implementation::EncryptionKey::from_config().await; + let storage = implementation::S3Storage::from_config(encryption).await; + report_internal_error!(storage.encrypt_and_upload_file(bucket_id, path, buf).await) } /// Delete a file from S3 by path pub async fn delete_from_s3(bucket_id: &str, path: &str) -> Result<()> { - let config = config().await; - let client = create_client(config.files.s3); - - report_internal_error!( - client - .delete_object() - .bucket(bucket_id) - .key(path) - .send() - .await - )?; - - Ok(()) + let encryption = implementation::EncryptionKey::from_config().await; + let storage = implementation::S3Storage::from_config(encryption).await; + report_internal_error!(storage.delete_file(bucket_id, path).await) } /// Determine size of image at temp file pub fn image_size(f: &NamedTempFile) -> Option<(usize, usize)> { - if let Ok(size) = imagesize::size(f.path()) - .inspect_err(|err| tracing::error!("Failed to generate image size! {err:?}")) - { - Some((size.width, size.height)) - } else { - None - } + let media = MediaImpl::new(Files { + blocked_mime_types: Default::default(), + clamd_host: Default::default(), + encryption_key: Default::default(), + limit: FilesLimit { + max_mega_pixels: 0, + max_pixel_side: 0, + min_file_size: 0, + min_resolution: [0, 0], + }, + preview: Default::default(), + s3: FilesS3 { + access_key_id: Default::default(), + default_bucket: Default::default(), + endpoint: Default::default(), + path_style_buckets: Default::default(), + region: Default::default(), + secret_access_key: Default::default(), + }, + scan_mime_types: Default::default(), + webp_quality: Default::default(), + }); + + media.image_size(f) } /// Determine size of image with buffer pub fn image_size_vec(v: &[u8], mime: &str) -> Option<(usize, usize)> { - match mime { - "image/svg+xml" => { - let tree = - report_internal_error!(usvg::Tree::from_data(v, &Default::default())).ok()?; + let media = MediaImpl::new(Files { + blocked_mime_types: Default::default(), + clamd_host: Default::default(), + encryption_key: Default::default(), + limit: FilesLimit { + max_mega_pixels: 0, + max_pixel_side: 0, + min_file_size: 0, + min_resolution: [0, 0], + }, + preview: Default::default(), + s3: FilesS3 { + access_key_id: Default::default(), + default_bucket: Default::default(), + endpoint: Default::default(), + path_style_buckets: Default::default(), + region: Default::default(), + secret_access_key: Default::default(), + }, + scan_mime_types: Default::default(), + webp_quality: Default::default(), + }); - let size = tree.size(); - Some((size.width() as usize, size.height() as usize)) - } - _ => { - if let Ok(size) = imagesize::blob_size(v) - .inspect_err(|err| tracing::error!("Failed to generate image size! {err:?}")) - { - Some((size.width, size.height)) - } else { - None - } - } - } + media.image_size_vec(v, mime) } /// Determine size of video at temp file pub fn video_size(f: &NamedTempFile) -> Option<(i64, i64)> { - if let Ok(data) = ffprobe::ffprobe(f.path()) - .inspect_err(|err| tracing::error!("Failed to ffprobe file! {err:?}")) - { - // Use first valid stream - for stream in data.streams { - if let (Some(w), Some(h)) = (stream.width, stream.height) { - return Some((w, h)); - } - } + let media = MediaImpl::new(Files { + blocked_mime_types: Default::default(), + clamd_host: Default::default(), + encryption_key: Default::default(), + limit: FilesLimit { + max_mega_pixels: 0, + max_pixel_side: 0, + min_file_size: 0, + min_resolution: [0, 0], + }, + preview: Default::default(), + s3: FilesS3 { + access_key_id: Default::default(), + default_bucket: Default::default(), + endpoint: Default::default(), + path_style_buckets: Default::default(), + region: Default::default(), + secret_access_key: Default::default(), + }, + scan_mime_types: Default::default(), + webp_quality: Default::default(), + }); - None - } else { - None - } + media.video_size(f) } /// Decode image from reader pub fn decode_image(reader: &mut R, mime: &str) -> Result { - match mime { - // Read image using jxl-oxide crate - "image/jxl" => { - let jxl_image = report_internal_error!(jxl_oxide::JxlImage::builder().read(reader))?; - if let Ok(frame) = jxl_image.render_frame(0) { - match frame.color_channels().len() { - 3 => Ok(DynamicImage::ImageRgb8( - DynamicImage::ImageRgb32F( - ImageBuffer::from_vec( - jxl_image.width(), - jxl_image.height(), - frame.image().buf().to_vec(), - ) - .ok_or_else(|| create_error!(ImageProcessingFailed))?, - ) - .to_rgb8(), - )), - 4 => Ok(DynamicImage::ImageRgba8( - DynamicImage::ImageRgba32F( - ImageBuffer::from_vec( - jxl_image.width(), - jxl_image.height(), - frame.image().buf().to_vec(), - ) - .ok_or_else(|| create_error!(ImageProcessingFailed))?, - ) - .to_rgba8(), - )), - _ => Err(create_error!(ImageProcessingFailed)), - } - } else { - Err(create_error!(ImageProcessingFailed)) - } - } - // Read image using resvg - "image/svg+xml" => { - // usvg doesn't support Read trait so copy to buffer - let mut buf = Vec::new(); - report_internal_error!(reader.read_to_end(&mut buf))?; + let media = MediaImpl::new(Files { + blocked_mime_types: Default::default(), + clamd_host: Default::default(), + encryption_key: Default::default(), + limit: FilesLimit { + max_mega_pixels: 0, + max_pixel_side: 0, + min_file_size: 0, + min_resolution: [0, 0], + }, + preview: Default::default(), + s3: FilesS3 { + access_key_id: Default::default(), + default_bucket: Default::default(), + endpoint: Default::default(), + path_style_buckets: Default::default(), + region: Default::default(), + secret_access_key: Default::default(), + }, + scan_mime_types: Default::default(), + webp_quality: Default::default(), + }); - let tree = report_internal_error!(usvg::Tree::from_data(&buf, &Default::default()))?; - let size = tree.size(); - let mut pixmap = Pixmap::new(size.width() as u32, size.height() as u32) - .ok_or_else(|| create_error!(ImageProcessingFailed))?; - - let mut pixmap_mut = pixmap.as_mut(); - resvg::render(&tree, Default::default(), &mut pixmap_mut); - - Ok(DynamicImage::ImageRgba8( - ImageBuffer::from_vec( - size.width() as u32, - size.height() as u32, - pixmap.data().to_vec(), - ) - .ok_or_else(|| create_error!(ImageProcessingFailed))?, - )) - } - // Check if we can read using image-rs crate - _ => report_internal_error!(report_internal_error!( - image::ImageReader::new(reader).with_guessed_format() - )? - .decode()), - } + report_internal_error!(media.decode_image(reader, mime)) } /// Check whether given reader has a valid image pub fn is_valid_image(reader: &mut R, mime: &str) -> bool { - match mime { - // Check if we can read using jxl-oxide crate - "image/jxl" => jxl_oxide::JxlImage::builder() - .read(reader) - .inspect_err(|err| tracing::error!("Failed to read JXL! {err:?}")) - .is_ok(), - // Check if we can read using image-rs crate - _ => !matches!( - image::ImageReader::new(reader) - .with_guessed_format() - .inspect_err(|err| tracing::error!("Failed to read image! {err:?}")) - .map(|f| f.decode()), - Err(_) | Ok(Err(_)) - ), - } + let media = MediaImpl::new(Files { + blocked_mime_types: Default::default(), + clamd_host: Default::default(), + encryption_key: Default::default(), + limit: FilesLimit { + max_mega_pixels: 0, + max_pixel_side: 0, + min_file_size: 0, + min_resolution: [0, 0], + }, + preview: Default::default(), + s3: FilesS3 { + access_key_id: Default::default(), + default_bucket: Default::default(), + endpoint: Default::default(), + path_style_buckets: Default::default(), + region: Default::default(), + secret_access_key: Default::default(), + }, + scan_mime_types: Default::default(), + webp_quality: Default::default(), + }); + + media.is_valid_image(reader, mime) } /// Create thumbnail from given image pub async fn create_thumbnail(image: DynamicImage, tag: &str) -> Vec { - // Load configuration - let config = config().await; - let [w, h] = config.files.preview.get(tag).unwrap(); - - // Create thumbnail - //.resize(width as u32, height as u32, image::imageops::FilterType::Gaussian) - // resize is about 2.5x slower, - // thumbnail doesn't have terrible quality - // so we use thumbnail - let image = image.thumbnail(image.width().min(*w as u32), image.height().min(*h as u32)); - - // Encode it into WEBP - let encoder = webp::Encoder::from_image(&image).expect("Could not create encoder."); - if config.files.webp_quality != 100.0 { - encoder.encode(config.files.webp_quality).to_vec() - } else { - encoder.encode_lossless().to_vec() - } + let media = MediaImpl::from_config().await; + media.create_thumbnail(image, tag) } diff --git a/crates/core/files/src/repositories/encryption_repository.rs b/crates/core/files/src/repositories/encryption_repository.rs new file mode 100644 index 00000000..98e2a498 --- /dev/null +++ b/crates/core/files/src/repositories/encryption_repository.rs @@ -0,0 +1,6 @@ +use anyhow::Result; + +pub trait EncryptionRepository: Send + Sync + 'static { + fn decrypt_buffer(&self, buf: Vec, iv: &str) -> anyhow::Result>; + fn encrypt_buffer(&self, buf: &[u8]) -> Result<(Vec, String)>; +} diff --git a/crates/core/files/src/repositories/file_storage_repository.rs b/crates/core/files/src/repositories/file_storage_repository.rs new file mode 100644 index 00000000..af97be80 --- /dev/null +++ b/crates/core/files/src/repositories/file_storage_repository.rs @@ -0,0 +1,22 @@ +use anyhow::Result; + +#[async_trait::async_trait] +pub trait FileStorageRepository: Send + Sync + 'static { + async fn create_bucket(&self, bucket_id: &str) -> anyhow::Result<()>; + + async fn fetch_and_decrypt_file( + &self, + bucket_id: &str, + path: &str, + iv: &str, + ) -> Result>; + + async fn encrypt_and_upload_file( + &self, + bucket_id: &str, + path: &str, + buf: &[u8], + ) -> Result; + + async fn delete_file(&self, bucket_id: &str, path: &str) -> Result<()>; +} diff --git a/crates/core/files/src/repositories/media_repository.rs b/crates/core/files/src/repositories/media_repository.rs new file mode 100644 index 00000000..4cc4feca --- /dev/null +++ b/crates/core/files/src/repositories/media_repository.rs @@ -0,0 +1,30 @@ +use anyhow::Result; +use image::DynamicImage; +use std::io::{BufRead, Read, Seek}; +use tempfile::NamedTempFile; +use thiserror::Error; + +pub trait MediaRepository: Send + Sync + 'static { + fn image_size(&self, f: &NamedTempFile) -> Option<(usize, usize)>; + fn image_size_vec(&self, v: &[u8], mime: &str) -> Option<(usize, usize)>; + + fn decode_image( + &self, + reader: &mut R, + mime: &str, + ) -> Result; + + fn is_valid_image(&self, reader: &mut R, mime: &str) -> bool; + + fn create_thumbnail(&self, image: DynamicImage, tag: &str) -> Vec; + + fn video_size(&self, f: &NamedTempFile) -> Option<(i64, i64)>; +} + +#[derive(Debug, Error)] +pub enum MediaError { + #[error("image processing failed because {cause}")] + ImageProcessingFailed { cause: String }, + #[error(transparent)] + Unknown(#[from] anyhow::Error), +} diff --git a/crates/core/files/src/repositories/mod.rs b/crates/core/files/src/repositories/mod.rs new file mode 100644 index 00000000..cb0749e7 --- /dev/null +++ b/crates/core/files/src/repositories/mod.rs @@ -0,0 +1,7 @@ +mod encryption_repository; +mod file_storage_repository; +mod media_repository; + +pub use encryption_repository::EncryptionRepository; +pub use file_storage_repository::FileStorageRepository; +pub use media_repository::{MediaError, MediaRepository}; diff --git a/crates/core/files/tests/assets/anim-icos.apng b/crates/core/files/tests/assets/anim-icos.apng new file mode 100644 index 00000000..b50cbe36 Binary files /dev/null and b/crates/core/files/tests/assets/anim-icos.apng differ diff --git a/crates/core/files/tests/assets/anim-icos.gif b/crates/core/files/tests/assets/anim-icos.gif new file mode 100644 index 00000000..5b81b05e Binary files /dev/null and b/crates/core/files/tests/assets/anim-icos.gif differ diff --git a/crates/core/files/tests/assets/anim-icos.jxl b/crates/core/files/tests/assets/anim-icos.jxl new file mode 100644 index 00000000..bf3d1e44 Binary files /dev/null and b/crates/core/files/tests/assets/anim-icos.jxl differ diff --git a/crates/core/files/tests/assets/anim-icos.webp b/crates/core/files/tests/assets/anim-icos.webp new file mode 100644 index 00000000..ebf34e14 Binary files /dev/null and b/crates/core/files/tests/assets/anim-icos.webp differ diff --git a/crates/core/files/tests/assets/corrupted.png b/crates/core/files/tests/assets/corrupted.png new file mode 100644 index 00000000..f0fcce47 Binary files /dev/null and b/crates/core/files/tests/assets/corrupted.png differ diff --git a/crates/core/files/tests/assets/dice.jxl b/crates/core/files/tests/assets/dice.jxl new file mode 100644 index 00000000..d1a5a7d0 Binary files /dev/null and b/crates/core/files/tests/assets/dice.jxl differ diff --git a/crates/core/files/tests/assets/dice.webp b/crates/core/files/tests/assets/dice.webp new file mode 100644 index 00000000..ee3dfe53 Binary files /dev/null and b/crates/core/files/tests/assets/dice.webp differ diff --git a/crates/core/files/tests/assets/test.jpeg b/crates/core/files/tests/assets/test.jpeg new file mode 100644 index 00000000..504bd6bc Binary files /dev/null and b/crates/core/files/tests/assets/test.jpeg differ diff --git a/crates/core/files/tests/assets/test.png b/crates/core/files/tests/assets/test.png new file mode 100644 index 00000000..9de075cd Binary files /dev/null and b/crates/core/files/tests/assets/test.png differ diff --git a/crates/core/files/tests/integration_test.rs b/crates/core/files/tests/integration_test.rs new file mode 100644 index 00000000..1827a55a --- /dev/null +++ b/crates/core/files/tests/integration_test.rs @@ -0,0 +1,31 @@ +use std::io::Cursor; + +use revolt_files::{EncryptionKey, FileStorageRepository, MediaImpl, MediaRepository, S3Storage}; + +#[tokio::test] +async fn test_image_roundtrip_png() { + let media = MediaImpl::from_config().await; + let encryption = EncryptionKey::from_config().await; + let s3 = S3Storage::from_config(encryption).await; + + let buf = include_bytes!("./assets/test.png"); + let bucket_id = uuid::Uuid::new_v4().to_string(); + + s3.create_bucket(&bucket_id).await.unwrap(); + + let mut reader = Cursor::new(buf); + media.decode_image(&mut reader, "image/png").unwrap(); + + let iv = s3 + .encrypt_and_upload_file(&bucket_id, "/my-file", buf) + .await + .unwrap(); + + let buf = s3 + .fetch_and_decrypt_file(&bucket_id, "/my-file", &iv) + .await + .unwrap(); + + let mut reader = Cursor::new(buf); + media.decode_image(&mut reader, "image/png").unwrap(); +} diff --git a/crates/core/files/tests/s3_test.rs b/crates/core/files/tests/s3_test.rs new file mode 100644 index 00000000..95d1afcf --- /dev/null +++ b/crates/core/files/tests/s3_test.rs @@ -0,0 +1,42 @@ +use revolt_files::{EncryptionKey, FileStorageRepository, S3Storage}; + +#[tokio::test] +async fn test_upload_and_download() { + let encryption = EncryptionKey::from_config().await; + let s3 = S3Storage::from_config(encryption).await; + + let buf = [67]; + let bucket_id = uuid::Uuid::new_v4().to_string(); + + s3.create_bucket(&bucket_id).await.unwrap(); + + let iv = s3 + .encrypt_and_upload_file(&bucket_id, "/my-file", &buf) + .await + .unwrap(); + + let buf = s3 + .fetch_and_decrypt_file(&bucket_id, "/my-file", &iv) + .await + .unwrap(); + + assert_eq!(buf.len(), 1); + assert_eq!(buf[0], 67); +} + +#[tokio::test] +async fn test_upload_and_delete() { + let encryption = EncryptionKey::from_config().await; + let s3 = S3Storage::from_config(encryption).await; + + let buf = [67]; + let bucket_id = uuid::Uuid::new_v4().to_string(); + + s3.create_bucket(&bucket_id).await.unwrap(); + + s3.encrypt_and_upload_file(&bucket_id, "/my-file", &buf) + .await + .unwrap(); + + s3.delete_file(&bucket_id, "/my-file").await.unwrap(); +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 31578d3b..ed196614 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "stable" \ No newline at end of file +channel = "1.92.0" \ No newline at end of file