commit d0156ad51c95b43b2738d5ba0a2217958f9b4ef3 Author: O K Date: Mon May 18 11:45:56 2026 +0300 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..55cac87 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2303 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7178fe5f7d460b13895ebb9dcb28a3a6216d2df2574a0806cb51b555d297f38" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bitstream-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" + +[[package]] +name = "built" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "data-url" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fast_qr" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f5ea9788036fedaf55f43a2db0ba01eedf47d26fd6852f01e5cf51952571d57" +dependencies = [ + "resvg", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "fontconfig-parser" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" +dependencies = [ + "roxmltree", +] + +[[package]] +name = "fontdb" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "gif" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imagesize" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" + +[[package]] +name = "imgref" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40fac9d56ed6437b198fddba683305e8e2d651aa42647f00f5ae542e7f5c94a2" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "kurbo" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62" +dependencies = [ + "arrayvec", + "euclid", + "smallvec", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memmap2" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +dependencies = [ + "libc", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "nasm-rs" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4d98d0065f4b1daf164b3eafb11974c94662e5e2396cf03f32d0bb5c17da51" +dependencies = [ + "rayon", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +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 = "rav1e" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" +dependencies = [ + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av1-grain", + "bitstream-io", + "built", + "cc", + "cfg-if", + "interpolate_name", + "itertools", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "nasm-rs", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "once_cell", + "paste", + "profiling", + "rand", + "rand_chacha", + "simd_helpers", + "system-deps", + "thiserror", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.11.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5825c26fddd16ab9f515930d49028a630efec172e903483c94796cfe31893e6b" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "resvg" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8928798c0a55e03c9ca6c4c6846f76377427d2c1e1f7e6de3c06ae57942df43" +dependencies = [ + "gif", + "image-webp", + "log", + "pico-args", + "rgb", + "svgtypes", + "tiny-skia", + "usvg", + "zune-jpeg", +] + +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rustybuzz" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" +dependencies = [ + "bitflags 2.11.1", + "bytemuck", + "core_maths", + "log", + "smallvec", + "ttf-parser", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "server18004" +version = "0.1.0" +dependencies = [ + "axum", + "base64", + "clap", + "fast_qr", + "flate2", + "http-body-util", + "png", + "ravif", + "serde", + "serde_json", + "tempfile", + "tokio", + "toml 1.1.2+spec-1.1.0", + "tower", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + +[[package]] +name = "simplecss" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" +dependencies = [ + "log", +] + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +dependencies = [ + "float-cmp", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "svgtypes" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" +dependencies = [ + "kurbo", + "siphasher", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml 0.8.23", + "version-compare", +] + +[[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.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "png", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.3", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "winnow 0.7.15", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +dependencies = [ + "core_maths", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" + +[[package]] +name = "unicode-ccc" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-script" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" + +[[package]] +name = "unicode-vo" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "usvg" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef" +dependencies = [ + "base64", + "data-url", + "flate2", + "fontdb", + "imagesize", + "kurbo", + "log", + "pico-args", + "roxmltree", + "rustybuzz", + "simplecss", + "siphasher", + "strict-num", + "svgtypes", + "tiny-skia-path", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-jpeg" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..7465ca2 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "server18004" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "server18004" +path = "src/main.rs" + +[dependencies] +axum = "0.8.9" +base64 = "0.22.1" +clap = { version = "4.6.1", features = ["derive"] } +fast_qr = { version = "0.13.1", features = ["image"] } +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.149" +tokio = { version = "1.52.3", features = ["full"] } +toml = "1.1.2" +tracing = "0.1.44" +tracing-subscriber = { version = "0.3.23", features = ["env-filter"] } +png = "0.17.16" +ravif = "0.11.11" +flate2 = "1.0.35" + +[dev-dependencies] +tower = { version = "0.5", features = ["util"] } +http-body-util = "0.1" +tempfile = "3" diff --git a/README.md b/README.md new file mode 100644 index 0000000..b51d19e --- /dev/null +++ b/README.md @@ -0,0 +1,272 @@ +# server18004 ๐Ÿš€ + +A high-performance, production-ready QR code generation server written in Rust. It utilizes the [Axum](https://github.com/tokio-rs/axum) web framework and is highly optimized for maximum execution speed, minimal memory footprint, and low-latency rendering. + +The server leverages a multi-port paradigm to separate public restricted QR rendering from internal/unrestricted administration and generation APIs. + +--- + +## ๐ŸŒŸ Key Features + +- **Multi-Port Isolation**: + - **Port 4081 (SVG)**: Restricted vector QR generation via catch-all/fallback handler. + - **Port 4082 (PNG)**: Restricted high-speed raster QR generation. + - **Port 4084 (AVIF)**: Restricted high-speed modern AVIF QR generation. + - **Port 4083 (API)**: Unrestricted administration API for domain management and custom generation. +- **High-Performance Graphic Encoders**: + - **PNG Generator**: Features custom 4x manual bit-multiplication scaling to a 1-bit Grayscale raster buffer, completely bypassing expensive image scaling filters. Leverages the standard `png` crate with `Compression::Fast` and `FilterType::NoFilter` for low-latency output. + - **AVIF Generator**: Leverages the speed of the `ravif` encoder operating in Speed 10 (Fastest) mode with Lossless quality, backed by a custom 4x RGBA scaling loop. +- **Smart Reverse-Proxy Support**: + - Resolves client hostnames by inspecting `X-Forwarded-Host`, `X-Real-Host`, and standard `Host` headers. Compatible with Nginx, HAProxy, and Apache reverse-proxy setups. + - Detects request schema (HTTP vs HTTPS) using the `X-Forwarded-Proto` header. +- **Domain Restricted Generation**: + - Fallback endpoints validate the requester's base domain against an allowlist. + - A designated default domain (e.g., `18004.pro`) is always allowed. +- **Runtime Domain Management**: + - Read-optimized, thread-safe memory storage (`Arc>>`). + - Dynamic API additions/removals with immediate, safe file persistence. + +--- + +## ๐Ÿ› ๏ธ Port & Routing Architecture + +```mermaid +graph TD + subgraph Restricted Ports (Allowlist Validation) + P1[Port 4081: SVG] -->|Catch-all| H_SVG[Handle SVG] + P2[Port 4082: PNG] -->|Catch-all| H_PNG[Handle PNG] + P4[Port 4084: AVIF] -->|Catch-all| H_AVIF[Handle AVIF] + end + + subgraph Unrestricted Port + P3[Port 4083: API] -->|POST /generate| API_GEN[Custom QR Generation] + P3 -->|POST /domains/add| API_ADD[Add Domain] + P3 -->|POST /domains/remove| API_REM[Remove Domain] + P3 -->|GET /domains| API_LST[List Domains] + P3 -->|GET /health| API_HLT[Health Check] + end +``` + +### 1. Restricted Endpoints (Ports 4081, 4082, 4084) +These ports use fallback routes (catch-all). Any request path and query string will be automatically translated into a QR code pointing to the origin host & path. +* **How it works**: + 1. The server reads the hostname from the incoming headers (checking `X-Forwarded-Host`, then `X-Real-Host`, then `Host`). + 2. The first subdomain is stripped (e.g., `qr.example.com` becomes `example.com`). + 3. The base domain is checked against the allowlist. If it's not present (and doesn't match the default domain), a `403 Forbidden` response is returned. + 4. If allowed, it generates a QR code encoding `://` (e.g., `https://example.com/some/path?ref=123`). + +### 2. Unrestricted Endpoints (Port 4083) +A standard REST API for programmatic QR generation (without domain restrictions) and runtime control over the allowed domains. + +--- + +## ๐Ÿ“ฆ Installation & Setup + +### Requirements +- Rust (Cargo) 1.70+ + +### Local Development +To run the server locally: +```bash +cargo run -- --config-path ./server.conf --domains-path ./domains.conf +``` + +### Production Deployment +The project comes with a robust installer (`install.sh`) that builds the release binary, creates a dedicated, unprivileged system user (`qrserver`), registers config templates, and configures a `systemd` service: + +```bash +# 1. Build and install with root/sudo privileges +sudo ./install.sh + +# 2. Start and enable the service +sudo systemctl enable --now server18004 + +# 3. Verify the status and view logs +sudo systemctl status server18004 +sudo journalctl -u server18004 -f +``` + +--- + +## โš™๏ธ Configuration + +### Server Configuration (`/etc/server18004/server.conf`) +An easy-to-use TOML file configuring server ports and the default domain: + +```toml +# Port for restricted SVG QR code generation +port_svg = 4081 + +# Port for restricted PNG QR code generation +port_png = 4082 + +# Port for unrestricted JSON API +port_api = 4083 + +# Port for restricted AVIF QR code generation +port_avif = 4084 + +# Default domain that is always allowed on restricted ports +default_domain = "example.com" +``` + +### Domains Allowlist (`/etc/server18004/domains.conf`) +A simple line-based text file. Comments starting with `#` and blank lines are ignored. +```text +# Allowed base domains +example.com +mycompany.org +testdomain.dev +``` + +--- + +## ๐Ÿ“– Sample Usage & API Specifications + +### 1. Fallback QR Code Generation (Restricted Ports) + +When navigating to or requesting any path from the restricted ports, a QR code is returned directly. + +#### SVG Format (Port 4081) +Assuming `example.com` is in the allowed domain list: +```bash +curl -i -H "Host: qr.example.com" http://localhost:4081/welcome?user=john +``` +* **Result**: Returns an `image/svg+xml` payload containing a QR code that redirects to `https://example.com/welcome?user=john`. + +#### PNG Format (Port 4082) +```bash +curl -i -H "Host: qr.example.com" http://localhost:4082/app/download +``` +* **Result**: Returns an `image/png` payload containing a QR code encoding `https://example.com/app/download`. + +#### AVIF Format (Port 4084) +```bash +curl -i -H "Host: qr.example.com" http://localhost:4084/promo +``` +* **Result**: Returns an `image/avif` payload containing a QR code encoding `https://example.com/promo`. + +--- + +### 2. Programmatic API (Port 4083) + +The API port allows custom QR generation with flexible format settings and base64 options. + +#### POST `/generate` +Generates a QR code for arbitrary text content. + +##### Payload Schema: +```json +{ + "text": "Your string here", + "ecl": "L", // Error Correction Level: L, M, Q, H (Default: L) + "format": "svg", // Output: svg, png, avif, base64, base64url (Default: svg) + "base64_source": "png", // For base64: png, svg, avif (Default: png) + "module_size": 10 // Optional custom scaling factor +} +``` + +##### Example 1: Direct PNG Output +```bash +curl -X POST http://localhost:4083/generate \ + -H "Content-Type: application/json" \ + -d '{ + "text": "https://rust-lang.org", + "format": "png", + "ecl": "M" + }' --output qr.png +``` + +##### Example 2: Base64 Encoded JSON Response +```bash +curl -X POST http://localhost:4083/generate \ + -H "Content-Type: application/json" \ + -d '{ + "text": "Hello World", + "format": "base64", + "base64_source": "svg" + }' +``` +**Response JSON**: +```json +{ + "data": "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMyAzMyIgc2hhcGUtcmVuZGVyaW5nPSJjcmlzcEVkZ2VzIj4...", + "format": "base64", + "source": "svg" +} +``` + +--- + +### 3. Domain Management API (Port 4083) + +#### GET `/domains` +Retrieves a list of all currently allowed domains. +```bash +curl http://localhost:4083/domains +``` +**Response**: +```json +{ + "success": true, + "message": "3 domain(s) configured", + "domains": ["example.com", "mycompany.org", "testdomain.dev"] +} +``` + +#### POST `/domains/add` +Adds a domain to the allowlist and immediately persists it to the configuration file on disk. +```bash +curl -X POST http://localhost:4083/domains/add \ + -H "Content-Type: application/json" \ + -d '{"domain": "newdomain.com"}' +``` +**Response**: +```json +{ + "success": true, + "message": "Domain 'newdomain.com' added successfully", + "domains": ["example.com", "mycompany.org", "testdomain.dev", "newdomain.com"] +} +``` + +#### POST `/domains/remove` +Removes a domain from the allowlist and updates the file on disk. +```bash +curl -X POST http://localhost:4083/domains/remove \ + -H "Content-Type: application/json" \ + -d '{"domain": "newdomain.com"}' +``` +**Response**: +```json +{ + "success": true, + "message": "Domain 'newdomain.com' removed successfully", + "domains": ["example.com", "mycompany.org", "testdomain.dev"] +} +``` + +#### GET `/health` +Simple health check endpoint for proxy checking or system monitors. +```bash +curl -i http://localhost:4083/health +``` +**Response**: +```http +HTTP/1.1 200 OK +content-type: text/plain; charset=utf-8 +content-length: 2 + +OK +``` + +--- + +## ๐Ÿงช Testing + +The codebase has a comprehensive suite of unit tests verifying domain manipulation, Host/IP extraction, and QR encoders. + +To run the tests: +```bash +cargo test +``` diff --git a/config/domains.conf.example b/config/domains.conf.example new file mode 100644 index 0000000..afecb8e --- /dev/null +++ b/config/domains.conf.example @@ -0,0 +1,15 @@ +# Domain Allowlist for 18004 Server +# One domain per line. Lines starting with # are comments. +# These domains are allowed on the restricted SVG/PNG ports. +# +# The server strips the first subdomain from incoming requests: +# Request to qr.example.com -> checks "example.com" against this list +# Request to png.mysite.org -> checks "mysite.org" against this list +# +# Domains can also be added/removed at runtime via the API port: +# POST /domains/add {"domain": "newsite.com"} +# POST /domains/remove {"domain": "oldsite.com"} +# Runtime changes are persisted back to this file. + +example.com +mysite.org diff --git a/config/server.conf.example b/config/server.conf.example new file mode 100644 index 0000000..3c807eb --- /dev/null +++ b/config/server.conf.example @@ -0,0 +1,18 @@ +# 18004 Server Configuration +# Copy this file to /etc/server18004/server.conf + +# Port for restricted SVG QR code generation +port_svg = 4081 + +# Port for restricted PNG QR code generation +port_png = 4082 + +# Port for unrestricted JSON API (generate, domain management) +port_api = 4083 + +# Port for restricted AVIF QR code generation +port_avif = 4084 + +# Default domain that is always allowed on restricted ports +# (does not need to be in the domains allowlist) +default_domain = "example.com" diff --git a/config/server18004.service b/config/server18004.service new file mode 100644 index 0000000..2988c20 --- /dev/null +++ b/config/server18004.service @@ -0,0 +1,56 @@ +[Unit] +Description=QR Code Generation Server (ISO 18004) +Documentation=https://github.com/your-org/server18004 +After=network.target +Wants=network-online.target + +[Service] +Type=simple +User=qrserver +Group=qrserver + +# Binary location (adjust after cargo build --release) +ExecStart=/usr/local/bin/server18004 \ + --config-path /etc/server18004/server.conf \ + --domains-path /etc/server18004/domains.conf + +# Restart policy +Restart=on-failure +RestartSec=5 +StartLimitIntervalSec=60 +StartLimitBurst=5 + +# Environment +Environment=RUST_LOG=info +# Uncomment for debug logging: +# Environment=RUST_LOG=debug + +# Security hardening +NoNewPrivileges=true +ProtectSystem=strict +ProtectHome=true +PrivateTmp=true +PrivateDevices=true +ProtectKernelTunables=true +ProtectKernelModules=true +ProtectControlGroups=true +RestrictSUIDSGID=true +MemoryDenyWriteExecute=true +LockPersonality=true +RestrictRealtime=true +RestrictNamespaces=true + +# Allow writing to config directory (for domain persistence) +ReadWritePaths=/etc/server18004 + +# Resource limits +LimitNOFILE=65536 +LimitNPROC=4096 + +# Logging +StandardOutput=journal +StandardError=journal +SyslogIdentifier=server18004 + +[Install] +WantedBy=multi-user.target diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..fe064ba --- /dev/null +++ b/install.sh @@ -0,0 +1,77 @@ +#!/bin/bash +set -euo pipefail + +# QR Server Installation Script +# Run as root or with sudo + +BINARY_NAME="server18004" +INSTALL_DIR="/usr/local/bin" +CONFIG_DIR="/etc/server18004" +SERVICE_USER="qrserver" +SERVICE_FILE="/etc/systemd/system/server18004.service" + +echo "=== server18004 Installer ===" + +# 1. Build release binary +echo "[1/5] Building release binary..." +if command -v cargo &>/dev/null; then + cargo build --release + echo " โœ“ Built target/release/${BINARY_NAME}" +elif [ -f "target/release/${BINARY_NAME}" ]; then + echo " โœ“ Found existing binary in target/release/${BINARY_NAME}, skipping build" +else + echo " โœ— Error: 'cargo' not found and no existing binary in target/release/" + echo " Please run 'cargo build --release' as your normal user first," + echo " then run this script with sudo." + exit 1 +fi + +# 2. Create service user +echo "[2/5] Creating service user..." +if ! id -u "${SERVICE_USER}" &>/dev/null; then + useradd --system --no-create-home --shell /usr/sbin/nologin "${SERVICE_USER}" + echo " โœ“ Created user ${SERVICE_USER}" +else + echo " โœ“ User ${SERVICE_USER} already exists" +fi + +# 3. Install binary +echo "[3/5] Installing binary..." +cp "target/release/${BINARY_NAME}" "${INSTALL_DIR}/${BINARY_NAME}" +chmod 755 "${INSTALL_DIR}/${BINARY_NAME}" +echo " โœ“ Installed to ${INSTALL_DIR}/${BINARY_NAME}" + +# 4. Install config files +echo "[4/5] Installing configuration..." +mkdir -p "${CONFIG_DIR}" +if [ ! -f "${CONFIG_DIR}/server.conf" ]; then + cp config/server.conf.example "${CONFIG_DIR}/server.conf" + echo " โœ“ Installed server.conf" +else + echo " โš  server.conf already exists, skipping (see config/server.conf.example)" +fi +if [ ! -f "${CONFIG_DIR}/domains.conf" ]; then + cp config/domains.conf.example "${CONFIG_DIR}/domains.conf" + echo " โœ“ Installed domains.conf" +else + echo " โš  domains.conf already exists, skipping (see config/domains.conf.example)" +fi +chown -R "${SERVICE_USER}:${SERVICE_USER}" "${CONFIG_DIR}" +echo " โœ“ Set ownership to ${SERVICE_USER}" + +# 5. Install systemd service +echo "[5/5] Installing systemd service..." +cp config/server18004.service "${SERVICE_FILE}" +systemctl daemon-reload +echo " โœ“ Service installed" + +echo "" +echo "=== Installation Complete ===" +echo "" +echo "Next steps:" +echo " 1. Edit config: sudo nano ${CONFIG_DIR}/server.conf" +echo " 2. Edit domains: sudo nano ${CONFIG_DIR}/domains.conf" +echo " 3. Start service: sudo systemctl start server18004" +echo " 4. Enable on boot: sudo systemctl enable server18004" +echo " 5. Check status: sudo systemctl status server18004" +echo " 6. View logs: sudo journalctl -u server18004 -f" diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..612095d --- /dev/null +++ b/src/config.rs @@ -0,0 +1,87 @@ +use clap::Parser; +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; +use std::fs::File; +use std::io::{BufRead, BufReader, Write}; +use std::path::Path; + +#[derive(Parser, Debug, Clone)] +#[command(author, version, about, long_about = None)] +pub struct Cli { + #[arg(short, long, default_value = "/etc/server18004/server.conf")] + pub config_path: String, + + #[arg(short, long, default_value = "/etc/server18004/domains.conf")] + pub domains_path: String, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct ServerConfig { + pub port_svg: u16, + pub port_png: u16, + pub port_api: u16, + pub port_avif: u16, + pub default_domain: String, +} + +impl Default for ServerConfig { + fn default() -> Self { + Self { + port_svg: 4081, + port_png: 4082, + port_api: 4083, + port_avif: 4084, + default_domain: "example.com".to_string(), + } + } +} + +impl ServerConfig { + pub fn load(path: &str) -> Result> { + let content = std::fs::read_to_string(path)?; + let config: ServerConfig = toml::from_str(&content)?; + Ok(config) + } + + pub fn generate_default(path: &str) -> Result<(), Box> { + let default_config = ServerConfig::default(); + if let Some(parent) = Path::new(path).parent() { + std::fs::create_dir_all(parent)?; + } + let content = toml::to_string_pretty(&default_config)?; + std::fs::write(path, content)?; + Ok(()) + } +} + +pub fn load_domains(path: &str) -> std::io::Result> { + let file = match File::open(path) { + Ok(f) => f, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + return Ok(HashSet::new()); + } + Err(e) => return Err(e), + }; + + let reader = BufReader::new(file); + let mut domains = HashSet::new(); + for line in reader.lines() { + let line = line?; + let trimmed = line.trim(); + if !trimmed.is_empty() && !trimmed.starts_with('#') { + domains.insert(trimmed.to_string()); + } + } + Ok(domains) +} + +pub fn save_domains(path: &str, domains: &HashSet) -> std::io::Result<()> { + if let Some(parent) = Path::new(path).parent() { + std::fs::create_dir_all(parent)?; + } + let mut file = File::create(path)?; + for domain in domains { + writeln!(file, "{}", domain)?; + } + Ok(()) +} diff --git a/src/handlers/api.rs b/src/handlers/api.rs new file mode 100644 index 0000000..2e58e37 --- /dev/null +++ b/src/handlers/api.rs @@ -0,0 +1,328 @@ +use axum::{ + extract::State, + http::StatusCode, + response::IntoResponse, + Json, +}; +use base64::{engine::general_purpose, Engine as _}; +use serde::{Deserialize, Serialize}; + +use crate::config; +use crate::qr; +use crate::state::AppState; +use fast_qr::ECL; + +// โ”€โ”€โ”€ QR Generation Request โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +#[derive(Deserialize)] +pub struct GenerateRequest { + /// The text content to encode in the QR code. + pub text: String, + + /// Error correction level: "L", "M", "Q", or "H". Defaults to "L". + #[serde(default = "default_ecl")] + pub ecl: String, + + /// Output format: "svg", "png", "base64", or "base64url". Defaults to "svg". + #[serde(default = "default_format")] + pub format: String, + + /// For base64/base64url: the underlying image format to encode. + /// Either "png" or "svg". Defaults to "png". + #[serde(default = "default_base64_source")] + pub base64_source: String, + + /// Module size (pixels per block). Defaults to 10. + pub module_size: Option, +} + +fn default_ecl() -> String { + "L".to_string() +} + +fn default_format() -> String { + "svg".to_string() +} + +fn default_base64_source() -> String { + "png".to_string() +} + +#[derive(Serialize)] +pub struct Base64Response { + pub data: String, + pub format: String, + pub source: String, +} + +#[derive(Serialize)] +pub struct ErrorResponse { + pub error: String, +} + +/// POST /generate โ€” Generate a custom QR code. +pub async fn generate_qr( + Json(req): Json, +) -> impl IntoResponse { + let ecl = qr::parse_ecl(&req.ecl); + + match req.format.to_lowercase().as_str() { + "svg" => match qr::generate_svg(&req.text, ecl, req.module_size) { + Ok(svg) => ( + StatusCode::OK, + [("content-type", "image/svg+xml")], + svg, + ) + .into_response(), + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse { error: e }), + ) + .into_response(), + }, + "png" => match qr::generate_png(&req.text, ecl, req.module_size) { + Ok(png) => ( + StatusCode::OK, + [("content-type", "image/png")], + png, + ) + .into_response(), + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse { error: e }), + ) + .into_response(), + }, + "avif" => match qr::generate_avif(&req.text, ecl, req.module_size) { + Ok(avif) => ( + StatusCode::OK, + [("content-type", "image/avif")], + avif, + ) + .into_response(), + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse { error: e }), + ) + .into_response(), + }, + "base64" => generate_base64(&req.text, ecl, &req.base64_source, req.module_size, false), + "base64url" => generate_base64(&req.text, ecl, &req.base64_source, req.module_size, true), + other => ( + StatusCode::BAD_REQUEST, + Json(ErrorResponse { + error: format!("Unknown format '{}'. Use: svg, png, base64, base64url", other), + }), + ) + .into_response(), + } +} + +fn generate_base64( + text: &str, + ecl: ECL, + source: &str, + module_size: Option, + url_safe: bool, +) -> axum::response::Response { + let (data_bytes, source_name) = match source.to_lowercase().as_str() { + "svg" => match qr::generate_svg(text, ecl, module_size) { + Ok(svg) => (svg.into_bytes(), "svg"), + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse { error: e }), + ) + .into_response(); + } + }, + "avif" => match qr::generate_avif(text, ecl, module_size) { + Ok(avif) => (avif, "avif"), + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse { error: e }), + ) + .into_response(); + } + }, + _ => match qr::generate_png(text, ecl, module_size) { + Ok(png) => (png, "png"), + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse { error: e }), + ) + .into_response(); + } + }, + }; + + let encoded = if url_safe { + general_purpose::URL_SAFE_NO_PAD.encode(&data_bytes) + } else { + general_purpose::STANDARD.encode(&data_bytes) + }; + + let format_name = if url_safe { "base64url" } else { "base64" }; + + ( + StatusCode::OK, + Json(Base64Response { + data: encoded, + format: format_name.to_string(), + source: source_name.to_string(), + }), + ) + .into_response() +} + +// โ”€โ”€โ”€ Domain Management โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +#[derive(Deserialize)] +pub struct DomainRequest { + pub domain: String, +} + +#[derive(Serialize)] +pub struct DomainResponse { + pub success: bool, + pub message: String, + pub domains: Vec, +} + +/// POST /domains/add โ€” Add a domain to the allowlist and persist to disk. +pub async fn add_domain( + State(state): State, + Json(req): Json, +) -> impl IntoResponse { + let domain = req.domain.trim().to_lowercase(); + if domain.is_empty() { + return ( + StatusCode::BAD_REQUEST, + Json(DomainResponse { + success: false, + message: "Domain cannot be empty".to_string(), + domains: vec![], + }), + ); + } + + // Safety: DNS max length is 253. Only allow valid domain characters to prevent injection/file corruption. + if domain.len() > 253 || !domain.chars().all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-') { + return ( + StatusCode::BAD_REQUEST, + Json(DomainResponse { + success: false, + message: "Invalid domain format (ASCII alphanumeric, dots, and hyphens only)".to_string(), + domains: vec![], + }), + ); + } + + let mut domains = state.domains.write().await; + let was_new = domains.insert(domain.clone()); + + // Persist to disk + if let Err(e) = config::save_domains(&state.domains_path, &domains) { + tracing::error!(error = %e, "Failed to persist domains to disk"); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(DomainResponse { + success: false, + message: format!("Domain added to memory but failed to persist: {}", e), + domains: domains.iter().cloned().collect(), + }), + ); + } + + let message = if was_new { + format!("Domain '{}' added successfully", domain) + } else { + format!("Domain '{}' was already in the list", domain) + }; + + tracing::info!(%domain, "Domain added to allowlist"); + + ( + StatusCode::OK, + Json(DomainResponse { + success: true, + message, + domains: domains.iter().cloned().collect(), + }), + ) +} + +/// POST /domains/remove โ€” Remove a domain from the allowlist and persist to disk. +pub async fn remove_domain( + State(state): State, + Json(req): Json, +) -> impl IntoResponse { + let domain = req.domain.trim().to_lowercase(); + if domain.is_empty() { + return ( + StatusCode::BAD_REQUEST, + Json(DomainResponse { + success: false, + message: "Domain cannot be empty".to_string(), + domains: vec![], + }), + ); + } + + let mut domains = state.domains.write().await; + let was_present = domains.remove(&domain); + + // Persist to disk + if let Err(e) = config::save_domains(&state.domains_path, &domains) { + tracing::error!(error = %e, "Failed to persist domains to disk"); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(DomainResponse { + success: false, + message: format!("Domain removed from memory but failed to persist: {}", e), + domains: domains.iter().cloned().collect(), + }), + ); + } + + let message = if was_present { + format!("Domain '{}' removed successfully", domain) + } else { + format!("Domain '{}' was not in the list", domain) + }; + + tracing::info!(%domain, "Domain removed from allowlist"); + + ( + StatusCode::OK, + Json(DomainResponse { + success: true, + message, + domains: domains.iter().cloned().collect(), + }), + ) +} + +/// GET /domains โ€” List all currently allowed domains. +pub async fn list_domains( + State(state): State, +) -> impl IntoResponse { + let domains = state.domains.read().await; + let list: Vec = domains.iter().cloned().collect(); + + ( + StatusCode::OK, + Json(DomainResponse { + success: true, + message: format!("{} domain(s) configured", list.len()), + domains: list, + }), + ) +} + +/// GET /health โ€” Simple health check endpoint. +pub async fn health() -> impl IntoResponse { + (StatusCode::OK, "OK") +} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs new file mode 100644 index 0000000..63a8454 --- /dev/null +++ b/src/handlers/mod.rs @@ -0,0 +1,2 @@ +pub mod restricted; +pub mod api; diff --git a/src/handlers/restricted.rs b/src/handlers/restricted.rs new file mode 100644 index 0000000..fbb020a --- /dev/null +++ b/src/handlers/restricted.rs @@ -0,0 +1,252 @@ +use axum::{ + extract::State, + http::{HeaderMap, StatusCode, Uri}, + response::IntoResponse, +}; +use crate::qr; +use crate::state::AppState; +use fast_qr::ECL; + +/// Extracts the effective host from proxy headers. +/// Priority: X-Forwarded-Host > X-Real-Host > Host header. +/// Compatible with HAProxy and Apache reverse proxy setups. +fn extract_host(headers: &HeaderMap) -> Option { + // X-Forwarded-Host may contain multiple hosts (comma-separated), take the first + if let Some(val) = headers.get("x-forwarded-host") { + if let Ok(s) = val.to_str() { + let host = s.split(',').next().unwrap_or("").trim(); + if !host.is_empty() { + // Strip port if present + return Some(strip_port(host).to_lowercase()); + } + } + } + + // X-Real-Host (sometimes used by nginx/haproxy) + if let Some(val) = headers.get("x-real-host") { + if let Ok(s) = val.to_str() { + let host = s.trim(); + if !host.is_empty() { + return Some(strip_port(host).to_lowercase()); + } + } + } + + // Standard Host header + if let Some(val) = headers.get("host") { + if let Ok(s) = val.to_str() { + let host = s.trim(); + if !host.is_empty() { + return Some(strip_port(host).to_lowercase()); + } + } + } + + None +} + +/// Strips port number from a host string (e.g., "example.com:8080" -> "example.com", "[::1]:8080" -> "[::1]"). +fn strip_port(host: &str) -> &str { + if let Some(idx) = host.rfind(']') { + // It's an IPv6 address, return the part inside and including brackets + return &host[..=idx]; + } + // For regular hostnames, split at the first colon + host.split(':').next().unwrap_or(host) +} + +/// Strips the first subdomain from a hostname. +/// e.g., "qr.example.com" -> "example.com" +/// e.g., "sub.deep.example.com" -> "deep.example.com" +/// If there's no subdomain (e.g., "example.com"), returns as-is. +fn strip_first_subdomain(host: &str) -> &str { + match host.find('.') { + Some(idx) => &host[idx + 1..], + None => host, + } +} + +/// Constructs the URL to encode in the QR code. +/// Uses X-Forwarded-Proto to detect scheme (defaults to https). +fn build_qr_url(headers: &HeaderMap, host: &str, uri: &Uri) -> String { + let scheme = headers + .get("x-forwarded-proto") + .and_then(|v| v.to_str().ok()) + .map(|s| s.split(',').next().unwrap_or("https").trim()) + .unwrap_or("https"); + + let base_domain = strip_first_subdomain(host); + let path = uri.path(); + let query = uri.query().map(|q| format!("?{}", q)).unwrap_or_default(); + + format!("{}://{}{}{}", scheme, base_domain, path, query) +} + +/// Handler for Port 1 (restricted, SVG output). +pub async fn handle_svg( + State(state): State, + headers: HeaderMap, + uri: Uri, +) -> impl IntoResponse { + handle_restricted(state, headers, uri, OutputFormat::Svg).await +} + +/// Handler for Port 2 (restricted, PNG output). +pub async fn handle_png( + State(state): State, + headers: HeaderMap, + uri: Uri, +) -> impl IntoResponse { + handle_restricted(state, headers, uri, OutputFormat::Png).await +} + +/// Handler for Port 4 (restricted, AVIF output). +pub async fn handle_avif( + State(state): State, + headers: HeaderMap, + uri: Uri, +) -> impl IntoResponse { + handle_restricted(state, headers, uri, OutputFormat::Avif).await +} + +enum OutputFormat { + Svg, + Png, + Avif, +} + +async fn handle_restricted( + state: AppState, + headers: HeaderMap, + uri: Uri, + format: OutputFormat, +) -> axum::response::Response { + // 1. Extract host + let host = match extract_host(&headers) { + Some(h) => h, + None => { + return ( + StatusCode::BAD_REQUEST, + "Missing Host header", + ) + .into_response(); + } + }; + + // 2. Determine the base domain (strip first subdomain) + let base_domain = strip_first_subdomain(&host).to_string(); + + // 3. Check domain against allowlist + { + let domains = state.domains.read().await; + if !domains.contains(&base_domain) && base_domain != state.default_domain { + return ( + StatusCode::FORBIDDEN, + format!("Domain '{}' is not in the allowed list", base_domain), + ) + .into_response(); + } + } + + // 4. Build the URL to encode + let qr_url = build_qr_url(&headers, &host, &uri); + + tracing::debug!(host = %host, base_domain = %base_domain, qr_url = %qr_url, "Generating QR code"); + + // 5. Generate QR code in the requested format + match format { + OutputFormat::Svg => match qr::generate_svg(&qr_url, ECL::L, None) { + Ok(svg) => ( + StatusCode::OK, + [ + ("content-type", "image/svg+xml"), + ("cache-control", "public, max-age=86400"), + ], + svg, + ) + .into_response(), + Err(e) => { + tracing::error!(error = %e, "SVG generation failed"); + (StatusCode::INTERNAL_SERVER_ERROR, e).into_response() + } + }, + OutputFormat::Png => match qr::generate_png(&qr_url, ECL::L, None) { + Ok(png) => ( + StatusCode::OK, + [ + ("content-type", "image/png"), + ("cache-control", "public, max-age=86400"), + ], + png, + ) + .into_response(), + Err(e) => { + tracing::error!(error = %e, "PNG generation failed"); + (StatusCode::INTERNAL_SERVER_ERROR, e).into_response() + } + }, + OutputFormat::Avif => match qr::generate_avif(&qr_url, ECL::L, None) { + Ok(avif) => ( + StatusCode::OK, + [ + ("content-type", "image/avif"), + ("cache-control", "public, max-age=86400"), + ], + avif, + ) + .into_response(), + Err(e) => { + tracing::error!(error = %e, "AVIF generation failed"); + (StatusCode::INTERNAL_SERVER_ERROR, e).into_response() + } + }, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_strip_port() { + assert_eq!(strip_port("example.com:8080"), "example.com"); + assert_eq!(strip_port("example.com"), "example.com"); + assert_eq!(strip_port("[::1]:8080"), "[::1]"); + } + + #[test] + fn test_strip_first_subdomain() { + assert_eq!(strip_first_subdomain("qr.example.com"), "example.com"); + assert_eq!( + strip_first_subdomain("a.b.example.com"), + "b.example.com" + ); + assert_eq!(strip_first_subdomain("example.com"), "com"); + assert_eq!(strip_first_subdomain("localhost"), "localhost"); + } + + #[test] + fn test_extract_host_x_forwarded() { + let mut headers = HeaderMap::new(); + headers.insert("x-forwarded-host", "qr.example.com:443".parse().unwrap()); + headers.insert("host", "internal.server:8080".parse().unwrap()); + assert_eq!(extract_host(&headers), Some("qr.example.com".to_string())); + } + + #[test] + fn test_extract_host_multiple_forwarded() { + let mut headers = HeaderMap::new(); + headers.insert( + "x-forwarded-host", + "qr.example.com, proxy.internal".parse().unwrap(), + ); + assert_eq!(extract_host(&headers), Some("qr.example.com".to_string())); + } + + #[test] + fn test_extract_host_fallback() { + let mut headers = HeaderMap::new(); + headers.insert("host", "qr.example.com".parse().unwrap()); + assert_eq!(extract_host(&headers), Some("qr.example.com".to_string())); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..f3bdec3 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,4 @@ +pub mod config; +pub mod handlers; +pub mod qr; +pub mod state; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..7eba6b8 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,107 @@ +use axum::{routing::{get, post}, Router}; +use clap::Parser; +use std::net::SocketAddr; +use tokio::net::TcpListener; +use tracing_subscriber::EnvFilter; + +use server18004::config::{self, Cli, ServerConfig}; +use server18004::handlers; +use server18004::state::AppState; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Initialize tracing + tracing_subscriber::fmt() + .with_env_filter( + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")), + ) + .init(); + + // Parse CLI args + let cli = Cli::parse(); + + // Load or generate server config + let server_config = match ServerConfig::load(&cli.config_path) { + Ok(cfg) => { + tracing::info!(path = %cli.config_path, "Loaded server configuration"); + cfg + } + Err(e) => { + tracing::warn!( + error = %e, + path = %cli.config_path, + "Failed to load config, generating default" + ); + ServerConfig::generate_default(&cli.config_path)?; + ServerConfig::load(&cli.config_path)? + } + }; + + // Load domains + let domains = config::load_domains(&cli.domains_path)?; + tracing::info!(count = domains.len(), path = %cli.domains_path, "Loaded domain allowlist"); + + // Build shared state + let app_state = AppState::new( + domains, + cli.domains_path.clone(), + server_config.default_domain.clone(), + ); + + // โ”€โ”€ Port 1: Restricted SVG โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + let svg_state = app_state.clone(); + let svg_router = Router::new() + .fallback(handlers::restricted::handle_svg) + .layer(axum::extract::DefaultBodyLimit::max(1024 * 1024)) // 1MB limit + .with_state(svg_state); + + // โ”€โ”€ Port 2: Restricted PNG โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + let png_state = app_state.clone(); + let png_router = Router::new() + .fallback(handlers::restricted::handle_png) + .layer(axum::extract::DefaultBodyLimit::max(1024 * 1024)) // 1MB limit + .with_state(png_state); + + // โ”€โ”€ Port 4: Restricted AVIF โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + let avif_state = app_state.clone(); + let avif_router = Router::new() + .fallback(handlers::restricted::handle_avif) + .layer(axum::extract::DefaultBodyLimit::max(1024 * 1024)) // 1MB limit + .with_state(avif_state); + + // โ”€โ”€ Port 3: Unrestricted API โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + let api_state = app_state.clone(); + let api_router = Router::new() + .route("/generate", post(handlers::api::generate_qr)) + .route("/domains/add", post(handlers::api::add_domain)) + .route("/domains/remove", post(handlers::api::remove_domain)) + .route("/domains", get(handlers::api::list_domains)) + .route("/health", get(handlers::api::health)) + .layer(axum::extract::DefaultBodyLimit::max(1024 * 1024)) // 1MB limit + .with_state(api_state); + + // โ”€โ”€ Bind and serve โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + let addr_svg = SocketAddr::from(([0, 0, 0, 0], server_config.port_svg)); + let addr_png = SocketAddr::from(([0, 0, 0, 0], server_config.port_png)); + let addr_api = SocketAddr::from(([0, 0, 0, 0], server_config.port_api)); + let addr_avif = SocketAddr::from(([0, 0, 0, 0], server_config.port_avif)); + + let listener_svg = TcpListener::bind(addr_svg).await?; + let listener_png = TcpListener::bind(addr_png).await?; + let listener_api = TcpListener::bind(addr_api).await?; + let listener_avif = TcpListener::bind(addr_avif).await?; + + tracing::info!(addr = %addr_svg, "Starting restricted SVG server"); + tracing::info!(addr = %addr_png, "Starting restricted PNG server"); + tracing::info!(addr = %addr_api, "Starting unrestricted API server"); + tracing::info!(addr = %addr_avif, "Starting restricted AVIF server"); + + let svc_svg = axum::serve(listener_svg, svg_router); + let svc_png = axum::serve(listener_png, png_router); + let svc_api = axum::serve(listener_api, api_router); + let svc_avif = axum::serve(listener_avif, avif_router); + + tokio::try_join!(svc_svg, svc_png, svc_api, svc_avif)?; + + Ok(()) +} diff --git a/src/qr.rs b/src/qr.rs new file mode 100644 index 0000000..1687c15 --- /dev/null +++ b/src/qr.rs @@ -0,0 +1,190 @@ +use fast_qr::ECL; +use fast_qr::qr::QRBuilder; +use png::{ColorType, BitDepth, Compression, FilterType}; +use ravif::{RGBA8, Img, ColorModel}; +use std::io::Cursor; + +/// Maximum characters in a QR code (theoretical limit for Version 40 is 4,296 alphanumeric). +pub const MAX_CONTENT_LEN: usize = 4296; + +pub fn generate_svg(content: &str, ecc: ECL, _module_size: Option) -> Result { + if content.len() > MAX_CONTENT_LEN { + return Err(format!("Content too long (max {} characters)", MAX_CONTENT_LEN)); + } + + let qrcode = QRBuilder::new(content) + .ecl(ecc) + .build() + .map_err(|e| format!("Failed to generate QR: {:?}", e))?; + + // SVG is naturally scalable, we stick to the default vector output for speed. + let svg = fast_qr::convert::svg::SvgBuilder::default() + .to_str(&qrcode); + + Ok(svg) +} + +/// Highly optimized PNG generation: +/// - 4x manual bit-multiplication loop +/// - 1-bit Grayscale (binary) +/// - Fast compression, No filtering +pub fn generate_png(content: &str, ecc: ECL, _module_size: Option) -> Result, String> { + if content.len() > MAX_CONTENT_LEN { + return Err(format!("Content too long (max {} characters)", MAX_CONTENT_LEN)); + } + + let qrcode = QRBuilder::new(content) + .ecl(ecc) + .build() + .map_err(|e| format!("Failed to generate QR: {:?}", e))?; + + let qr_size = qrcode.size; + let padding = 4; // Standard QR quiet zone (4 modules) + let total_modules = qr_size + (padding * 2); + let img_size = total_modules * 4; + let row_bytes = (img_size + 7) / 8; + + // Initialize with 0xFF (white) + let mut buffer = vec![0xFFu8; row_bytes * img_size]; + + // Fast manual 4x scaling loop with padding offset + // Since PNG 1-bit Grayscale: 0 = black, 1 = white. + for y in 0..qr_size { + let row_offset = y * qr_size; + // The QR modules start at 'padding' modules from the top/left + let py_start = (y + padding) * 4; + for x in 0..qr_size { + if qrcode.data[row_offset + x].0 & 1 != 0 { + let px_start = (x + padding) * 4; + // Set 4x4 block to black (0) + for dy in 0..4 { + let py = py_start + dy; + let img_row_start = py * row_bytes; + for dx in 0..4 { + let px = px_start + dx; + let byte_idx = img_row_start + (px / 8); + let bit_mask = !(1 << (7 - (px % 8))); + buffer[byte_idx] &= bit_mask; + } + } + } + } + } + + let mut out = Vec::new(); + { + let mut encoder = png::Encoder::new(Cursor::new(&mut out), img_size as u32, img_size as u32); + encoder.set_color(ColorType::Grayscale); + encoder.set_depth(BitDepth::One); + encoder.set_compression(Compression::Fast); + encoder.set_filter(FilterType::NoFilter); + + let mut writer = encoder.write_header() + .map_err(|e| format!("Failed to write PNG header: {}", e))?; + writer.write_image_data(&buffer) + .map_err(|e| format!("Failed to write PNG data: {}", e))?; + } + + Ok(out) +} + +/// Optimized AVIF generation: +/// - Manual 4x pixel multiplication (RGBA8 for ravif compatibility) +/// - Lossless mode (Quality 100) +/// - Maximum encoding speed (Speed 10) +pub fn generate_avif(content: &str, ecc: ECL, _module_size: Option) -> Result, String> { + if content.len() > MAX_CONTENT_LEN { + return Err(format!("Content too long (max {} characters)", MAX_CONTENT_LEN)); + } + + let qrcode = QRBuilder::new(content) + .ecl(ecc) + .build() + .map_err(|e| format!("Failed to generate QR: {:?}", e))?; + + let qr_size = qrcode.size; + let padding = 4; // Standard quiet zone + let total_modules = qr_size + (padding * 2); + let img_size = total_modules * 4; + + // ravif takes RGBA8. We fill with white. + let mut pixels = vec![RGBA8::new(255, 255, 255, 255); img_size * img_size]; + + // Fast manual 4x scaling loop with padding offset + for y in 0..qr_size { + let qr_row_offset = y * qr_size; + let py_start = (y + padding) * 4; + for x in 0..qr_size { + if qrcode.data[qr_row_offset + x].0 & 1 != 0 { + let px_start = (x + padding) * 4; + let black = RGBA8::new(0, 0, 0, 255); + for dy in 0..4 { + let py = py_start + dy; + let img_row_offset = py * img_size; + for dx in 0..4 { + let px = px_start + dx; + pixels[img_row_offset + px] = black; + } + } + } + } + } + + let img = Img::new(&pixels[..], img_size, img_size); + + let encoded = ravif::Encoder::new() + .with_quality(100.0) + .with_speed(10) + .with_internal_color_model(ColorModel::RGB) + .encode_rgba(img) + .map_err(|e| format!("AVIF encoding failed: {}", e))?; + + Ok(encoded.avif_file) +} + +pub fn parse_ecl(ecl_str: &str) -> ECL { + match ecl_str.to_uppercase().as_str() { + "L" => ECL::L, + "M" => ECL::M, + "Q" => ECL::Q, + "H" => ECL::H, + _ => ECL::L, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_ecl() { + assert!(matches!(parse_ecl("L"), ECL::L)); + assert!(matches!(parse_ecl("m"), ECL::M)); + assert!(matches!(parse_ecl("q"), ECL::Q)); + assert!(matches!(parse_ecl("h"), ECL::H)); + assert!(matches!(parse_ecl("invalid"), ECL::L)); + } + + #[test] + fn test_generate_svg_returns_valid_svg() { + let svg = generate_svg("https://example.com", ECL::L, None).unwrap(); + assert!(svg.contains(""), "Output should contain closing SVG tag"); + } + + #[test] + fn test_generate_png_returns_valid_png() { + let png = generate_png("https://example.com", ECL::L, None).unwrap(); + // PNG files start with the magic bytes: 0x89 P N G + assert!(png.len() > 8, "PNG should have content"); + assert_eq!(&png[0..4], &[0x89, 0x50, 0x4E, 0x47], "Should have PNG magic bytes"); + } + + #[test] + fn test_generate_avif_returns_valid_avif() { + let avif = generate_avif("https://example.com", ECL::L, None).unwrap(); + assert!(avif.len() > 16, "AVIF should have content"); + // AVIF files usually have "ftypavif" + assert!(avif.windows(8).any(|w| w == b"ftypavif"), "Should have AVIF ftyp"); + } +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..20949a3 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,24 @@ +use std::collections::HashSet; +use std::sync::Arc; +use tokio::sync::RwLock; + +/// Shared application state accessible by all server instances. +#[derive(Clone)] +pub struct AppState { + /// Thread-safe, read-optimized set of allowed domains. + pub domains: Arc>>, + /// Path to the domains config file on disk (for persistence). + pub domains_path: String, + /// The default domain from the server config. + pub default_domain: String, +} + +impl AppState { + pub fn new(domains: HashSet, domains_path: String, default_domain: String) -> Self { + Self { + domains: Arc::new(RwLock::new(domains)), + domains_path, + default_domain, + } + } +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 0000000..1290090 --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,23 @@ +use std::collections::HashSet; +use server18004::state::AppState; + +/// Creates a test AppState with the given allowed domains. +/// Uses a temp directory for the domains file so tests don't interfere. +pub fn test_state(domains: &[&str]) -> AppState { + let domain_set: HashSet = domains.iter().map(|s| s.to_string()).collect(); + + // Use a temp file for domain persistence during tests + let tmp_dir = tempfile::tempdir().expect("Failed to create temp dir"); + let domains_path = tmp_dir + .path() + .join("domains.conf") + .to_str() + .unwrap() + .to_string(); + + // We need to leak the TempDir so it doesn't get cleaned up during the test + // (the test functions are short-lived, this is fine for testing) + std::mem::forget(tmp_dir); + + AppState::new(domain_set, domains_path, "test-default.com".to_string()) +} diff --git a/tests/common/test_health.http b/tests/common/test_health.http new file mode 100644 index 0000000..cff0aa2 --- /dev/null +++ b/tests/common/test_health.http @@ -0,0 +1,3 @@ +GET http://127.0.0.1:4083/domains HTTP/1.1 +content-type: application/json + diff --git a/tests/integration_test.rs b/tests/integration_test.rs new file mode 100644 index 0000000..120a0f3 --- /dev/null +++ b/tests/integration_test.rs @@ -0,0 +1,530 @@ +use axum::{ + body::Body, + http::{Request, StatusCode}, + routing::{get, post}, + Router, +}; +use http_body_util::BodyExt; +use serde_json::{json, Value}; +use tower::ServiceExt; // for oneshot + +mod common; +use common::test_state; + +// โ”€โ”€โ”€ Restricted SVG Server Tests โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +fn svg_app(state: server18004::state::AppState) -> Router { + Router::new() + .fallback(server18004::handlers::restricted::handle_svg) + .with_state(state) +} + +fn png_app(state: server18004::state::AppState) -> Router { + Router::new() + .fallback(server18004::handlers::restricted::handle_png) + .with_state(state) +} + +fn api_app(state: server18004::state::AppState) -> Router { + Router::new() + .route("/generate", post(server18004::handlers::api::generate_qr)) + .route("/domains/add", post(server18004::handlers::api::add_domain)) + .route("/domains/remove", post(server18004::handlers::api::remove_domain)) + .route("/domains", get(server18004::handlers::api::list_domains)) + .route("/health", get(server18004::handlers::api::health)) + .with_state(state) +} + +#[tokio::test] +async fn test_svg_allowed_domain() { + let state = test_state(&["example.com"]); + let app = svg_app(state); + + let response = app + .oneshot( + Request::builder() + .uri("/some/path") + .header("host", "qr.example.com") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response.headers().get("content-type").unwrap(), + "image/svg+xml" + ); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let svg = String::from_utf8(body.to_vec()).unwrap(); + assert!(svg.contains(" 8, "PNG should have content"); + assert_eq!(&body[0..4], &[0x89, 0x50, 0x4E, 0x47], "Should have PNG magic bytes"); +} + +#[tokio::test] +async fn test_png_forbidden_domain() { + let state = test_state(&["example.com"]); + let app = png_app(state); + + let response = app + .oneshot( + Request::builder() + .uri("/test") + .header("host", "qr.evil.com") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::FORBIDDEN); +} + +// โ”€โ”€โ”€ API Server Tests โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +#[tokio::test] +async fn test_health_endpoint() { + let state = test_state(&[]); + let app = api_app(state); + + let response = app + .oneshot( + Request::builder() + .uri("/health") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); +} + +#[tokio::test] +async fn test_generate_svg() { + let state = test_state(&[]); + let app = api_app(state); + + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/generate") + .header("content-type", "application/json") + .body(Body::from( + serde_json::to_string(&json!({ + "text": "https://example.com", + "format": "svg" + })) + .unwrap(), + )) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response.headers().get("content-type").unwrap(), + "image/svg+xml" + ); +} + +#[tokio::test] +async fn test_generate_png() { + let state = test_state(&[]); + let app = api_app(state); + + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/generate") + .header("content-type", "application/json") + .body(Body::from( + serde_json::to_string(&json!({ + "text": "https://example.com", + "format": "png", + "ecl": "H" + })) + .unwrap(), + )) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response.headers().get("content-type").unwrap(), + "image/png" + ); +} + +#[tokio::test] +async fn test_generate_base64() { + let state = test_state(&[]); + let app = api_app(state); + + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/generate") + .header("content-type", "application/json") + .body(Body::from( + serde_json::to_string(&json!({ + "text": "https://example.com", + "format": "base64", + "base64_source": "png" + })) + .unwrap(), + )) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let json: Value = serde_json::from_slice(&body).unwrap(); + assert_eq!(json["format"], "base64"); + assert_eq!(json["source"], "png"); + assert!(!json["data"].as_str().unwrap().is_empty()); +} + +#[tokio::test] +async fn test_generate_base64url_svg_source() { + let state = test_state(&[]); + let app = api_app(state); + + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/generate") + .header("content-type", "application/json") + .body(Body::from( + serde_json::to_string(&json!({ + "text": "hello world", + "format": "base64url", + "base64_source": "svg" + })) + .unwrap(), + )) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let json: Value = serde_json::from_slice(&body).unwrap(); + assert_eq!(json["format"], "base64url"); + assert_eq!(json["source"], "svg"); + // base64url should not contain + or / or = + let data = json["data"].as_str().unwrap(); + assert!(!data.contains('+'), "base64url should not contain +"); + assert!(!data.contains('/'), "base64url should not contain /"); +} + +#[tokio::test] +async fn test_generate_invalid_format() { + let state = test_state(&[]); + let app = api_app(state); + + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/generate") + .header("content-type", "application/json") + .body(Body::from( + serde_json::to_string(&json!({ + "text": "test", + "format": "bmp" + })) + .unwrap(), + )) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::BAD_REQUEST); +} + +// โ”€โ”€โ”€ Domain Management Tests โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +#[tokio::test] +async fn test_list_domains_empty() { + let state = test_state(&[]); + let app = api_app(state); + + let response = app + .oneshot( + Request::builder() + .uri("/domains") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let json: Value = serde_json::from_slice(&body).unwrap(); + assert!(json["success"].as_bool().unwrap()); + assert!(json["domains"].as_array().unwrap().is_empty()); +} + +#[tokio::test] +async fn test_add_domain() { + let state = test_state(&[]); + let app = api_app(state); + + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/domains/add") + .header("content-type", "application/json") + .body(Body::from( + serde_json::to_string(&json!({ + "domain": "newdomain.com" + })) + .unwrap(), + )) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let json: Value = serde_json::from_slice(&body).unwrap(); + assert!(json["success"].as_bool().unwrap()); + assert!(json["domains"] + .as_array() + .unwrap() + .iter() + .any(|d| d == "newdomain.com")); +} + +#[tokio::test] +async fn test_add_empty_domain() { + let state = test_state(&[]); + let app = api_app(state); + + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/domains/add") + .header("content-type", "application/json") + .body(Body::from( + serde_json::to_string(&json!({ + "domain": " " + })) + .unwrap(), + )) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::BAD_REQUEST); +} + +#[tokio::test] +async fn test_remove_domain() { + let state = test_state(&["example.com", "test.com"]); + let app = api_app(state); + + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/domains/remove") + .header("content-type", "application/json") + .body(Body::from( + serde_json::to_string(&json!({ + "domain": "example.com" + })) + .unwrap(), + )) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let json: Value = serde_json::from_slice(&body).unwrap(); + assert!(json["success"].as_bool().unwrap()); + assert!(!json["domains"] + .as_array() + .unwrap() + .iter() + .any(|d| d == "example.com")); +} + +#[tokio::test] +async fn test_cache_control_headers() { + let state = test_state(&["example.com"]); + let app = svg_app(state); + + let response = app + .oneshot( + Request::builder() + .uri("/test") + .header("host", "qr.example.com") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response.headers().get("cache-control").unwrap(), + "public, max-age=86400" + ); +} + +#[tokio::test] +async fn test_x_forwarded_proto() { + let state = test_state(&["example.com"]); + let app = svg_app(state); + + let response = app + .oneshot( + Request::builder() + .uri("/mypage?q=1") + .header("host", "qr.example.com") + .header("x-forwarded-proto", "http") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + // The QR code should encode http://example.com/mypage?q=1 + let body = response.into_body().collect().await.unwrap().to_bytes(); + let svg = String::from_utf8(body.to_vec()).unwrap(); + assert!(svg.contains("