From 94eb1167f73ebc50ca253df1ab591e69341c911f Mon Sep 17 00:00:00 2001 From: Benji Grant Date: Sat, 22 Jul 2023 14:06:12 +1000 Subject: [PATCH 01/19] Add a scrape_link_tags fn --- Cargo.lock | 1855 ++++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 7 + src/get_favicon.rs | 81 ++ src/main.rs | 26 +- 4 files changed, 1878 insertions(+), 91 deletions(-) create mode 100644 src/get_favicon.rs diff --git a/Cargo.lock b/Cargo.lock index 5140d6c..caf036d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "anstream" version = "0.3.2" @@ -51,18 +66,99 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" + +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" +[[package]] +name = "bumpalo" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" + +[[package]] +name = "bytemuck" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + [[package]] name = "cc" version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "clap" version = "4.3.15" @@ -104,12 +200,113 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +[[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.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "data-url" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d7439c3735f405729d52c3fbbe4de140eaf938a1fe47d227c27f8254d4302a5" + +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + +[[package]] +name = "encoding_rs" +version = "0.8.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +dependencies = [ + "cfg-if", +] + [[package]] name = "errno" version = "0.3.1" @@ -131,176 +328,1664 @@ dependencies = [ "libc", ] +[[package]] +name = "exr" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1e481eb11a482815d3e9d618db8c42a93207134662873809335a92327440c18" +dependencies = [ + "bit_field", + "flume", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + [[package]] name = "favicon-rover" version = "0.1.0" dependencies = [ "clap", + "image", + "reqwest", + "resvg", + "thiserror", + "tl", + "tokio", + "url", ] [[package]] -name = "heck" -version = "0.4.1" +name = "fdeflate" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10" +dependencies = [ + "simd-adler32", +] [[package]] -name = "hermit-abi" -version = "0.3.2" +name = "flate2" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +dependencies = [ + "crc32fast", + "miniz_oxide", +] [[package]] -name = "is-terminal" -version = "0.4.9" +name = "float-cmp" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" + +[[package]] +name = "flume" +version = "0.10.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" dependencies = [ - "hermit-abi", - "rustix", - "windows-sys", + "futures-core", + "futures-sink", + "nanorand", + "pin-project", + "spin", ] [[package]] -name = "libc" -version = "0.2.147" +name = "fnv" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] -name = "linux-raw-sys" -version = "0.4.3" +name = "fontconfig-parser" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" +checksum = "4ab2e12762761366dcb876ab8b6e0cfa4797ddcd890575919f008b5ba655672a" +dependencies = [ + "roxmltree", +] [[package]] -name = "once_cell" -version = "1.18.0" +name = "fontdb" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "af8d8cbea8f21307d7e84bca254772981296f058a1d36b461bf4d83a7499fc9e" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser 0.19.1", +] [[package]] -name = "proc-macro2" -version = "1.0.66" +name = "foreign-types" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ - "unicode-ident", + "foreign-types-shared", ] [[package]] -name = "quote" -version = "1.0.31" +name = "foreign-types-shared" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" dependencies = [ - "proc-macro2", + "percent-encoding", ] [[package]] -name = "rustix" -version = "0.38.4" +name = "futures-channel" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" dependencies = [ - "bitflags", - "errno", + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "js-sys", "libc", - "linux-raw-sys", - "windows-sys", + "wasi", + "wasm-bindgen", ] [[package]] -name = "strsim" -version = "0.10.0" +name = "gif" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045" +dependencies = [ + "color_quant", + "weezl", +] [[package]] -name = "syn" -version = "2.0.26" +name = "gimli" +version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45c3457aacde3c65315de5031ec191ce46604304d2446e803d71ade03308d970" +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" + +[[package]] +name = "h2" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", ] [[package]] -name = "unicode-ident" -version = "1.0.11" +name = "half" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0" +dependencies = [ + "crunchy", +] [[package]] -name = "utf8parse" -version = "0.2.1" +name = "hashbrown" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] -name = "windows-sys" -version = "0.48.0" +name = "heck" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" dependencies = [ - "windows-targets", + "libc", ] [[package]] -name = "windows-targets" -version = "0.48.1" +name = "hermit-abi" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "bytes", + "fnv", + "itoa", ] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.0" +name = "http-body" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] [[package]] -name = "windows_aarch64_msvc" -version = "0.48.0" +name = "httparse" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] -name = "windows_i686_gnu" -version = "0.48.0" +name = "httpdate" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" [[package]] -name = "windows_i686_msvc" -version = "0.48.0" +name = "hyper" +version = "0.14.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] [[package]] -name = "windows_x86_64_gnu" -version = "0.48.0" +name = "hyper-tls" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] [[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.0" +name = "idna" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.48.0" +name = "image" +version = "0.24.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527909aa81e20ac3a44803521443a765550f09b5130c2c2fa1ea59c2f8f50a3a" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif", + "jpeg-decoder", + "num-rational", + "num-traits", + "png", + "qoi", + "tiff", +] + +[[package]] +name = "imagesize" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.2", + "libc", + "windows-sys", +] + +[[package]] +name = "ipnet" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" + +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi 0.3.2", + "rustix 0.38.4", + "windows-sys", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "jpeg-decoder" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" +dependencies = [ + "rayon", +] + +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "kurbo" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd85a5776cd9500c2e2059c8c76c3b01528566b7fcbaf8098b55a33fc298849b" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "linux-raw-sys" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" + +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memmap2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d28bba84adfe6646737845bc5ebbfa2c08424eb1c37e94a1fd2a82adb56a872" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[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.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi 0.2.6", + "libc", +] + +[[package]] +name = "object" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "openssl" +version = "0.10.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "pin-project" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "030ad2bc4db10a8944cb0d837f158bdfec4d4a4873ab701a95046770d11f8842" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "png" +version = "0.17.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59871cc5b6cce7eaccca5a802b4173377a1c2ba90654246789a8fa2334426d11" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "proc-macro2" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quote" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rayon" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + +[[package]] +name = "rctree" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b42e27ef78c35d3998403c1d26f3efd9e135d3e5121b0a4845cc5cc27547f4f" + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "reqwest" +version = "0.11.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "resvg" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6554f47c38eca56827eea7f285c2a3018b4e12e0e195cc105833c008be338f1" +dependencies = [ + "gif", + "jpeg-decoder", + "log", + "pico-args", + "png", + "rgb", + "svgtypes", + "tiny-skia", + "usvg", +] + +[[package]] +name = "rgb" +version = "0.8.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20ec2d3e3fc7a92ced357df9cebd5a10b6fb2aa1ee797bf7e9ce2f17dffc8f59" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "roxmltree" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8f595a457b6b8c6cda66a48503e92ee8d19342f905948f29c383200ec9eb1d8" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustix" +version = "0.37.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys", +] + +[[package]] +name = "rustix" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" +dependencies = [ + "bitflags 2.3.3", + "errno", + "libc", + "linux-raw-sys 0.4.3", + "windows-sys", +] + +[[package]] +name = "rustybuzz" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162bdf42e261bee271b3957691018634488084ef577dddeb6420a9684cab2a6a" +dependencies = [ + "bitflags 1.3.2", + "bytemuck", + "smallvec", + "ttf-parser 0.18.1", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-general-category", + "unicode-script", +] + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "security-framework" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.171" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9" + +[[package]] +name = "serde_json" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d03b412469450d4404fe8499a268edd7f8b79fecb074b0d812ad64ca21f4031b" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[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 = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "238abfbb77c1915110ad968465608b68e869e0772622c9656714e73e5a1a522f" + +[[package]] +name = "simplecss" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a11be7c62927d9427e9f40f3444d5499d868648e2edbc4e2116de69e7ec0e89d" +dependencies = [ + "log", +] + +[[package]] +name = "siphasher" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" + +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] + +[[package]] +name = "slotmap" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "socket2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[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.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "svgtypes" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed4b0611e7f3277f68c0fa18e385d9e2d26923691379690039548f867cef02a7" +dependencies = [ + "kurbo", + "siphasher", +] + +[[package]] +name = "syn" +version = "2.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45c3457aacde3c65315de5031ec191ce46604304d2446e803d71ade03308d970" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" +dependencies = [ + "autocfg", + "cfg-if", + "fastrand", + "redox_syscall", + "rustix 0.37.23", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tiff" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7449334f9ff2baf290d55d73983a7d6fa15e01198faef72af07e2a8db851e471" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + +[[package]] +name = "tiny-skia" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7db11798945fa5c3e5490c794ccca7c6de86d3afdd54b4eb324109939c6f37bc" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "png", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f60aa35c89ac2687ace1a2556eaaea68e8c0d47408a2e3e7f5c98a489e7281c" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +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 = "tl" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5e993a1c7c32fdf90a308cec4d457f507b2573acc909bd6e7a092321664fdb3" + +[[package]] +name = "tokio" +version = "1.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +dependencies = [ + "autocfg", + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + +[[package]] +name = "ttf-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0609f771ad9c6155384897e1df4d948e692667cc0588548b68eb44d052b27633" + +[[package]] +name = "ttf-parser" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a464a4b34948a5f67fddd2b823c62d9d92e44be75058b99939eae6c5b6960b33" + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d12260fb92d52f9008be7e4bca09f584780eb2266dc8fecc6a192bec561694" + +[[package]] +name = "unicode-ccc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2520efa644f8268dce4dcd3050eaa7fc044fca03961e9998ac7e2e92b77cf1" + +[[package]] +name = "unicode-general-category" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2281c8c1d221438e373249e065ca4989c4c36952c211ff21a0ee91c44a3869e7" + +[[package]] +name = "unicode-ident" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-script" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d817255e1bed6dfd4ca47258685d14d2bdcfbc64fdc9e3819bd5848057b8ecc" + +[[package]] +name = "unicode-vo" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" + +[[package]] +name = "url" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "usvg" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14d09ddfb0d93bf84824c09336d32e42f80961a9d1680832eb24fdf249ce11e6" +dependencies = [ + "base64", + "log", + "pico-args", + "usvg-parser", + "usvg-text-layout", + "usvg-tree", + "xmlwriter", +] + +[[package]] +name = "usvg-parser" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d19bf93d230813599927d88557014e0908ecc3531666d47c634c6838bc8db408" +dependencies = [ + "data-url", + "flate2", + "imagesize", + "kurbo", + "log", + "roxmltree", + "simplecss", + "siphasher", + "svgtypes", + "usvg-tree", +] + +[[package]] +name = "usvg-text-layout" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "035044604e89652c0a2959b8b356946997a52649ba6cade45928c2842376feb4" +dependencies = [ + "fontdb", + "kurbo", + "log", + "rustybuzz", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "usvg-tree", +] + +[[package]] +name = "usvg-tree" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7939a7e4ed21cadb5d311d6339730681c3e24c3e81d60065be80e485d3fc8b92" +dependencies = [ + "rctree", + "strict-num", + "svgtypes", + "tiny-skia-path", +] + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[package]] +name = "web-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "weezl" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + +[[package]] +name = "xmlparser" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d25c75bf9ea12c4040a97f829154768bbbce366287e2dc044af160cd79a13fd" + +[[package]] +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] diff --git a/Cargo.toml b/Cargo.toml index b8df553..805166f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,3 +12,10 @@ authors = ["Benji Grant", "Ewan Breakey"] [dependencies] clap = { version = "4.3.15", features = ["derive"] } +image = "0.24.6" +reqwest = "0.11.18" +resvg = "0.35.0" +thiserror = "1.0.43" +tl = "0.7.7" +tokio = { version = "1.29.1", features = ["full"] } +url = "2.4.0" diff --git a/src/get_favicon.rs b/src/get_favicon.rs new file mode 100644 index 0000000..55ffd58 --- /dev/null +++ b/src/get_favicon.rs @@ -0,0 +1,81 @@ +use thiserror::Error; +use url::Url; + +struct Favicon { + image: bool, + did_fallback: bool, +} + +struct Link { + href: String, + size: Option, +} + +#[derive(Error, Debug)] +enum ScrapeError { + #[error(transparent)] + Reqwest(#[from] reqwest::Error), + + #[error(transparent)] + HTMLParse(#[from] tl::ParseError), + + #[error(transparent)] + URLParse(#[from] url::ParseError), + + #[error("link not found")] + LinkNotFound, +} + +async fn scrape_link_tags(url: Url) -> Result { + let res = reqwest::get(url).await?; + let html = res.text().await?; + + let dom = tl::parse(&html, tl::ParserOptions::default())?; + let parser = dom.parser(); + let mut links: Vec<_> = dom + .query_selector("link[rel*=\"icon\"]") + .unwrap() + .map(|link| link.get(parser).unwrap().as_tag().unwrap().attributes()) + .filter_map(|attr| match attr.get("href").flatten() { + Some(href) => { + if let Some(media) = attr.get("media").flatten() { + if String::from(media.as_utf8_str()) + .replace(' ', "") + .to_ascii_lowercase() + == *"prefers-color-scheme:dark" + { + return None; + } + } + Some(Link { + href: href.as_utf8_str().into_owned(), + size: attr.get("sizes").flatten().and_then(|sizes| { + sizes + .as_utf8_str() + .split_once('x') + .and_then(|(size, _)| size.parse().ok()) + }), + }) + } + None => None, + }) + .collect(); + + if links.is_empty() { + return Err(ScrapeError::LinkNotFound); + } + + links.sort_unstable_by_key(|link| link.size); + + Ok(Url::parse(&links.get(0).unwrap().href)?) +} + +async fn get_favicon(url: Url, size: Option) -> Result { + let image_url = scrape_link_tags(url) + .await + .unwrap_or_else(|_| url.join("/favicon.ico").unwrap()); + + // Fetch the image + + () +} diff --git a/src/main.rs b/src/main.rs index 1c3a6cc..efd131f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,17 @@ -use clap::{Parser, Subcommand}; +use clap::{Parser, Subcommand, ValueEnum}; + +mod get_favicon; + +#[derive(Clone, ValueEnum, Debug)] +enum ImageKind { + Png, + Jpg, + Webp, + Bmp, + Ico, + Gif, + Tiff, +} #[derive(Parser, Debug)] #[command( @@ -13,15 +26,15 @@ struct Cli { /// Square pixel size of the favicon #[arg(short, long)] - size: Option, + size: Option, /// Path to save favicon to if not using stdout #[arg(short, long)] out: Option, /// Image type to save favicon (overrides file extension if provided) - #[arg(short, long, default_value_t = String::from("webp"))] - r#type: String, + #[arg(value_enum, short, long, default_value_t = ImageKind::Webp)] + r#type: ImageKind, #[command(subcommand)] command: Option, @@ -37,7 +50,7 @@ enum Commands { /// Port to use for http server #[arg(short, long, default_value_t = 3000)] - port: i16, + port: u16, /// URL or regex allowed by CORS #[arg(short, long, default_values_t = [String::from("*")])] @@ -45,7 +58,8 @@ enum Commands { }, } -fn main() { +#[tokio::main] +async fn main() { let cli = Cli::parse(); dbg!(cli); From f954063ff0a336e6cee37e9d1cba3670eb983216 Mon Sep 17 00:00:00 2001 From: Ewan Breakey Date: Sat, 22 Jul 2023 15:51:22 +1000 Subject: [PATCH 02/19] Continue working on fetching favicons --- Cargo.lock | 66 +++++++++++++++++++++++++++++ Cargo.toml | 3 +- README.md | 2 +- src/cli.rs | 55 ++++++++++++++++++++++++ src/get_favicon.rs | 103 ++++++++++++++++++++++++++++++++++++++------- src/main.rs | 76 ++++++++------------------------- 6 files changed, 228 insertions(+), 77 deletions(-) create mode 100644 src/cli.rs diff --git a/Cargo.lock b/Cargo.lock index caf036d..ff36894 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -361,6 +361,7 @@ dependencies = [ "image", "reqwest", "resvg", + "strum", "thiserror", "tl", "tokio", @@ -473,6 +474,23 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[package]] +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures-sink" version = "0.3.28" @@ -492,9 +510,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ "futures-core", + "futures-io", + "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", + "slab", ] [[package]] @@ -1169,10 +1192,12 @@ dependencies = [ "serde_urlencoded", "tokio", "tokio-native-tls", + "tokio-util", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", "winreg", ] @@ -1245,6 +1270,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + [[package]] name = "rustybuzz" version = "0.7.0" @@ -1422,6 +1453,28 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6069ca09d878a33f883cc06aaa9718ede171841d3832450354410b718b097232" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "svgtypes" version = "0.11.0" @@ -1856,6 +1909,19 @@ version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +[[package]] +name = "wasm-streams" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bbae3363c08332cadccd13b67db371814cd214c2524020932f0804b8cf7c078" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.64" diff --git a/Cargo.toml b/Cargo.toml index 805166f..7c2b3a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,8 +13,9 @@ authors = ["Benji Grant", "Ewan Breakey"] [dependencies] clap = { version = "4.3.15", features = ["derive"] } image = "0.24.6" -reqwest = "0.11.18" +reqwest = { version = "0.11.18", features = ["stream"] } resvg = "0.35.0" +strum = { version = "0.25.0", features = ["derive"] } thiserror = "1.0.43" tl = "0.7.7" tokio = { version = "1.29.1", features = ["full"] } diff --git a/README.md b/README.md index 4f0d611..0eee743 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ favicon-rover serve --help # show help information ### CORS -By default, any origin is allowed to request from this API. To lock it down, use the `--origin` command line options to specify any amount of origins. If an origin starts and ends with `/` it will be treated as a regexp. For example `favicon-rover serve -o http://example1.com -o /\.example2\.com$/` will accept any request from "http://example1.com" or from a subdomain of "example2.com". +By default, any origin is allowed to make a request to this API. To lock it down, use the `--origin` command line options to specify any amount of origins. If an origin starts and ends with `/` it will be treated as a regexp. For example `favicon-rover serve -o http://example1.com -o /\.example2\.com$/` will accept any request from "http://example1.com" or from a subdomain of "example2.com". ## Development diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..72d70eb --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,55 @@ +use clap::{Parser, Subcommand, ValueEnum}; + +#[derive(Clone, ValueEnum, Debug)] +pub enum ImageFormatOutput { + Png, + Jpeg, + Webp, + Bmp, + Ico, + Gif, + Tiff, +} + +#[derive(Parser, Debug)] +#[command(author, version, about)] +pub struct Cli { + #[command(subcommand)] + pub command: Option, +} + +#[derive(Subcommand, Debug)] +pub enum Command { + /// Fetch the favicon for a specified url + Get { + /// Host to fetch the favicon for + url: Option, + + /// Square pixel size of the favicon + #[arg(short, long)] + size: Option, + + /// Path to save favicon to if not using stdout + #[arg(short, long)] + out: Option, + + /// Image format to save favicon as (overrides file extension if provided) + #[arg(value_enum, short, long, default_value_t = ImageFormatOutput::Webp)] + format: ImageFormatOutput, + }, + + /// Start a favicon scout web server + Serve { + /// Host to use for http server + #[arg(long, default_value_t = String::from("localhost"), value_name = "URL")] + host: String, + + /// Port to use for http server + #[arg(short, long, default_value_t = 3000)] + port: u16, + + /// URL or regex allowed by CORS + #[arg(short, long, default_values_t = [String::from("*")])] + origin: Vec, + }, +} diff --git a/src/get_favicon.rs b/src/get_favicon.rs index 55ffd58..f76af0e 100644 --- a/src/get_favicon.rs +++ b/src/get_favicon.rs @@ -1,20 +1,36 @@ +use reqwest::header::{HeaderValue, CONTENT_TYPE}; +use std::io; use thiserror::Error; use url::Url; -struct Favicon { - image: bool, - did_fallback: bool, +#[derive(Debug)] +pub enum Favicon { + Image(image::DynamicImage), + Fallback(image::DynamicImage, GetFaviconError), } +#[derive(Debug)] struct Link { href: String, size: Option, } +#[derive(Error, Debug)] +pub enum GetFaviconError { + #[error(transparent)] + Scrape(#[from] ScrapeError), + + #[error(transparent)] + Network(#[from] reqwest::Error), + + #[error("Failed to decode image: {0}")] + ImageError(#[from] image::ImageError), +} + #[derive(Error, Debug)] enum ScrapeError { #[error(transparent)] - Reqwest(#[from] reqwest::Error), + Network(#[from] reqwest::Error), #[error(transparent)] HTMLParse(#[from] tl::ParseError), @@ -26,8 +42,73 @@ enum ScrapeError { LinkNotFound, } -async fn scrape_link_tags(url: Url) -> Result { - let res = reqwest::get(url).await?; +pub async fn get_favicon(target_url: &Url) -> Favicon { + match fetch_favicon(target_url).await { + Ok(image) => image, + Err(error) => Favicon::Fallback(generate_fallback(target_url), error), + } +} + +async fn generate_fallback(target_url: &Url) -> image::DynamicImage { + todo!() +} + +/// Fetch the favicon for a given url +pub async fn fetch_favicon(target_url: &Url) -> Result { + // Determine favicon url + let image_url = scrape_link_tags(target_url) + .await + .unwrap_or_else(|_| target_url.join("/favicon.ico").unwrap()); + + // Fetch the image + let res = reqwest::get(image_url).await?; + + // Get HTTP response body + let body = res.bytes().await?; + let cursor = io::Cursor::new(body); + + // Create reader and attempt to guess image format + let image_reader = image::io::Reader::new(cursor) + .with_guessed_format() + .expect("Cursor IO shouldn't fail"); + + // Decode the image! + // TODO: this is blocking, should it be in a tokio blocking_task? + let image = image_reader.decode()?; + + Ok(Favicon::Image(image)) +} + +enum ImageFormat { + Png, + Jpeg, + Webp, + Bmp, + Ico, + Gif, + Tiff, + Svg, +} + +impl ImageFormat { + // pub fn from_content_type(content_type: &HeaderValue) -> Option { + // match content_type.to_str().ok()? { + // "image/png" => Some(Self::Png), + // "image/jpeg" => Some(Self::Jpeg), + // "image/webp" => Some(Self::Webp), + // "image/bmp" => Some(Self::Bmp), + // "image/gif" => Some(Self::Gif), + // "image/tiff" => Some(Self::Tiff), + // "image/svg+xml" => Some(Self::Svg), + // "image/vnd.microsoft.icon" | "image/x-icon" => Some(Self::Ico), + // _ => None, + // } + // } +} + +/// Scrape the tags from a given URL to find a favicon url +async fn scrape_link_tags(url: &Url) -> Result { + let res = reqwest::get(url.clone()).await?; let html = res.text().await?; let dom = tl::parse(&html, tl::ParserOptions::default())?; @@ -69,13 +150,3 @@ async fn scrape_link_tags(url: Url) -> Result { Ok(Url::parse(&links.get(0).unwrap().href)?) } - -async fn get_favicon(url: Url, size: Option) -> Result { - let image_url = scrape_link_tags(url) - .await - .unwrap_or_else(|_| url.join("/favicon.ico").unwrap()); - - // Fetch the image - - () -} diff --git a/src/main.rs b/src/main.rs index efd131f..e69cbf0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,66 +1,24 @@ -use clap::{Parser, Subcommand, ValueEnum}; - +mod cli; mod get_favicon; -#[derive(Clone, ValueEnum, Debug)] -enum ImageKind { - Png, - Jpg, - Webp, - Bmp, - Ico, - Gif, - Tiff, -} - -#[derive(Parser, Debug)] -#[command( - author, - version, - about, - long_about = "Fetch the favicon from a specified url" -)] -struct Cli { - /// Host to fetch the favicon for - url: Option, - - /// Square pixel size of the favicon - #[arg(short, long)] - size: Option, - - /// Path to save favicon to if not using stdout - #[arg(short, long)] - out: Option, - - /// Image type to save favicon (overrides file extension if provided) - #[arg(value_enum, short, long, default_value_t = ImageKind::Webp)] - r#type: ImageKind, - - #[command(subcommand)] - command: Option, -} - -#[derive(Subcommand, Debug)] -enum Commands { - /// Start a favicon scout web server - Serve { - /// Host to use for http server - #[arg(long, default_value_t = String::from("localhost"), value_name = "URL")] - host: String, - - /// Port to use for http server - #[arg(short, long, default_value_t = 3000)] - port: u16, - - /// URL or regex allowed by CORS - #[arg(short, long, default_values_t = [String::from("*")])] - origin: Vec, - }, -} +use clap::Parser; +use cli::{Cli, Command}; #[tokio::main] async fn main() { let cli = Cli::parse(); - - dbg!(cli); + match cli.command { + Some(Command::Get { + url, + out, + size, + format, + }) => { + let favicon = get_favicon(&url); + } + Some(serve @ Command::Serve { .. }) => { + // TODO + } + None => {} + } } From 4287f307057ff563f4190db444c4444f8c4f30f6 Mon Sep 17 00:00:00 2001 From: Ewan Breakey Date: Sat, 22 Jul 2023 15:54:00 +1000 Subject: [PATCH 03/19] Correctly call .await on fallback --- src/get_favicon.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/get_favicon.rs b/src/get_favicon.rs index f76af0e..ffb619b 100644 --- a/src/get_favicon.rs +++ b/src/get_favicon.rs @@ -45,7 +45,7 @@ enum ScrapeError { pub async fn get_favicon(target_url: &Url) -> Favicon { match fetch_favicon(target_url).await { Ok(image) => image, - Err(error) => Favicon::Fallback(generate_fallback(target_url), error), + Err(error) => Favicon::Fallback(generate_fallback(target_url).await, error), } } From ee8b5919a51208174cf6d901897b14918d6cccd5 Mon Sep 17 00:00:00 2001 From: Ewan Breakey Date: Sat, 22 Jul 2023 15:54:04 +1000 Subject: [PATCH 04/19] Correctly call .await on fallback --- src/cli.rs | 3 ++- src/main.rs | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 72d70eb..ab7127b 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,4 +1,5 @@ use clap::{Parser, Subcommand, ValueEnum}; +use url::Url; #[derive(Clone, ValueEnum, Debug)] pub enum ImageFormatOutput { @@ -23,7 +24,7 @@ pub enum Command { /// Fetch the favicon for a specified url Get { /// Host to fetch the favicon for - url: Option, + url: Url, /// Square pixel size of the favicon #[arg(short, long)] diff --git a/src/main.rs b/src/main.rs index e69cbf0..5dd0eae 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,8 @@ mod get_favicon; use clap::Parser; use cli::{Cli, Command}; +use get_favicon::get_favicon; +use url::Url; #[tokio::main] async fn main() { @@ -14,7 +16,8 @@ async fn main() { size, format, }) => { - let favicon = get_favicon(&url); + let favicon = get_favicon(&url).await; + dbg!(favicon); } Some(serve @ Command::Serve { .. }) => { // TODO From 681f9018b7e153cab06d06c518ddf12a8cd6e1d6 Mon Sep 17 00:00:00 2001 From: Ewan Breakey Date: Sat, 22 Jul 2023 17:15:40 +1000 Subject: [PATCH 05/19] Wrap image structs in custom Image struct --- src/cli.rs | 10 ++++--- src/get_favicon.rs | 74 +++++++++++++++++++++++++++++++++++++++------- src/main.rs | 39 +++++++++++++++++++++--- 3 files changed, 104 insertions(+), 19 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index ab7127b..2341b6b 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use clap::{Parser, Subcommand, ValueEnum}; use url::Url; @@ -28,15 +30,15 @@ pub enum Command { /// Square pixel size of the favicon #[arg(short, long)] - size: Option, + size: Option, /// Path to save favicon to if not using stdout #[arg(short, long)] - out: Option, + out: Option, /// Image format to save favicon as (overrides file extension if provided) - #[arg(value_enum, short, long, default_value_t = ImageFormatOutput::Webp)] - format: ImageFormatOutput, + #[arg(value_enum, short, long)] + format: Option, }, /// Start a favicon scout web server diff --git a/src/get_favicon.rs b/src/get_favicon.rs index ffb619b..a9f2e4a 100644 --- a/src/get_favicon.rs +++ b/src/get_favicon.rs @@ -1,15 +1,49 @@ -use reqwest::header::{HeaderValue, CONTENT_TYPE}; +use image::imageops::FilterType; use std::io; use thiserror::Error; use url::Url; +const DEFAULT_IMAGE_SIZE: u32 = 256; + #[derive(Debug)] -pub enum Favicon { - Image(image::DynamicImage), - Fallback(image::DynamicImage, GetFaviconError), +struct Image { + pub data: image::DynamicImage, + pub format: Option, } #[derive(Debug)] +pub enum Favicon { + Image(Image), + Fallback(Image, GetFaviconError), +} + +impl Favicon { + pub fn image(&self) -> &Image { + match self { + Favicon::Image(image) => &image, + Favicon::Fallback(image, _) => &image, + } + } + + fn set_image_data(&mut self, data: image::DynamicImage) { + match self { + Self::Image(ref mut img) => { + (*img).data = data; + } + Self::Fallback(ref mut img, _) => { + (*img).data = data; + } + } + } + + pub fn resize(&mut self, size: u32) { + let image = self.image(); + let image = image.data.resize_to_fill(size, size, FilterType::Lanczos3); + self.set_image_data(image); + } +} + +#[derive(Debug, Clone)] struct Link { href: String, size: Option, @@ -28,7 +62,7 @@ pub enum GetFaviconError { } #[derive(Error, Debug)] -enum ScrapeError { +pub enum ScrapeError { #[error(transparent)] Network(#[from] reqwest::Error), @@ -42,14 +76,28 @@ enum ScrapeError { LinkNotFound, } -pub async fn get_favicon(target_url: &Url) -> Favicon { +pub async fn get_favicon(target_url: &Url, size: Option) -> Favicon { match fetch_favicon(target_url).await { - Ok(image) => image, - Err(error) => Favicon::Fallback(generate_fallback(target_url).await, error), + // We have an image from the target, resize if applicable and return + Ok(mut image) => { + if let Some(size) = size { + image.resize(size); + } + image + } + + // We didn't get an image, generate one + Err(error) => Favicon::Fallback( + Image { + data: generate_fallback(target_url, size.unwrap_or(DEFAULT_IMAGE_SIZE)).await, + format: None, + }, + error, + ), } } -async fn generate_fallback(target_url: &Url) -> image::DynamicImage { +async fn generate_fallback(target_url: &Url, size: u32) -> image::DynamicImage { todo!() } @@ -74,9 +122,13 @@ pub async fn fetch_favicon(target_url: &Url) -> Result // Decode the image! // TODO: this is blocking, should it be in a tokio blocking_task? - let image = image_reader.decode()?; + let image_format = image_reader.format(); // TODO: this being none might need to be an error + let image_data = image_reader.decode()?; - Ok(Favicon::Image(image)) + Ok(Favicon::Image(Image { + data: image_data, + format: image_format, + })) } enum ImageFormat { diff --git a/src/main.rs b/src/main.rs index 5dd0eae..e2a630b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,11 @@ mod cli; mod get_favicon; +use std::io::{self, BufWriter, Write}; + use clap::Parser; use cli::{Cli, Command}; use get_favicon::get_favicon; -use url::Url; #[tokio::main] async fn main() { @@ -16,10 +17,40 @@ async fn main() { size, format, }) => { - let favicon = get_favicon(&url).await; - dbg!(favicon); + // Get favicon (may be a fallback) + let favicon = get_favicon(&url, size).await; + + // Can we guess the format from the "out" path? + let format = format + .map( + |f| image::ImageFormat::Png, /* TODO: convert f from cli::format to image::format */ + ) + .or_else(|| out.and_then(|path| image::ImageFormat::from_path(path).ok())); + + // Format the image + if let Some(format) = format { + // TODO: format the image and update the internal format + // favicon.format(format); + } + + // Determine output format + // TODO: get from image itself, then fallback to PNG + let out_format = image::ImageOutputFormat::Png; + + // Write favicon to `out` or stdout if not specified + // TODO: figure out this mess, should prob just call to differente methods on + // image.data if out is present or not. the DynamicImage struct has a method for + // writing to a path which could help + let image = favicon.image(); + let writer: Box = if let Some(out) = out { + Box::new(std::fs::File::open(out).unwrap()) // TODO: handle error + } else { + Box::new(io::stdout()) + }; + let writer = BufWriter::new(writer); + let format = image.data.write_to(&mut writer, out_format); } - Some(serve @ Command::Serve { .. }) => { + Some(Command::Serve { .. }) => { // TODO } None => {} From 4e7f62960412d6aee2dcb84912d93babf3f1bbf9 Mon Sep 17 00:00:00 2001 From: Ewan Breakey Date: Sat, 22 Jul 2023 22:46:05 +1000 Subject: [PATCH 06/19] Implement writing images to stdout and files --- img.png | Bin 0 -> 1504 bytes src/cli.rs | 14 +++++++++ src/favicon.rs | 57 +++++++++++++++++++++++++++++++++++++ src/get_favicon.rs | 68 ++------------------------------------------ src/image_writer.rs | 57 +++++++++++++++++++++++++++++++++++++ src/main.rs | 40 ++++++++++---------------- 6 files changed, 145 insertions(+), 91 deletions(-) create mode 100644 img.png create mode 100644 src/favicon.rs create mode 100644 src/image_writer.rs diff --git a/img.png b/img.png new file mode 100644 index 0000000000000000000000000000000000000000..87e86e3d7a92be2c667bda30a10c70103511b6ef GIT binary patch literal 1504 zcmcIk`%{t$6opJJ9miPRB~!srXLU^3va|<zbA7;W>Qbu)_yEdO)Y4K5 zzch6>)3i}^bDb>c!?H9*X>=P@e1bw|7!ThdtnUA?XXf7f%bk19oSAd;&jp>fFxz7W zfj}(q0l4osr^8n5Fxl)W>FJ9Qh;cn0ck%)is+fY_cEXo9;lC62mBI6iTpgi!v~F)~^gW#;*|5Bc~oQ^R|g2 zT1_$btG0L$syl`ItU~+h%nxYJw_|Ona#pzp)gy3L`j)&rjH(it=Tf0bqL4#udt`KSvOI`w?6SZL|$wA?x@vLv> zC)Y#Yg6k=DB+M;to4HqO%1w1m-XQo--+>ICcikA(Q|Rs~^zX7gvz}OGp@INw0b<53 zSUrt-RTybUpBJ_2d6U~K;Adi&FLC;1rVQ)Z=eE=u0mf}E)KTIZagACh7;D?8hpB!< z#4II(J)NS=ol)*mm&6?V5cyRk(-^yI?t_iyNcpzc$zUH6h6L%j;p8p}kAq*KsfA zgL1K2-@5t}fVO|~bMmnMW3WlS{2owR+l}iA+5Y~bwwXmGS2b)p0G~8OD%0N>-JK`L z&p*Th`q`*_IcZgcB^umbGDn_(Ug5~)E8OpZHTy?pD{Ms^KnunRXmzhSYXL>{g<@LG z!hZBXD_Xawjn{83edycP`VMsREOv^7w@>av$uOnt;JD>-a>BBj)Dc~Yc{3nq7>`cK zqW`>gHp&{-3g!MfOpiqHSM)o|;-wY~@tP*GA8#ZRv~(=}Wm6j1j#?tI8{wbF?lu-zKgDXC)_(`}l;kiNsxwbF);wH%rp^NPj{E z6y|A=v(1s3h@w!vxkV|>V|z=tQ5uQrfIJQuTq-Ia2A0q21XsRk2?cQS}XfQVBV#Iy&}+E%H_kP>z>HbC)QqqV=JA zuPU)42aREaJMmzr(Z^9rY^^%M6#Y*A8QkY)Hn~c{_N;yIKsRRJT~lFgXf@$MrXqVQ zOQ$*SZL2wg2zB5^OgVW(XHEZoDQ1E4b6S3}ejGhB*dheulay;%p6t~hv>I<(L$6VS z>`ra;P1djxon=a`XRpS1eX2A~fAywbwE(Jj>Lwx?o#yJ9aguT`sC3kg44EK4&eTy0 z*$o&r@^-2NQ;>N>(d~zu1?X!wU^hvp5-D{Us@y+n_Bnf6)-->+E2~eV`;b*EB@Y~j z#r1xgP8!or|8f+{mvhG}!x$H)>A;aQ>47**C?+IP(T9>qY0NJSiBNrrvBA@YKTdx7 zY{i#W&CYP0&>qS_xOjrk%G9n939uE}YFt;L1_GvwkxFC6a=+bKg8oo}*%w1OMY<6! zai`S?bQLUW0^SX>mAT7e#%>si%FHMlP%SYu8@f>i!)H`Sh+4Q0mp+VM=TR5)^VB{7 zjf~?q*4AG!mUIl$=LEVBti!KUI<(4t;8iVBNm|j{2lG;o!A0zr>EWab)8t7$p_+D( zLm7Iad~9Vo-H{;Ul}xyx^tmKo404MWYXd`fGMN8~3C9OZkH9cUn93T@DFdSK0agZo z;0RF$Ki%ah($ literal 0 HcmV?d00001 diff --git a/src/cli.rs b/src/cli.rs index 2341b6b..8bec5ea 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -14,6 +14,20 @@ pub enum ImageFormatOutput { Tiff, } +impl From for image::ImageFormat { + fn from(value: ImageFormatOutput) -> Self { + match value { + ImageFormatOutput::Png => image::ImageFormat::Png, + ImageFormatOutput::Jpeg => image::ImageFormat::Jpeg, + ImageFormatOutput::Webp => image::ImageFormat::WebP, + ImageFormatOutput::Bmp => image::ImageFormat::Bmp, + ImageFormatOutput::Ico => image::ImageFormat::Ico, + ImageFormatOutput::Gif => image::ImageFormat::Gif, + ImageFormatOutput::Tiff => image::ImageFormat::Tiff, + } + } +} + #[derive(Parser, Debug)] #[command(author, version, about)] pub struct Cli { diff --git a/src/favicon.rs b/src/favicon.rs new file mode 100644 index 0000000..6fd791f --- /dev/null +++ b/src/favicon.rs @@ -0,0 +1,57 @@ +use image::imageops::FilterType; + +use crate::get_favicon::GetFaviconError; + +#[derive(Debug)] +pub struct Image { + pub data: image::DynamicImage, + pub format: Option, +} + +#[derive(Debug)] +pub enum Favicon { + Image(Image), + Fallback(Image, GetFaviconError), +} + +impl Favicon { + pub fn image(&self) -> &Image { + match self { + Favicon::Image(image) => &image, + Favicon::Fallback(image, _) => &image, + } + } + + pub fn format(&mut self, format: image::ImageFormat) { + // "Formatting" is just changing the format we want + self.set_image_format(format) + } + + fn set_image_format(&mut self, format: image::ImageFormat) { + match self { + Self::Image(ref mut img) => { + (*img).format = Some(format); + } + Self::Fallback(ref mut img, _) => { + (*img).format = Some(format); + } + } + } + + fn set_image_data(&mut self, data: image::DynamicImage) { + match self { + Self::Image(ref mut img) => { + (*img).data = data; + } + Self::Fallback(ref mut img, _) => { + (*img).data = data; + } + } + } + + pub fn resize(&mut self, size: u32) { + let image = self.image(); + let image = image.data.resize_to_fill(size, size, FilterType::Lanczos3); + self.set_image_data(image); + } +} diff --git a/src/get_favicon.rs b/src/get_favicon.rs index a9f2e4a..3f889be 100644 --- a/src/get_favicon.rs +++ b/src/get_favicon.rs @@ -1,47 +1,10 @@ -use image::imageops::FilterType; use std::io; use thiserror::Error; use url::Url; -const DEFAULT_IMAGE_SIZE: u32 = 256; - -#[derive(Debug)] -struct Image { - pub data: image::DynamicImage, - pub format: Option, -} - -#[derive(Debug)] -pub enum Favicon { - Image(Image), - Fallback(Image, GetFaviconError), -} - -impl Favicon { - pub fn image(&self) -> &Image { - match self { - Favicon::Image(image) => &image, - Favicon::Fallback(image, _) => &image, - } - } - - fn set_image_data(&mut self, data: image::DynamicImage) { - match self { - Self::Image(ref mut img) => { - (*img).data = data; - } - Self::Fallback(ref mut img, _) => { - (*img).data = data; - } - } - } +use crate::favicon::{Favicon, Image}; - pub fn resize(&mut self, size: u32) { - let image = self.image(); - let image = image.data.resize_to_fill(size, size, FilterType::Lanczos3); - self.set_image_data(image); - } -} +const DEFAULT_IMAGE_SIZE: u32 = 256; #[derive(Debug, Clone)] struct Link { @@ -131,33 +94,6 @@ pub async fn fetch_favicon(target_url: &Url) -> Result })) } -enum ImageFormat { - Png, - Jpeg, - Webp, - Bmp, - Ico, - Gif, - Tiff, - Svg, -} - -impl ImageFormat { - // pub fn from_content_type(content_type: &HeaderValue) -> Option { - // match content_type.to_str().ok()? { - // "image/png" => Some(Self::Png), - // "image/jpeg" => Some(Self::Jpeg), - // "image/webp" => Some(Self::Webp), - // "image/bmp" => Some(Self::Bmp), - // "image/gif" => Some(Self::Gif), - // "image/tiff" => Some(Self::Tiff), - // "image/svg+xml" => Some(Self::Svg), - // "image/vnd.microsoft.icon" | "image/x-icon" => Some(Self::Ico), - // _ => None, - // } - // } -} - /// Scrape the tags from a given URL to find a favicon url async fn scrape_link_tags(url: &Url) -> Result { let res = reqwest::get(url.clone()).await?; diff --git a/src/image_writer.rs b/src/image_writer.rs new file mode 100644 index 0000000..459a992 --- /dev/null +++ b/src/image_writer.rs @@ -0,0 +1,57 @@ +use std::fs; +use std::io::{self, BufWriter}; +use std::path::PathBuf; + +use crate::favicon::Image; +use image::ImageError; + +pub enum ImageWriter { + ToFile(BufWriter), + ToStdout(io::Cursor>), +} + +impl ImageWriter { + pub fn new(file_path: Option) -> Self { + match file_path { + Some(path) => Self::ToFile(BufWriter::new(fs::File::create(path.clone()).unwrap())), + None => Self::ToStdout(io::Cursor::new(Vec::new())), + } + } + + pub fn write_image(&mut self, image: &Image) -> Result<(), ImageError> { + let format: image::ImageOutputFormat = image + .format + .unwrap_or(image::ImageFormat::Png) + .try_into() + .unwrap(); + + image.data.write_to(self, format) + } +} + +impl io::Write for ImageWriter { + fn write(&mut self, buf: &[u8]) -> io::Result { + match self { + ImageWriter::ToFile(writer) => writer.write(buf), + ImageWriter::ToStdout(writer) => writer.write(buf), + } + } + + fn flush(&mut self) -> io::Result<()> { + match self { + ImageWriter::ToFile(writer) => writer.flush(), + ImageWriter::ToStdout(writer) => writer + .flush() + .and_then(|_| io::stdout().write_all(&writer.get_ref())), + } + } +} + +impl io::Seek for ImageWriter { + fn seek(&mut self, pos: io::SeekFrom) -> io::Result { + match self { + ImageWriter::ToFile(writer) => writer.seek(pos), + ImageWriter::ToStdout(writer) => writer.seek(pos), + } + } +} diff --git a/src/main.rs b/src/main.rs index e2a630b..ede11a7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,14 @@ mod cli; +mod favicon; mod get_favicon; +mod image_writer; -use std::io::{self, BufWriter, Write}; +use std::io::Write; use clap::Parser; use cli::{Cli, Command}; use get_favicon::get_favicon; +use image_writer::ImageWriter; #[tokio::main] async fn main() { @@ -18,38 +21,25 @@ async fn main() { format, }) => { // Get favicon (may be a fallback) - let favicon = get_favicon(&url, size).await; + let mut favicon = get_favicon(&url, size).await; // Can we guess the format from the "out" path? - let format = format - .map( - |f| image::ImageFormat::Png, /* TODO: convert f from cli::format to image::format */ - ) - .or_else(|| out.and_then(|path| image::ImageFormat::from_path(path).ok())); + let format: Option = format.map(|f| f.into()).or_else(|| { + out.as_ref() + .and_then(|path| image::ImageFormat::from_path(path).ok()) + }); // Format the image if let Some(format) = format { - // TODO: format the image and update the internal format - // favicon.format(format); + favicon.format(format); } - // Determine output format - // TODO: get from image itself, then fallback to PNG - let out_format = image::ImageOutputFormat::Png; - - // Write favicon to `out` or stdout if not specified - // TODO: figure out this mess, should prob just call to differente methods on - // image.data if out is present or not. the DynamicImage struct has a method for - // writing to a path which could help - let image = favicon.image(); - let writer: Box = if let Some(out) = out { - Box::new(std::fs::File::open(out).unwrap()) // TODO: handle error - } else { - Box::new(io::stdout()) - }; - let writer = BufWriter::new(writer); - let format = image.data.write_to(&mut writer, out_format); + // Write the image + let mut writer = ImageWriter::new(out); + writer.write_image(favicon.image()).unwrap(); + writer.flush().unwrap(); } + Some(Command::Serve { .. }) => { // TODO } From f1f25a0ce21d72cc813007a2ea927793ded138ef Mon Sep 17 00:00:00 2001 From: Ewan Breakey Date: Sat, 22 Jul 2023 23:59:30 +1000 Subject: [PATCH 07/19] Setup cargo feature for server --- Cargo.lock | 106 ++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 6 +++ src/cli.rs | 32 ++++++++----- src/favicon.rs | 12 ++--- src/image_writer.rs | 4 +- src/main.rs | 9 +++- src/server.rs | 3 ++ 7 files changed, 150 insertions(+), 22 deletions(-) create mode 100644 src/server.rs diff --git a/Cargo.lock b/Cargo.lock index ff36894..daffe4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,12 +78,72 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +[[package]] +name = "async-trait" +version = "0.1.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "axum" +version = "0.6.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a1de45611fdb535bfde7b7de4fd54f4fd2b17b1737c0a59b69bf9b92074b8c" +dependencies = [ + "async-trait", + "axum-core", + "bitflags 1.3.2", + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.68" @@ -357,6 +417,7 @@ dependencies = [ name = "favicon-rover" version = "0.1.0" dependencies = [ + "axum", "clap", "image", "reqwest", @@ -836,6 +897,12 @@ version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +[[package]] +name = "matchit" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40" + [[package]] name = "memchr" version = "2.5.0" @@ -1353,6 +1420,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" +dependencies = [ + "itoa", + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1496,6 +1573,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + [[package]] name = "tempfile" version = "3.6.0" @@ -1643,6 +1726,28 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + [[package]] name = "tower-service" version = "0.3.2" @@ -1656,6 +1761,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if", + "log", "pin-project-lite", "tracing-core", ] diff --git a/Cargo.toml b/Cargo.toml index 7c2b3a2..d2286cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,13 @@ authors = ["Benji Grant", "Ewan Breakey"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = [] +server = ["dep:axum"] + [dependencies] +axum = { version = "0.6.19", optional = true } + clap = { version = "4.3.15", features = ["derive"] } image = "0.24.6" reqwest = { version = "0.11.18", features = ["stream"] } diff --git a/src/cli.rs b/src/cli.rs index 8bec5ea..5849d6f 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,5 +1,8 @@ use std::path::PathBuf; +#[cfg(feature = "server")] +use clap::Args; + use clap::{Parser, Subcommand, ValueEnum}; use url::Url; @@ -55,18 +58,23 @@ pub enum Command { format: Option, }, - /// Start a favicon scout web server - Serve { - /// Host to use for http server - #[arg(long, default_value_t = String::from("localhost"), value_name = "URL")] - host: String, + /// Start a favicon rover web server + #[cfg(feature = "server")] + Serve(ServerOptions), +} + +#[cfg(feature = "server")] +#[derive(Args, Debug)] +pub struct ServerOptions { + /// Host to use for http server + #[arg(long, default_value_t = String::from("localhost"), value_name = "URL")] + host: String, - /// Port to use for http server - #[arg(short, long, default_value_t = 3000)] - port: u16, + /// Port to use for http server + #[arg(short, long, default_value_t = 3000)] + port: u16, - /// URL or regex allowed by CORS - #[arg(short, long, default_values_t = [String::from("*")])] - origin: Vec, - }, + /// URL or regex allowed by CORS + #[arg(short, long, default_values_t = [String::from("*")])] + origin: Vec, } diff --git a/src/favicon.rs b/src/favicon.rs index 6fd791f..1c63fbb 100644 --- a/src/favicon.rs +++ b/src/favicon.rs @@ -17,8 +17,8 @@ pub enum Favicon { impl Favicon { pub fn image(&self) -> &Image { match self { - Favicon::Image(image) => &image, - Favicon::Fallback(image, _) => &image, + Favicon::Image(image) => image, + Favicon::Fallback(image, _) => image, } } @@ -30,10 +30,10 @@ impl Favicon { fn set_image_format(&mut self, format: image::ImageFormat) { match self { Self::Image(ref mut img) => { - (*img).format = Some(format); + img.format = Some(format); } Self::Fallback(ref mut img, _) => { - (*img).format = Some(format); + img.format = Some(format); } } } @@ -41,10 +41,10 @@ impl Favicon { fn set_image_data(&mut self, data: image::DynamicImage) { match self { Self::Image(ref mut img) => { - (*img).data = data; + img.data = data; } Self::Fallback(ref mut img, _) => { - (*img).data = data; + img.data = data; } } } diff --git a/src/image_writer.rs b/src/image_writer.rs index 459a992..d2561b8 100644 --- a/src/image_writer.rs +++ b/src/image_writer.rs @@ -13,7 +13,7 @@ pub enum ImageWriter { impl ImageWriter { pub fn new(file_path: Option) -> Self { match file_path { - Some(path) => Self::ToFile(BufWriter::new(fs::File::create(path.clone()).unwrap())), + Some(path) => Self::ToFile(BufWriter::new(fs::File::create(path).unwrap())), None => Self::ToStdout(io::Cursor::new(Vec::new())), } } @@ -42,7 +42,7 @@ impl io::Write for ImageWriter { ImageWriter::ToFile(writer) => writer.flush(), ImageWriter::ToStdout(writer) => writer .flush() - .and_then(|_| io::stdout().write_all(&writer.get_ref())), + .and_then(|_| io::stdout().write_all(writer.get_ref())), } } } diff --git a/src/main.rs b/src/main.rs index ede11a7..ede359b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,9 @@ mod favicon; mod get_favicon; mod image_writer; +#[cfg(feature = "server")] +mod server; + use std::io::Write; use clap::Parser; @@ -40,9 +43,11 @@ async fn main() { writer.flush().unwrap(); } - Some(Command::Serve { .. }) => { - // TODO + #[cfg(feature = "server")] + Some(Command::Serve(options)) => { + server::start_server(options).await; } + None => {} } } diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000..88605bf --- /dev/null +++ b/src/server.rs @@ -0,0 +1,3 @@ +use crate::cli::ServerOptions; + +pub async fn start_server(options: ServerOptions) {} From dc56c979140bebfa6366abaa4938841cc5b962a4 Mon Sep 17 00:00:00 2001 From: Ewan Breakey Date: Sun, 23 Jul 2023 02:14:09 +1000 Subject: [PATCH 08/19] Create FaviconImage wrapper for image --- Cargo.lock | 108 ++++++++++++++++++++++++++++++++++++ Cargo.toml | 6 +- src/{cli.rs => cli_args.rs} | 8 +-- src/fallback.rs | 5 ++ src/favicon.rs | 38 +++++++++---- src/favicon_image.rs | 85 ++++++++++++++++++++++++++++ src/get_favicon.rs | 18 +++--- src/image_writer.rs | 14 ++--- src/main.rs | 23 ++++++-- src/server.rs | 81 ++++++++++++++++++++++++++- 10 files changed, 342 insertions(+), 44 deletions(-) rename src/{cli.rs => cli_args.rs} (93%) create mode 100644 src/fallback.rs create mode 100644 src/favicon_image.rs diff --git a/Cargo.lock b/Cargo.lock index daffe4b..156f7f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -426,6 +426,10 @@ dependencies = [ "thiserror", "tl", "tokio", + "tower", + "tower-http", + "tracing", + "tracing-subscriber", "url", ] @@ -687,6 +691,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range-header" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" + [[package]] name = "httparse" version = "1.8.0" @@ -981,6 +991,16 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -1080,6 +1100,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "parking_lot" version = "0.12.1" @@ -1442,6 +1468,15 @@ dependencies = [ "serde", ] +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -1613,6 +1648,16 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "tiff" version = "0.8.1" @@ -1742,6 +1787,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ae70283aba8d2a8b411c695c437fe25b8b5e44e23e780662002fc72fb47a82" +dependencies = [ + "bitflags 2.3.3", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-range-header", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower-layer" version = "0.3.2" @@ -1763,9 +1827,21 @@ dependencies = [ "cfg-if", "log", "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tracing-core" version = "0.1.31" @@ -1773,6 +1849,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", ] [[package]] @@ -1922,6 +2024,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index d2286cf..b965968 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,10 +12,14 @@ authors = ["Benji Grant", "Ewan Breakey"] [features] default = [] -server = ["dep:axum"] +server = ["dep:axum", "dep:tower-http", "dep:tower", "dep:tracing-subscriber", "dep:tracing"] [dependencies] axum = { version = "0.6.19", optional = true } +tower-http = { version = "0.4.3", features = ["trace", "cors"], optional = true } +tower = { version = "0.4.13", optional = true } +tracing = { version = "0.1.37", optional = true } +tracing-subscriber = { version = "0.3.17", optional = true } clap = { version = "4.3.15", features = ["derive"] } image = "0.24.6" diff --git a/src/cli.rs b/src/cli_args.rs similarity index 93% rename from src/cli.rs rename to src/cli_args.rs index 5849d6f..88e27fd 100644 --- a/src/cli.rs +++ b/src/cli_args.rs @@ -67,14 +67,14 @@ pub enum Command { #[derive(Args, Debug)] pub struct ServerOptions { /// Host to use for http server - #[arg(long, default_value_t = String::from("localhost"), value_name = "URL")] - host: String, + #[arg(long, default_value_t = String::from("127.0.0.1"), value_name = "URL")] + pub host: String, /// Port to use for http server #[arg(short, long, default_value_t = 3000)] - port: u16, + pub port: u16, /// URL or regex allowed by CORS #[arg(short, long, default_values_t = [String::from("*")])] - origin: Vec, + pub origin: Vec, } diff --git a/src/fallback.rs b/src/fallback.rs new file mode 100644 index 0000000..e9121bd --- /dev/null +++ b/src/fallback.rs @@ -0,0 +1,5 @@ +use crate::favicon_image::FaviconImage; + +pub fn generate_fallback(name: String, size: u32) -> FaviconImage { + todo!() +} diff --git a/src/favicon.rs b/src/favicon.rs index 1c63fbb..39b68b5 100644 --- a/src/favicon.rs +++ b/src/favicon.rs @@ -1,21 +1,15 @@ -use image::imageops::FilterType; - +use crate::favicon_image::FaviconImage; use crate::get_favicon::GetFaviconError; - -#[derive(Debug)] -pub struct Image { - pub data: image::DynamicImage, - pub format: Option, -} +use image::imageops::FilterType; #[derive(Debug)] pub enum Favicon { - Image(Image), - Fallback(Image, GetFaviconError), + Image(FaviconImage), + Fallback(FaviconImage, GetFaviconError), } impl Favicon { - pub fn image(&self) -> &Image { + pub fn image(&self) -> &FaviconImage { match self { Favicon::Image(image) => image, Favicon::Fallback(image, _) => image, @@ -55,3 +49,25 @@ impl Favicon { self.set_image_data(image); } } + +#[cfg(feature = "server")] +mod server { + use super::*; + use axum::response::IntoResponse; + + impl IntoResponse for Favicon { + fn into_response(self) -> axum::response::Response { + match self { + Favicon::Image(image) => image.into_response(), + Favicon::Fallback(image, error) => ( + [ + ("x-fallback", "true".to_owned()), + ("x-fallback-reason", error.to_string()), + ], + image, + ) + .into_response(), + } + } + } +} diff --git a/src/favicon_image.rs b/src/favicon_image.rs new file mode 100644 index 0000000..5da7c02 --- /dev/null +++ b/src/favicon_image.rs @@ -0,0 +1,85 @@ +use std::io; +use thiserror::Error; + +#[derive(Debug)] +pub struct FaviconImage { + pub data: image::DynamicImage, + pub format: Option, +} + +#[derive(Error, Debug)] +pub enum WriteImageError { + #[error(transparent)] + ImageError(#[from] image::ImageError), + + #[error("Unsupported image format")] + UnsupportedImageFormat, +} + +impl FaviconImage { + pub fn write_to( + &self, + writer: &mut (impl io::Write + io::Seek), + format: image::ImageFormat, + ) -> Result<(), WriteImageError> { + // Convert image format to output format type + let output_format: image::ImageOutputFormat = format + .try_into() + .map_err(|_| WriteImageError::UnsupportedImageFormat)?; + + // Write image + self.data.write_to(writer, output_format)?; + Ok(()) + } +} + +#[cfg(feature = "server")] +mod server { + use super::*; + use axum::response::IntoResponse; + + impl IntoResponse for FaviconImage { + fn into_response(self) -> axum::response::Response { + // Determine content type + // TODO: use accept-content header to determine + let format = image::ImageFormat::Png; + let content_type = format.content_type(); + + // Write image to buffer + let mut body = io::Cursor::new(Vec::new()); + self.write_to(&mut body, format).unwrap(); + + ([("content-type", content_type)], body.into_inner()).into_response() + } + } + + trait ImageFormatContentTypeExt { + fn content_type(&self) -> String; + } + + impl ImageFormatContentTypeExt for image::ImageFormat { + fn content_type(&self) -> String { + match self { + image::ImageFormat::Png => "image/png", + image::ImageFormat::Jpeg => "image/jpeg", + image::ImageFormat::Gif => "image/gif", + image::ImageFormat::WebP => "image/webp", + image::ImageFormat::Tiff => "image/tiff", + + image::ImageFormat::Bmp => "image/bmp", + image::ImageFormat::Ico => "image/x-icon", + image::ImageFormat::Avif => "image/avif", + + image::ImageFormat::OpenExr => todo!(), + image::ImageFormat::Farbfeld => todo!(), + image::ImageFormat::Qoi => todo!(), + image::ImageFormat::Pnm => todo!(), + image::ImageFormat::Tga => todo!(), + image::ImageFormat::Dds => todo!(), + image::ImageFormat::Hdr => todo!(), + _ => todo!(), + } + .into() + } + } +} diff --git a/src/get_favicon.rs b/src/get_favicon.rs index 3f889be..720e606 100644 --- a/src/get_favicon.rs +++ b/src/get_favicon.rs @@ -2,9 +2,9 @@ use std::io; use thiserror::Error; use url::Url; -use crate::favicon::{Favicon, Image}; +use crate::{fallback::generate_fallback, favicon::Favicon, favicon_image::FaviconImage}; -const DEFAULT_IMAGE_SIZE: u32 = 256; +pub const DEFAULT_IMAGE_SIZE: u32 = 256; #[derive(Debug, Clone)] struct Link { @@ -22,6 +22,9 @@ pub enum GetFaviconError { #[error("Failed to decode image: {0}")] ImageError(#[from] image::ImageError), + + #[error("Provided URL is not a valid url")] + InvalidUrl, } #[derive(Error, Debug)] @@ -51,19 +54,12 @@ pub async fn get_favicon(target_url: &Url, size: Option) -> Favicon { // We didn't get an image, generate one Err(error) => Favicon::Fallback( - Image { - data: generate_fallback(target_url, size.unwrap_or(DEFAULT_IMAGE_SIZE)).await, - format: None, - }, + generate_fallback(target_url.to_string(), size.unwrap_or(DEFAULT_IMAGE_SIZE)), error, ), } } -async fn generate_fallback(target_url: &Url, size: u32) -> image::DynamicImage { - todo!() -} - /// Fetch the favicon for a given url pub async fn fetch_favicon(target_url: &Url) -> Result { // Determine favicon url @@ -88,7 +84,7 @@ pub async fn fetch_favicon(target_url: &Url) -> Result let image_format = image_reader.format(); // TODO: this being none might need to be an error let image_data = image_reader.decode()?; - Ok(Favicon::Image(Image { + Ok(Favicon::Image(FaviconImage { data: image_data, format: image_format, })) diff --git a/src/image_writer.rs b/src/image_writer.rs index d2561b8..6976783 100644 --- a/src/image_writer.rs +++ b/src/image_writer.rs @@ -2,8 +2,7 @@ use std::fs; use std::io::{self, BufWriter}; use std::path::PathBuf; -use crate::favicon::Image; -use image::ImageError; +use crate::favicon_image::{FaviconImage, WriteImageError}; pub enum ImageWriter { ToFile(BufWriter), @@ -18,14 +17,9 @@ impl ImageWriter { } } - pub fn write_image(&mut self, image: &Image) -> Result<(), ImageError> { - let format: image::ImageOutputFormat = image - .format - .unwrap_or(image::ImageFormat::Png) - .try_into() - .unwrap(); - - image.data.write_to(self, format) + pub fn write_image(&mut self, image: &FaviconImage) -> Result<(), WriteImageError> { + let format = image.format.unwrap_or(image::ImageFormat::Png); + image.write_to(self, format) } } diff --git a/src/main.rs b/src/main.rs index ede359b..ca9a428 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,7 @@ -mod cli; +mod cli_args; +mod fallback; mod favicon; +mod favicon_image; mod get_favicon; mod image_writer; @@ -9,8 +11,8 @@ mod server; use std::io::Write; use clap::Parser; -use cli::{Cli, Command}; -use get_favicon::get_favicon; +use cli_args::{Cli, Command}; +use get_favicon::fetch_favicon; use image_writer::ImageWriter; #[tokio::main] @@ -24,7 +26,13 @@ async fn main() { format, }) => { // Get favicon (may be a fallback) - let mut favicon = get_favicon(&url, size).await; + let mut favicon = match fetch_favicon(&url).await { + Ok(favicon) => favicon, + Err(err) => { + eprintln!("failed to fetch favicon: {}", err); + return; + } + }; // Can we guess the format from the "out" path? let format: Option = format.map(|f| f.into()).or_else(|| { @@ -32,6 +40,11 @@ async fn main() { .and_then(|path| image::ImageFormat::from_path(path).ok()) }); + // Resize the image + if let Some(size) = size { + favicon.resize(size) + } + // Format the image if let Some(format) = format { favicon.format(format); @@ -45,7 +58,7 @@ async fn main() { #[cfg(feature = "server")] Some(Command::Serve(options)) => { - server::start_server(options).await; + server::start_server(options).await.unwrap(); } None => {} diff --git a/src/server.rs b/src/server.rs index 88605bf..cf8aeef 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,3 +1,80 @@ -use crate::cli::ServerOptions; +use std::collections::HashMap; +use std::net::{AddrParseError, IpAddr, SocketAddr}; +use std::str::FromStr; -pub async fn start_server(options: ServerOptions) {} +use axum::extract::{Path, Query}; +use axum::response::IntoResponse; +use axum::{routing::get, Router}; +use thiserror::Error; +use tower_http::trace::{DefaultMakeSpan, DefaultOnResponse, TraceLayer}; +use tracing::Level; +use url::Url; + +use crate::cli_args::ServerOptions; +use crate::fallback::generate_fallback; +use crate::favicon::Favicon; +use crate::get_favicon::{get_favicon, GetFaviconError, DEFAULT_IMAGE_SIZE}; + +#[derive(Error, Debug)] +pub enum ServerError { + #[error(transparent)] + InvalidHost(#[from] AddrParseError), +} + +pub async fn start_server(options: ServerOptions) -> Result<(), ServerError> { + // Init tracing + tracing_subscriber::fmt() + .with_max_level(Level::INFO) + .with_target(false) + .compact() + .init(); + + // Define axum app + let app = Router::new() + .route("/:path", get(get_favicon_handler)) + .layer( + TraceLayer::new_for_http() + .make_span_with(DefaultMakeSpan::new().level(Level::INFO)) + .on_response(DefaultOnResponse::new().level(Level::INFO)), + ); + + // Parse address + let addr = IpAddr::from_str(&options.host)?; + let addr = SocketAddr::new(addr, options.port); + + // Start server + tracing::info!("Starting favicon rover on {}", addr); + axum::Server::bind(&addr) + .serve(app.into_make_service_with_connect_info::()) + .with_graceful_shutdown(async { + tokio::signal::ctrl_c() + .await + .expect("Failed to install Ctrl+C handler") + }) + .await + .unwrap(); + + Ok(()) +} + +async fn get_favicon_handler( + Path(target_url_input): Path, + Query(params): Query>, +) -> impl IntoResponse { + // Determine requested size + let size: Option = params.get("size").and_then(|s| s.parse().ok()); + + // Parse the provided url + let target_url = Url::parse(&target_url_input) + .ok() + .or_else(|| Url::parse(&format!("http://{}", target_url_input)).ok()); + + // Get the favicon and send it + match target_url { + Some(target_url) => get_favicon(&target_url, size).await, + None => Favicon::Fallback( + generate_fallback(target_url_input, size.unwrap_or(DEFAULT_IMAGE_SIZE)), + GetFaviconError::InvalidUrl, + ), + } +} From 2d0946cd4e22a2b2a9702453a526f8fea370434b Mon Sep 17 00:00:00 2001 From: Ewan Breakey Date: Sun, 23 Jul 2023 14:26:30 +1000 Subject: [PATCH 09/19] Add webp codec support --- Cargo.lock | 39 +++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/favicon_image.rs | 24 ++++++++++++++++++++++-- src/get_favicon.rs | 30 +++++++++++++++++++++++++++--- 4 files changed, 89 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 156f7f3..14d80da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -212,6 +212,9 @@ name = "cc" version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +dependencies = [ + "jobserver", +] [[package]] name = "cfg-if" @@ -431,6 +434,7 @@ dependencies = [ "tracing", "tracing-subscriber", "url", + "webp", ] [[package]] @@ -614,6 +618,12 @@ version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "h2" version = "0.3.20" @@ -834,6 +844,15 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +[[package]] +name = "jobserver" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +dependencies = [ + "libc", +] + [[package]] name = "jpeg-decoder" version = "0.3.0" @@ -879,6 +898,16 @@ version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +[[package]] +name = "libwebp-sys" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5df1e76f0acef0058aa2164ccf74e610e716e7f9eeb3ee2283de7d43659d823" +dependencies = [ + "cc", + "glob", +] + [[package]] name = "linux-raw-sys" version = "0.3.8" @@ -2146,6 +2175,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cf4eb6b771babe49cf2499777779a2ba9ff9ed3e531040584605732fcfd2e35" +dependencies = [ + "image", + "libwebp-sys", +] + [[package]] name = "weezl" version = "0.1.7" diff --git a/Cargo.toml b/Cargo.toml index b965968..7ba14ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,3 +30,4 @@ thiserror = "1.0.43" tl = "0.7.7" tokio = { version = "1.29.1", features = ["full"] } url = "2.4.0" +webp = "0.2.4" diff --git a/src/favicon_image.rs b/src/favicon_image.rs index 5da7c02..497d190 100644 --- a/src/favicon_image.rs +++ b/src/favicon_image.rs @@ -1,6 +1,8 @@ use std::io; use thiserror::Error; +const WEBP_QUALITY: f32 = 70.0; + #[derive(Debug)] pub struct FaviconImage { pub data: image::DynamicImage, @@ -14,6 +16,9 @@ pub enum WriteImageError { #[error("Unsupported image format")] UnsupportedImageFormat, + + #[error(transparent)] + IOError(#[from] io::Error), } impl FaviconImage { @@ -22,6 +27,11 @@ impl FaviconImage { writer: &mut (impl io::Write + io::Seek), format: image::ImageFormat, ) -> Result<(), WriteImageError> { + // Seperately handle output of webp + if format == image::ImageFormat::WebP { + return self.write_to_webp(writer); + } + // Convert image format to output format type let output_format: image::ImageOutputFormat = format .try_into() @@ -31,6 +41,16 @@ impl FaviconImage { self.data.write_to(writer, output_format)?; Ok(()) } + + fn write_to_webp( + &self, + writer: &mut (impl io::Write + io::Seek), + ) -> Result<(), WriteImageError> { + let encoder = webp::Encoder::from_image(&self.data).expect("Image format is supported"); + let webp = encoder.encode(WEBP_QUALITY); + writer.write_all(webp.as_ref())?; + Ok(()) + } } #[cfg(feature = "server")] @@ -41,8 +61,8 @@ mod server { impl IntoResponse for FaviconImage { fn into_response(self) -> axum::response::Response { // Determine content type - // TODO: use accept-content header to determine - let format = image::ImageFormat::Png; + // TODO: use accept-content header to determine output format + let format = self.format.unwrap_or(image::ImageFormat::Jpeg); let content_type = format.content_type(); // Write image to buffer diff --git a/src/get_favicon.rs b/src/get_favicon.rs index 720e606..be15864 100644 --- a/src/get_favicon.rs +++ b/src/get_favicon.rs @@ -20,11 +20,17 @@ pub enum GetFaviconError { #[error(transparent)] Network(#[from] reqwest::Error), + #[error(transparent)] + TokioError(#[from] tokio::task::JoinError), + #[error("Failed to decode image: {0}")] ImageError(#[from] image::ImageError), #[error("Provided URL is not a valid url")] InvalidUrl, + + #[error("Cannot decode the image type")] + CannotDecode, } #[derive(Error, Debug)] @@ -80,9 +86,27 @@ pub async fn fetch_favicon(target_url: &Url) -> Result .expect("Cursor IO shouldn't fail"); // Decode the image! - // TODO: this is blocking, should it be in a tokio blocking_task? - let image_format = image_reader.format(); // TODO: this being none might need to be an error - let image_data = image_reader.decode()?; + let image_format = image_reader.format(); + let image_data = tokio::task::spawn_blocking(move || { + match image_format { + // Use `webp` crate to decode WebPs + Some(image::ImageFormat::WebP) => { + let data = image_reader.into_inner().into_inner(); + let decoder = webp::Decoder::new(&data); + decoder + .decode() + .ok_or(GetFaviconError::CannotDecode) + .map(|webp| webp.to_image()) + } + + // Use image to decode other + Some(_) => image_reader.decode().map_err(|e| e.into()), + + // We don't know the format + None => Err(GetFaviconError::CannotDecode), + } + }) + .await??; Ok(Favicon::Image(FaviconImage { data: image_data, From fa25ccc95af0bf62d096169337d0e1fb87ae0c5f Mon Sep 17 00:00:00 2001 From: Benji Grant Date: Sun, 17 Dec 2023 16:50:32 +1100 Subject: [PATCH 10/19] Generate fallback initial code --- .vscode/settings.json | 3 + Cargo.lock | 395 ++++++++++++++++++++++++------------------ Cargo.toml | 20 +-- img.png | Bin 1504 -> 0 bytes src/fallback.rs | 37 +++- 5 files changed, 279 insertions(+), 176 deletions(-) create mode 100644 .vscode/settings.json delete mode 100644 img.png diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..8e17e2e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "rust-analyzer.cargo.features": ["server"] +} diff --git a/Cargo.lock b/Cargo.lock index 14d80da..6af72af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,16 +19,15 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "anstream" -version = "0.3.2" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +checksum = "d664a92ecae85fd0a7392615844904654d1d5f5514837f471ddef4a057aba1b6" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", - "is-terminal", "utf8parse", ] @@ -53,17 +52,17 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" dependencies = [ - "windows-sys", + "windows-sys 0.48.0", ] [[package]] name = "anstyle-wincon" -version = "1.0.1" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" dependencies = [ "anstyle", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -97,9 +96,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "axum" -version = "0.6.19" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a1de45611fdb535bfde7b7de4fd54f4fd2b17b1737c0a59b69bf9b92074b8c" +checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" dependencies = [ "async-trait", "axum-core", @@ -179,9 +178,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.3.3" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" [[package]] name = "bumpalo" @@ -224,20 +223,19 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.3.15" +version = "4.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f644d0dac522c8b05ddc39aaaccc5b136d5dc4ff216610c5641e3be5becf56c" +checksum = "bfaff671f6b22ca62406885ece523383b9b64022e341e53e009a62ebc47a45f2" dependencies = [ "clap_builder", "clap_derive", - "once_cell", ] [[package]] name = "clap_builder" -version = "4.3.15" +version = "4.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af410122b9778e024f9e0fb35682cc09cc3f85cad5e8d3ba8f47a9702df6e73d" +checksum = "a216b506622bb1d316cd51328dce24e07bdff4a6128a47c7e7fad11878d5adbb" dependencies = [ "anstream", "anstyle", @@ -247,9 +245,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.3.12" +version = "4.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" dependencies = [ "heck", "proc-macro2", @@ -259,9 +257,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" [[package]] name = "color_quant" @@ -351,9 +349,9 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "data-url" -version = "0.2.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d7439c3735f405729d52c3fbbe4de140eaf938a1fe47d227c27f8254d4302a5" +checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" [[package]] name = "either" @@ -378,7 +376,7 @@ checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" dependencies = [ "errno-dragonfly", "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -487,21 +485,21 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ab2e12762761366dcb876ab8b6e0cfa4797ddcd890575919f008b5ba655672a" dependencies = [ - "roxmltree", + "roxmltree 0.18.0", ] [[package]] name = "fontdb" -version = "0.14.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af8d8cbea8f21307d7e84bca254772981296f058a1d36b461bf4d83a7499fc9e" +checksum = "98b88c54a38407f7352dd2c4238830115a6377741098ffd1f997c813d0e088a6" dependencies = [ "fontconfig-parser", "log", "memmap2", "slotmap", "tinyvec", - "ttf-parser 0.19.1", + "ttf-parser", ] [[package]] @@ -521,9 +519,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] @@ -736,7 +734,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.4.9", "tokio", "tower-service", "tracing", @@ -758,9 +756,9 @@ dependencies = [ [[package]] name = "idna" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -768,9 +766,9 @@ dependencies = [ [[package]] name = "image" -version = "0.24.6" +version = "0.24.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527909aa81e20ac3a44803521443a765550f09b5130c2c2fa1ea59c2f8f50a3a" +checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711" dependencies = [ "bytemuck", "byteorder", @@ -818,7 +816,7 @@ checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ "hermit-abi 0.3.2", "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -827,17 +825,6 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" -[[package]] -name = "is-terminal" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" -dependencies = [ - "hermit-abi 0.3.2", - "rustix 0.38.4", - "windows-sys", -] - [[package]] name = "itoa" version = "1.0.9" @@ -894,15 +881,15 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.147" +version = "0.2.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" [[package]] name = "libwebp-sys" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5df1e76f0acef0058aa2164ccf74e610e716e7f9eeb3ee2283de7d43659d823" +checksum = "3e0df0a0f9444d52aee6335cd724d21a2ee3285f646291799a72be518ec8ee3c" dependencies = [ "cc", "glob", @@ -914,12 +901,6 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" -[[package]] -name = "linux-raw-sys" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" - [[package]] name = "lock_api" version = "0.4.10" @@ -950,9 +931,9 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "memmap2" -version = "0.6.2" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d28bba84adfe6646737845bc5ebbfa2c08424eb1c37e94a1fd2a82adb56a872" +checksum = "8f850157af41022bbb1b04ed15c011ce4d59520be82a4e3718b10c34b02cb85e" dependencies = [ "libc", ] @@ -984,13 +965,13 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", "wasi", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1155,14 +1136,14 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.48.1", ] [[package]] name = "percent-encoding" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pico-args" @@ -1192,9 +1173,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.10" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" @@ -1287,9 +1268,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.18" +version = "0.11.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" dependencies = [ "base64", "bytes", @@ -1312,6 +1293,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", + "system-configuration", "tokio", "tokio-native-tls", "tokio-util", @@ -1326,9 +1308,9 @@ dependencies = [ [[package]] name = "resvg" -version = "0.35.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6554f47c38eca56827eea7f285c2a3018b4e12e0e195cc105833c008be338f1" +checksum = "cadccb3d99a9efb8e5e00c16fbb732cbe400db2ec7fc004697ee7d97d86cf1f4" dependencies = [ "gif", "jpeg-decoder", @@ -1359,6 +1341,12 @@ dependencies = [ "xmlparser", ] +[[package]] +name = "roxmltree" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -1375,21 +1363,8 @@ dependencies = [ "errno", "io-lifetimes", "libc", - "linux-raw-sys 0.3.8", - "windows-sys", -] - -[[package]] -name = "rustix" -version = "0.38.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" -dependencies = [ - "bitflags 2.3.3", - "errno", - "libc", - "linux-raw-sys 0.4.3", - "windows-sys", + "linux-raw-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1400,17 +1375,17 @@ checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "rustybuzz" -version = "0.7.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162bdf42e261bee271b3957691018634488084ef577dddeb6420a9684cab2a6a" +checksum = "f0ae5692c5beaad6a9e22830deeed7874eae8a4e3ba4076fb48e12c56856222c" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.1", "bytemuck", "smallvec", - "ttf-parser 0.18.1", + "ttf-parser", "unicode-bidi-mirroring", "unicode-ccc", - "unicode-general-category", + "unicode-properties", "unicode-script", ] @@ -1426,7 +1401,7 @@ version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" dependencies = [ - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1570,6 +1545,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "spin" version = "0.9.8" @@ -1618,9 +1603,9 @@ dependencies = [ [[package]] name = "svgtypes" -version = "0.11.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed4b0611e7f3277f68c0fa18e385d9e2d26923691379690039548f867cef02a7" +checksum = "6e44e288cd960318917cbd540340968b90becc8bc81f171345d706e7a89d9d70" dependencies = [ "kurbo", "siphasher", @@ -1643,6 +1628,27 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tempfile" version = "3.6.0" @@ -1653,24 +1659,24 @@ dependencies = [ "cfg-if", "fastrand", "redox_syscall", - "rustix 0.37.23", - "windows-sys", + "rustix", + "windows-sys 0.48.0", ] [[package]] name = "thiserror" -version = "1.0.43" +version = "1.0.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42" +checksum = "f11c217e1416d6f036b870f14e0413d480dbf28edbee1f877abaf0206af43bb7" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.43" +version = "1.0.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f" +checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df" dependencies = [ "proc-macro2", "quote", @@ -1689,9 +1695,9 @@ dependencies = [ [[package]] name = "tiff" -version = "0.8.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7449334f9ff2baf290d55d73983a7d6fa15e01198faef72af07e2a8db851e471" +checksum = "6d172b0f4d3fba17ba89811858b9d3d97f928aece846475bbda076ca46736211" dependencies = [ "flate2", "jpeg-decoder", @@ -1700,9 +1706,9 @@ dependencies = [ [[package]] name = "tiny-skia" -version = "0.10.0" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7db11798945fa5c3e5490c794ccca7c6de86d3afdd54b4eb324109939c6f37bc" +checksum = "b6a067b809476893fce6a254cf285850ff69c847e6cfbade6a20b655b6c7e80d" dependencies = [ "arrayref", "arrayvec", @@ -1715,9 +1721,9 @@ dependencies = [ [[package]] name = "tiny-skia-path" -version = "0.10.0" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f60aa35c89ac2687ace1a2556eaaea68e8c0d47408a2e3e7f5c98a489e7281c" +checksum = "5de35e8a90052baaaf61f171680ac2f8e925a1e43ea9d2e3a00514772250e541" dependencies = [ "arrayref", "bytemuck", @@ -1747,11 +1753,10 @@ checksum = "d5e993a1c7c32fdf90a308cec4d457f507b2573acc909bd6e7a092321664fdb3" [[package]] name = "tokio" -version = "1.29.1" +version = "1.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +checksum = "841d45b238a16291a4e1584e61820b8ae57d696cc5015c459c229ccc6990cc1c" dependencies = [ - "autocfg", "backtrace", "bytes", "libc", @@ -1760,16 +1765,16 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.5.5", "tokio-macros", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] name = "tokio-macros" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", @@ -1822,7 +1827,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55ae70283aba8d2a8b411c695c437fe25b8b5e44e23e780662002fc72fb47a82" dependencies = [ - "bitflags 2.3.3", + "bitflags 2.4.1", "bytes", "futures-core", "futures-util", @@ -1849,11 +1854,10 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.37" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "cfg-if", "log", "pin-project-lite", "tracing-attributes", @@ -1862,9 +1866,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.24" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", @@ -1873,9 +1877,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", "valuable", @@ -1883,20 +1887,20 @@ dependencies = [ [[package]] name = "tracing-log" -version = "0.1.3" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ - "lazy_static", "log", + "once_cell", "tracing-core", ] [[package]] name = "tracing-subscriber" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ "nu-ansi-term", "sharded-slab", @@ -1914,15 +1918,9 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "ttf-parser" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0609f771ad9c6155384897e1df4d948e692667cc0588548b68eb44d052b27633" - -[[package]] -name = "ttf-parser" -version = "0.19.1" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a464a4b34948a5f67fddd2b823c62d9d92e44be75058b99939eae6c5b6960b33" +checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" [[package]] name = "unicode-bidi" @@ -1942,12 +1940,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc2520efa644f8268dce4dcd3050eaa7fc044fca03961e9998ac7e2e92b77cf1" -[[package]] -name = "unicode-general-category" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2281c8c1d221438e373249e065ca4989c4c36952c211ff21a0ee91c44a3869e7" - [[package]] name = "unicode-ident" version = "1.0.11" @@ -1963,6 +1955,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-properties" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f91c8b21fbbaa18853c3d0801c78f4fc94cdb976699bb03e832e75f7fd22f0" + [[package]] name = "unicode-script" version = "0.5.5" @@ -1977,9 +1975,9 @@ checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" [[package]] name = "url" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", @@ -1988,9 +1986,9 @@ dependencies = [ [[package]] name = "usvg" -version = "0.35.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14d09ddfb0d93bf84824c09336d32e42f80961a9d1680832eb24fdf249ce11e6" +checksum = "38b0a51b72ab80ca511d126b77feeeb4fb1e972764653e61feac30adc161a756" dependencies = [ "base64", "log", @@ -2003,16 +2001,16 @@ dependencies = [ [[package]] name = "usvg-parser" -version = "0.35.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d19bf93d230813599927d88557014e0908ecc3531666d47c634c6838bc8db408" +checksum = "9bd4e3c291f45d152929a31f0f6c819245e2921bfd01e7bd91201a9af39a2bdc" dependencies = [ "data-url", "flate2", "imagesize", "kurbo", "log", - "roxmltree", + "roxmltree 0.19.0", "simplecss", "siphasher", "svgtypes", @@ -2021,9 +2019,9 @@ dependencies = [ [[package]] name = "usvg-text-layout" -version = "0.35.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "035044604e89652c0a2959b8b356946997a52649ba6cade45928c2842376feb4" +checksum = "d383a3965de199d7f96d4e11a44dd859f46e86de7f3dca9a39bf82605da0a37c" dependencies = [ "fontdb", "kurbo", @@ -2037,9 +2035,9 @@ dependencies = [ [[package]] name = "usvg-tree" -version = "0.35.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7939a7e4ed21cadb5d311d6339730681c3e24c3e81d60065be80e485d3fc8b92" +checksum = "8ee3d202ebdb97a6215604b8f5b4d6ef9024efd623cf2e373a6416ba976ec7d3" dependencies = [ "rctree", "strict-num", @@ -2154,9 +2152,9 @@ checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" [[package]] name = "wasm-streams" -version = "0.2.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bbae3363c08332cadccd13b67db371814cd214c2524020932f0804b8cf7c078" +checksum = "b4609d447824375f43e1ffbc051b50ad8f4b3ae8219680c94452ea05eb240ac7" dependencies = [ "futures-util", "js-sys", @@ -2177,9 +2175,9 @@ dependencies = [ [[package]] name = "webp" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cf4eb6b771babe49cf2499777779a2ba9ff9ed3e531040584605732fcfd2e35" +checksum = "4bb5d8e7814e92297b0e1c773ce43d290bef6c17452dafd9fc49e5edb5beba71" dependencies = [ "image", "libwebp-sys", @@ -2219,7 +2217,16 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.1", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", ] [[package]] @@ -2228,13 +2235,28 @@ version = "0.48.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", ] [[package]] @@ -2243,49 +2265,92 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + [[package]] name = "windows_aarch64_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + [[package]] name = "windows_i686_gnu" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + [[package]] name = "windows_i686_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + [[package]] name = "windows_x86_64_gnu" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + [[package]] name = "windows_x86_64_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + [[package]] name = "winreg" -version = "0.10.1" +version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ - "winapi", + "cfg-if", + "windows-sys 0.48.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 7ba14ac..a05ce36 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,16 +18,16 @@ server = ["dep:axum", "dep:tower-http", "dep:tower", "dep:tracing-subscriber", " axum = { version = "0.6.19", optional = true } tower-http = { version = "0.4.3", features = ["trace", "cors"], optional = true } tower = { version = "0.4.13", optional = true } -tracing = { version = "0.1.37", optional = true } -tracing-subscriber = { version = "0.3.17", optional = true } +tracing = { version = "0.1.40", optional = true } +tracing-subscriber = { version = "0.3.18", optional = true } -clap = { version = "4.3.15", features = ["derive"] } -image = "0.24.6" -reqwest = { version = "0.11.18", features = ["stream"] } -resvg = "0.35.0" +clap = { version = "4.4.11", features = ["derive"] } +image = "0.24.7" +reqwest = { version = "0.11.22", features = ["stream"] } +resvg = "0.37.0" strum = { version = "0.25.0", features = ["derive"] } -thiserror = "1.0.43" +thiserror = "1.0.51" tl = "0.7.7" -tokio = { version = "1.29.1", features = ["full"] } -url = "2.4.0" -webp = "0.2.4" +tokio = { version = "1.35.0", features = ["full"] } +url = "2.5.0" +webp = "0.2.6" diff --git a/img.png b/img.png deleted file mode 100644 index 87e86e3d7a92be2c667bda30a10c70103511b6ef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1504 zcmcIk`%{t$6opJJ9miPRB~!srXLU^3va|<zbA7;W>Qbu)_yEdO)Y4K5 zzch6>)3i}^bDb>c!?H9*X>=P@e1bw|7!ThdtnUA?XXf7f%bk19oSAd;&jp>fFxz7W zfj}(q0l4osr^8n5Fxl)W>FJ9Qh;cn0ck%)is+fY_cEXo9;lC62mBI6iTpgi!v~F)~^gW#;*|5Bc~oQ^R|g2 zT1_$btG0L$syl`ItU~+h%nxYJw_|Ona#pzp)gy3L`j)&rjH(it=Tf0bqL4#udt`KSvOI`w?6SZL|$wA?x@vLv> zC)Y#Yg6k=DB+M;to4HqO%1w1m-XQo--+>ICcikA(Q|Rs~^zX7gvz}OGp@INw0b<53 zSUrt-RTybUpBJ_2d6U~K;Adi&FLC;1rVQ)Z=eE=u0mf}E)KTIZagACh7;D?8hpB!< z#4II(J)NS=ol)*mm&6?V5cyRk(-^yI?t_iyNcpzc$zUH6h6L%j;p8p}kAq*KsfA zgL1K2-@5t}fVO|~bMmnMW3WlS{2owR+l}iA+5Y~bwwXmGS2b)p0G~8OD%0N>-JK`L z&p*Th`q`*_IcZgcB^umbGDn_(Ug5~)E8OpZHTy?pD{Ms^KnunRXmzhSYXL>{g<@LG z!hZBXD_Xawjn{83edycP`VMsREOv^7w@>av$uOnt;JD>-a>BBj)Dc~Yc{3nq7>`cK zqW`>gHp&{-3g!MfOpiqHSM)o|;-wY~@tP*GA8#ZRv~(=}Wm6j1j#?tI8{wbF?lu-zKgDXC)_(`}l;kiNsxwbF);wH%rp^NPj{E z6y|A=v(1s3h@w!vxkV|>V|z=tQ5uQrfIJQuTq-Ia2A0q21XsRk2?cQS}XfQVBV#Iy&}+E%H_kP>z>HbC)QqqV=JA zuPU)42aREaJMmzr(Z^9rY^^%M6#Y*A8QkY)Hn~c{_N;yIKsRRJT~lFgXf@$MrXqVQ zOQ$*SZL2wg2zB5^OgVW(XHEZoDQ1E4b6S3}ejGhB*dheulay;%p6t~hv>I<(L$6VS z>`ra;P1djxon=a`XRpS1eX2A~fAywbwE(Jj>Lwx?o#yJ9aguT`sC3kg44EK4&eTy0 z*$o&r@^-2NQ;>N>(d~zu1?X!wU^hvp5-D{Us@y+n_Bnf6)-->+E2~eV`;b*EB@Y~j z#r1xgP8!or|8f+{mvhG}!x$H)>A;aQ>47**C?+IP(T9>qY0NJSiBNrrvBA@YKTdx7 zY{i#W&CYP0&>qS_xOjrk%G9n939uE}YFt;L1_GvwkxFC6a=+bKg8oo}*%w1OMY<6! zai`S?bQLUW0^SX>mAT7e#%>si%FHMlP%SYu8@f>i!)H`Sh+4Q0mp+VM=TR5)^VB{7 zjf~?q*4AG!mUIl$=LEVBti!KUI<(4t;8iVBNm|j{2lG;o!A0zr>EWab)8t7$p_+D( zLm7Iad~9Vo-H{;Ul}xyx^tmKo404MWYXd`fGMN8~3C9OZkH9cUn93T@DFdSK0agZo z;0RF$Ki%ah($ diff --git a/src/fallback.rs b/src/fallback.rs index e9121bd..e6e3aa5 100644 --- a/src/fallback.rs +++ b/src/fallback.rs @@ -1,5 +1,40 @@ use crate::favicon_image::FaviconImage; +use image::{DynamicImage, RgbaImage}; +use resvg::{ + tiny_skia, + usvg::{self, fontdb, Options, TreeParsing, TreeTextToPath}, + Tree, +}; pub fn generate_fallback(name: String, size: u32) -> FaviconImage { - todo!() + let fallback_svg = format!( + r##" + + + {} + + "##, + name.chars().next().unwrap_or('?').to_ascii_uppercase() + ); + + let rtree = { + // TODO: include a font file in this project for consistent results + let mut fontdb = fontdb::Database::new(); + fontdb.load_system_fonts(); + + let mut tree = usvg::Tree::from_data(fallback_svg.as_bytes(), &Options::default()).unwrap(); + tree.convert_text(&fontdb); + Tree::from_usvg(&tree) + }; + + let pixmap_size = rtree.size.to_int_size(); + let mut pixmap = tiny_skia::Pixmap::new(pixmap_size.width(), pixmap_size.height()).unwrap(); + rtree.render(tiny_skia::Transform::default(), &mut pixmap.as_mut()); + + FaviconImage { + data: DynamicImage::ImageRgba8( + RgbaImage::from_raw(pixmap.width(), pixmap.height(), pixmap.data().to_vec()).unwrap(), + ), + format: None, + } } From 7ac37222bb846766a189e52c3500a711b1dc786a Mon Sep 17 00:00:00 2001 From: Ewan Breakey Date: Sun, 17 Dec 2023 18:09:14 +1100 Subject: [PATCH 11/19] Refactor favicon responses --- src/favicon.rs | 73 -------------------------------- src/favicon_image.rs | 46 ++++++++------------ src/get_favicon.rs | 28 ++---------- src/main.rs | 11 +++-- src/server/favicon_response.rs | 55 ++++++++++++++++++++++++ src/{server.rs => server/mod.rs} | 43 ++++++++++++++----- 6 files changed, 115 insertions(+), 141 deletions(-) delete mode 100644 src/favicon.rs create mode 100644 src/server/favicon_response.rs rename src/{server.rs => server/mod.rs} (65%) diff --git a/src/favicon.rs b/src/favicon.rs deleted file mode 100644 index 39b68b5..0000000 --- a/src/favicon.rs +++ /dev/null @@ -1,73 +0,0 @@ -use crate::favicon_image::FaviconImage; -use crate::get_favicon::GetFaviconError; -use image::imageops::FilterType; - -#[derive(Debug)] -pub enum Favicon { - Image(FaviconImage), - Fallback(FaviconImage, GetFaviconError), -} - -impl Favicon { - pub fn image(&self) -> &FaviconImage { - match self { - Favicon::Image(image) => image, - Favicon::Fallback(image, _) => image, - } - } - - pub fn format(&mut self, format: image::ImageFormat) { - // "Formatting" is just changing the format we want - self.set_image_format(format) - } - - fn set_image_format(&mut self, format: image::ImageFormat) { - match self { - Self::Image(ref mut img) => { - img.format = Some(format); - } - Self::Fallback(ref mut img, _) => { - img.format = Some(format); - } - } - } - - fn set_image_data(&mut self, data: image::DynamicImage) { - match self { - Self::Image(ref mut img) => { - img.data = data; - } - Self::Fallback(ref mut img, _) => { - img.data = data; - } - } - } - - pub fn resize(&mut self, size: u32) { - let image = self.image(); - let image = image.data.resize_to_fill(size, size, FilterType::Lanczos3); - self.set_image_data(image); - } -} - -#[cfg(feature = "server")] -mod server { - use super::*; - use axum::response::IntoResponse; - - impl IntoResponse for Favicon { - fn into_response(self) -> axum::response::Response { - match self { - Favicon::Image(image) => image.into_response(), - Favicon::Fallback(image, error) => ( - [ - ("x-fallback", "true".to_owned()), - ("x-fallback-reason", error.to_string()), - ], - image, - ) - .into_response(), - } - } - } -} diff --git a/src/favicon_image.rs b/src/favicon_image.rs index 497d190..bfc0209 100644 --- a/src/favicon_image.rs +++ b/src/favicon_image.rs @@ -1,3 +1,4 @@ +use image::{imageops::FilterType, ImageFormat}; use std::io; use thiserror::Error; @@ -51,19 +52,32 @@ impl FaviconImage { writer.write_all(webp.as_ref())?; Ok(()) } + + pub fn resize(self, size: u32) -> Self { + let data = self.data.resize_to_fill(size, size, FilterType::Lanczos3); + Self { data, ..self } + } + + pub fn reformat(self, format: ImageFormat) -> Self { + Self { + format: Some(format), + ..self + } + } } #[cfg(feature = "server")] mod server { + use crate::DEFAULT_IMAGE_FORMAT; + use super::*; use axum::response::IntoResponse; impl IntoResponse for FaviconImage { fn into_response(self) -> axum::response::Response { // Determine content type - // TODO: use accept-content header to determine output format - let format = self.format.unwrap_or(image::ImageFormat::Jpeg); - let content_type = format.content_type(); + let format = self.format.unwrap_or(DEFAULT_IMAGE_FORMAT); + let content_type = format.to_mime_type(); // Write image to buffer let mut body = io::Cursor::new(Vec::new()); @@ -76,30 +90,4 @@ mod server { trait ImageFormatContentTypeExt { fn content_type(&self) -> String; } - - impl ImageFormatContentTypeExt for image::ImageFormat { - fn content_type(&self) -> String { - match self { - image::ImageFormat::Png => "image/png", - image::ImageFormat::Jpeg => "image/jpeg", - image::ImageFormat::Gif => "image/gif", - image::ImageFormat::WebP => "image/webp", - image::ImageFormat::Tiff => "image/tiff", - - image::ImageFormat::Bmp => "image/bmp", - image::ImageFormat::Ico => "image/x-icon", - image::ImageFormat::Avif => "image/avif", - - image::ImageFormat::OpenExr => todo!(), - image::ImageFormat::Farbfeld => todo!(), - image::ImageFormat::Qoi => todo!(), - image::ImageFormat::Pnm => todo!(), - image::ImageFormat::Tga => todo!(), - image::ImageFormat::Dds => todo!(), - image::ImageFormat::Hdr => todo!(), - _ => todo!(), - } - .into() - } - } } diff --git a/src/get_favicon.rs b/src/get_favicon.rs index be15864..3e0ebef 100644 --- a/src/get_favicon.rs +++ b/src/get_favicon.rs @@ -2,9 +2,7 @@ use std::io; use thiserror::Error; use url::Url; -use crate::{fallback::generate_fallback, favicon::Favicon, favicon_image::FaviconImage}; - -pub const DEFAULT_IMAGE_SIZE: u32 = 256; +use crate::favicon_image::FaviconImage; #[derive(Debug, Clone)] struct Link { @@ -48,26 +46,8 @@ pub enum ScrapeError { LinkNotFound, } -pub async fn get_favicon(target_url: &Url, size: Option) -> Favicon { - match fetch_favicon(target_url).await { - // We have an image from the target, resize if applicable and return - Ok(mut image) => { - if let Some(size) = size { - image.resize(size); - } - image - } - - // We didn't get an image, generate one - Err(error) => Favicon::Fallback( - generate_fallback(target_url.to_string(), size.unwrap_or(DEFAULT_IMAGE_SIZE)), - error, - ), - } -} - /// Fetch the favicon for a given url -pub async fn fetch_favicon(target_url: &Url) -> Result { +pub async fn fetch_favicon(target_url: &Url) -> Result { // Determine favicon url let image_url = scrape_link_tags(target_url) .await @@ -108,10 +88,10 @@ pub async fn fetch_favicon(target_url: &Url) -> Result }) .await??; - Ok(Favicon::Image(FaviconImage { + Ok(FaviconImage { data: image_data, format: image_format, - })) + }) } /// Scrape the tags from a given URL to find a favicon url diff --git a/src/main.rs b/src/main.rs index ca9a428..d116a85 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,5 @@ mod cli_args; mod fallback; -mod favicon; mod favicon_image; mod get_favicon; mod image_writer; @@ -13,8 +12,12 @@ use std::io::Write; use clap::Parser; use cli_args::{Cli, Command}; use get_favicon::fetch_favicon; +use image::ImageFormat; use image_writer::ImageWriter; +pub const DEFAULT_IMAGE_SIZE: u32 = 256; +pub const DEFAULT_IMAGE_FORMAT: ImageFormat = ImageFormat::Jpeg; + #[tokio::main] async fn main() { let cli = Cli::parse(); @@ -42,17 +45,17 @@ async fn main() { // Resize the image if let Some(size) = size { - favicon.resize(size) + favicon = favicon.resize(size); } // Format the image if let Some(format) = format { - favicon.format(format); + favicon = favicon.reformat(format); } // Write the image let mut writer = ImageWriter::new(out); - writer.write_image(favicon.image()).unwrap(); + writer.write_image(&favicon).unwrap(); writer.flush().unwrap(); } diff --git a/src/server/favicon_response.rs b/src/server/favicon_response.rs new file mode 100644 index 0000000..7b8f163 --- /dev/null +++ b/src/server/favicon_response.rs @@ -0,0 +1,55 @@ +use crate::fallback::generate_fallback; +use crate::favicon_image::FaviconImage; +use crate::get_favicon::GetFaviconError; +use axum::http::{HeaderMap, HeaderName, HeaderValue}; +use axum::response::IntoResponse; +use image::ImageFormat; + +#[derive(Debug)] +pub struct FaviconResponse { + image: FaviconImage, + headers: HeaderMap, +} + +impl FaviconResponse { + pub fn from_fetch_result( + res_value: Result, + host: String, + size: u32, + format: ImageFormat, + ) -> Self { + // Construct response headers + let headers = match &res_value { + Ok(_) => Default::default(), + Err(error) => [ + ("x-fallback", "true"), + ("x-fallback-reason", &error.to_string()), + ] + .into_iter() + .map(|(k, v)| { + ( + HeaderName::from_static(k), + HeaderValue::from_str(v).unwrap(), + ) + }) + .collect(), + }; + + // Get image or fallback w/ correct size + let mut image = match res_value { + Ok(image) => image.resize(size), + Err(_) => generate_fallback(host, size), + }; + + // Set desired format + image = image.reformat(format); + + Self { image, headers } + } +} + +impl IntoResponse for FaviconResponse { + fn into_response(self) -> axum::response::Response { + (self.headers, self.image).into_response() + } +} diff --git a/src/server.rs b/src/server/mod.rs similarity index 65% rename from src/server.rs rename to src/server/mod.rs index cf8aeef..87449a9 100644 --- a/src/server.rs +++ b/src/server/mod.rs @@ -1,19 +1,25 @@ +mod favicon_response; + use std::collections::HashMap; use std::net::{AddrParseError, IpAddr, SocketAddr}; use std::str::FromStr; use axum::extract::{Path, Query}; +use axum::http::HeaderMap; use axum::response::IntoResponse; use axum::{routing::get, Router}; +use image::ImageFormat; use thiserror::Error; use tower_http::trace::{DefaultMakeSpan, DefaultOnResponse, TraceLayer}; use tracing::Level; use url::Url; use crate::cli_args::ServerOptions; -use crate::fallback::generate_fallback; -use crate::favicon::Favicon; -use crate::get_favicon::{get_favicon, GetFaviconError, DEFAULT_IMAGE_SIZE}; +use crate::get_favicon::{fetch_favicon, GetFaviconError}; +use crate::DEFAULT_IMAGE_FORMAT; +use crate::DEFAULT_IMAGE_SIZE; + +use self::favicon_response::FaviconResponse; #[derive(Error, Debug)] pub enum ServerError { @@ -60,21 +66,36 @@ pub async fn start_server(options: ServerOptions) -> Result<(), ServerError> { async fn get_favicon_handler( Path(target_url_input): Path, Query(params): Query>, + headers: HeaderMap, ) -> impl IntoResponse { // Determine requested size let size: Option = params.get("size").and_then(|s| s.parse().ok()); + // Determine requested format + let format: Option = headers.get(axum::http::header::ACCEPT).and_then(|accept| { + // TODO: parse accept header, determine most desired content type + let desired_mime_type = todo!(); + ImageFormat::from_mime_type(desired_mime_type) + }); + // Parse the provided url let target_url = Url::parse(&target_url_input) .ok() .or_else(|| Url::parse(&format!("http://{}", target_url_input)).ok()); - // Get the favicon and send it - match target_url { - Some(target_url) => get_favicon(&target_url, size).await, - None => Favicon::Fallback( - generate_fallback(target_url_input, size.unwrap_or(DEFAULT_IMAGE_SIZE)), - GetFaviconError::InvalidUrl, - ), - } + // Get the favicon + let favicon_res = match target_url { + Some(target_url) => fetch_favicon(&target_url).await, + None => Err(GetFaviconError::InvalidUrl), + }; + + // Construct a response + FaviconResponse::from_fetch_result( + favicon_res, + target_url + .and_then(|url| url.host_str().map(|s| s.to_owned())) + .unwrap_or("?".to_owned()), + size.unwrap_or(DEFAULT_IMAGE_SIZE), + format.unwrap_or(DEFAULT_IMAGE_FORMAT), + ) } From c0ab5cd8d2d31fd66afe14d4c90e42099ad51fec Mon Sep 17 00:00:00 2001 From: Benji Grant Date: Sun, 17 Dec 2023 18:52:28 +1100 Subject: [PATCH 12/19] Convert to image format based on Accept header --- Cargo.lock | 85 +++++++++++++++++++++++++++++++++++++++++------ Cargo.toml | 3 ++ src/fallback.rs | 5 ++- src/server/mod.rs | 29 +++++++++++++--- 4 files changed, 105 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6af72af..ac3b67f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,18 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "accept-header" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d9100fb78aa3b854faf01fd9f65f7d1805c47f0f2720919a4120b0a4be0dd83" +dependencies = [ + "http", + "itertools", + "mime", + "snafu", +] + [[package]] name = "addr2line" version = "0.20.0" @@ -85,7 +97,7 @@ checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.26", ] [[package]] @@ -252,7 +264,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.26", ] [[package]] @@ -353,6 +365,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + [[package]] name = "either" version = "1.8.1" @@ -418,9 +436,12 @@ dependencies = [ name = "favicon-rover" version = "0.1.0" dependencies = [ + "accept-header", "axum", "clap", "image", + "lazy_static", + "mime", "reqwest", "resvg", "strum", @@ -555,7 +576,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.26", ] [[package]] @@ -825,6 +846,15 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.9" @@ -1089,7 +1119,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.26", ] [[package]] @@ -1168,7 +1198,7 @@ checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.26", ] [[package]] @@ -1535,6 +1565,28 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +[[package]] +name = "snafu" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4de37ad025c587a29e8f3f5605c00f70b98715ef90b9061a815b9e59e9042d6" +dependencies = [ + "doc-comment", + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990079665f075b699031e9c08fd3ab99be5029b96f3b78dc0709e8f77e4efebf" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "socket2" version = "0.4.9" @@ -1598,7 +1650,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn", + "syn 2.0.26", ] [[package]] @@ -1611,6 +1663,17 @@ dependencies = [ "siphasher", ] +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.26" @@ -1680,7 +1743,7 @@ checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.26", ] [[package]] @@ -1778,7 +1841,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.26", ] [[package]] @@ -1872,7 +1935,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.26", ] [[package]] @@ -2105,7 +2168,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.26", "wasm-bindgen-shared", ] @@ -2139,7 +2202,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.26", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/Cargo.toml b/Cargo.toml index a05ce36..2225546 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,3 +31,6 @@ tl = "0.7.7" tokio = { version = "1.35.0", features = ["full"] } url = "2.5.0" webp = "0.2.6" +accept-header = "0.2.3" +mime = "0.3.17" +lazy_static = "1.4.0" diff --git a/src/fallback.rs b/src/fallback.rs index e6e3aa5..1226ceb 100644 --- a/src/fallback.rs +++ b/src/fallback.rs @@ -2,7 +2,7 @@ use crate::favicon_image::FaviconImage; use image::{DynamicImage, RgbaImage}; use resvg::{ tiny_skia, - usvg::{self, fontdb, Options, TreeParsing, TreeTextToPath}, + usvg::{self, fontdb, Options, Size, TreeParsing, TreeTextToPath}, Tree, }; @@ -24,6 +24,9 @@ pub fn generate_fallback(name: String, size: u32) -> FaviconImage { let mut tree = usvg::Tree::from_data(fallback_svg.as_bytes(), &Options::default()).unwrap(); tree.convert_text(&fontdb); + tree.size = tree + .size + .scale_to(Size::from_wh(size as f32, size as f32).unwrap()); Tree::from_usvg(&tree) }; diff --git a/src/server/mod.rs b/src/server/mod.rs index 87449a9..40ad9a2 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -4,11 +4,14 @@ use std::collections::HashMap; use std::net::{AddrParseError, IpAddr, SocketAddr}; use std::str::FromStr; +use accept_header::Accept; use axum::extract::{Path, Query}; use axum::http::HeaderMap; use axum::response::IntoResponse; use axum::{routing::get, Router}; use image::ImageFormat; +use lazy_static::lazy_static; +use mime::Mime; use thiserror::Error; use tower_http::trace::{DefaultMakeSpan, DefaultOnResponse, TraceLayer}; use tracing::Level; @@ -21,6 +24,18 @@ use crate::DEFAULT_IMAGE_SIZE; use self::favicon_response::FaviconResponse; +lazy_static! { + static ref SUPPORTED_OUTPUT_MIME_TYPES: Vec = { + use ImageFormat::*; + [ + Png, Jpeg, Gif, WebP, Pnm, Tiff, Tga, Dds, Bmp, Ico, Hdr, OpenExr, Farbfeld, Qoi, + ] + .into_iter() + .map(|format| Mime::from_str(format.to_mime_type()).unwrap()) + .collect() + }; +} + #[derive(Error, Debug)] pub enum ServerError { #[error(transparent)] @@ -73,9 +88,13 @@ async fn get_favicon_handler( // Determine requested format let format: Option = headers.get(axum::http::header::ACCEPT).and_then(|accept| { - // TODO: parse accept header, determine most desired content type - let desired_mime_type = todo!(); - ImageFormat::from_mime_type(desired_mime_type) + // Parse accept header, determine most desired content type + let accept: Accept = accept.to_str().unwrap().parse().unwrap(); + let mime_type = accept + .negotiate(&SUPPORTED_OUTPUT_MIME_TYPES) + .unwrap() + .to_string(); + ImageFormat::from_mime_type(mime_type) }); // Parse the provided url @@ -84,8 +103,8 @@ async fn get_favicon_handler( .or_else(|| Url::parse(&format!("http://{}", target_url_input)).ok()); // Get the favicon - let favicon_res = match target_url { - Some(target_url) => fetch_favicon(&target_url).await, + let favicon_res = match &target_url { + Some(target_url) => fetch_favicon(target_url).await, None => Err(GetFaviconError::InvalidUrl), }; From 0b9043510e23517ae49b4a202cdac34b701b3a16 Mon Sep 17 00:00:00 2001 From: Benji Grant Date: Sun, 17 Dec 2023 19:53:13 +1100 Subject: [PATCH 13/19] Render SVG favicons --- Cargo.toml | 6 ++-- src/fallback.rs | 30 +---------------- src/favicon_image.rs | 33 +++++++++++++++++++ src/get_favicon.rs | 76 +++++++++++++++++++++++++++++++++++--------- src/main.rs | 2 +- src/server/mod.rs | 2 +- 6 files changed, 100 insertions(+), 49 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2225546..d1bbdc4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ authors = ["Benji Grant", "Ewan Breakey"] [features] default = [] -server = ["dep:axum", "dep:tower-http", "dep:tower", "dep:tracing-subscriber", "dep:tracing"] +server = ["dep:axum", "dep:tower-http", "dep:tower", "dep:tracing-subscriber", "dep:tracing", "dep:accept-header", "dep:mime"] [dependencies] axum = { version = "0.6.19", optional = true } @@ -31,6 +31,6 @@ tl = "0.7.7" tokio = { version = "1.35.0", features = ["full"] } url = "2.5.0" webp = "0.2.6" -accept-header = "0.2.3" -mime = "0.3.17" +accept-header = { version = "0.2.3", optional = true} +mime = { version = "0.3.17", optional = true } lazy_static = "1.4.0" diff --git a/src/fallback.rs b/src/fallback.rs index 1226ceb..d829218 100644 --- a/src/fallback.rs +++ b/src/fallback.rs @@ -1,10 +1,4 @@ use crate::favicon_image::FaviconImage; -use image::{DynamicImage, RgbaImage}; -use resvg::{ - tiny_skia, - usvg::{self, fontdb, Options, Size, TreeParsing, TreeTextToPath}, - Tree, -}; pub fn generate_fallback(name: String, size: u32) -> FaviconImage { let fallback_svg = format!( @@ -17,27 +11,5 @@ pub fn generate_fallback(name: String, size: u32) -> FaviconImage { name.chars().next().unwrap_or('?').to_ascii_uppercase() ); - let rtree = { - // TODO: include a font file in this project for consistent results - let mut fontdb = fontdb::Database::new(); - fontdb.load_system_fonts(); - - let mut tree = usvg::Tree::from_data(fallback_svg.as_bytes(), &Options::default()).unwrap(); - tree.convert_text(&fontdb); - tree.size = tree - .size - .scale_to(Size::from_wh(size as f32, size as f32).unwrap()); - Tree::from_usvg(&tree) - }; - - let pixmap_size = rtree.size.to_int_size(); - let mut pixmap = tiny_skia::Pixmap::new(pixmap_size.width(), pixmap_size.height()).unwrap(); - rtree.render(tiny_skia::Transform::default(), &mut pixmap.as_mut()); - - FaviconImage { - data: DynamicImage::ImageRgba8( - RgbaImage::from_raw(pixmap.width(), pixmap.height(), pixmap.data().to_vec()).unwrap(), - ), - format: None, - } + FaviconImage::from_svg_str(fallback_svg, size) } diff --git a/src/favicon_image.rs b/src/favicon_image.rs index bfc0209..119690b 100644 --- a/src/favicon_image.rs +++ b/src/favicon_image.rs @@ -1,4 +1,10 @@ use image::{imageops::FilterType, ImageFormat}; +use image::{DynamicImage, RgbaImage}; +use resvg::{ + tiny_skia, + usvg::{self, fontdb, Options, Size, TreeParsing, TreeTextToPath}, + Tree, +}; use std::io; use thiserror::Error; @@ -64,6 +70,33 @@ impl FaviconImage { ..self } } + + pub fn from_svg_str(svg: String, size: u32) -> Self { + let rtree = { + // TODO: include a font file in this project for consistent results + let mut fontdb = fontdb::Database::new(); + fontdb.load_system_fonts(); + + let mut tree = usvg::Tree::from_data(svg.as_bytes(), &Options::default()).unwrap(); + tree.convert_text(&fontdb); + tree.size = tree + .size + .scale_to(Size::from_wh(size as f32, size as f32).unwrap()); + Tree::from_usvg(&tree) + }; + + let pixmap_size = rtree.size.to_int_size(); + let mut pixmap = tiny_skia::Pixmap::new(pixmap_size.width(), pixmap_size.height()).unwrap(); + rtree.render(tiny_skia::Transform::default(), &mut pixmap.as_mut()); + + Self { + data: DynamicImage::ImageRgba8( + RgbaImage::from_raw(pixmap.width(), pixmap.height(), pixmap.data().to_vec()) + .unwrap(), + ), + format: None, + } + } } #[cfg(feature = "server")] diff --git a/src/get_favicon.rs b/src/get_favicon.rs index 3e0ebef..a7eb46e 100644 --- a/src/get_favicon.rs +++ b/src/get_favicon.rs @@ -1,13 +1,25 @@ -use std::io; +use reqwest::{ + header::{CONTENT_TYPE, USER_AGENT}, + Client, +}; +use std::{io, sync::OnceLock}; use thiserror::Error; use url::Url; use crate::favicon_image::FaviconImage; +const BOT_USER_AGENT: &str = "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36"; + +static REQWEST_CLIENT: OnceLock = OnceLock::new(); + +fn reqwest_client() -> &'static Client { + REQWEST_CLIENT.get_or_init(|| Client::builder().build().unwrap()) +} + #[derive(Debug, Clone)] struct Link { href: String, - size: Option, + size: usize, } #[derive(Error, Debug)] @@ -47,14 +59,29 @@ pub enum ScrapeError { } /// Fetch the favicon for a given url -pub async fn fetch_favicon(target_url: &Url) -> Result { +pub async fn fetch_favicon(target_url: &Url, size: u32) -> Result { // Determine favicon url - let image_url = scrape_link_tags(target_url) + let image_url = scrape_link_tags(target_url, size) .await .unwrap_or_else(|_| target_url.join("/favicon.ico").unwrap()); // Fetch the image - let res = reqwest::get(image_url).await?; + let client = reqwest_client(); + let res = client + .get(image_url) + .header(USER_AGENT, BOT_USER_AGENT) + .send() + .await?; + + // Render SVGs + if res + .headers() + .get(CONTENT_TYPE) + .is_some_and(|content_type| content_type == "image/svg+xml") + { + let svg = res.text().await?; + return Ok(FaviconImage::from_svg_str(svg, size)); + } // Get HTTP response body let body = res.bytes().await?; @@ -95,8 +122,13 @@ pub async fn fetch_favicon(target_url: &Url) -> Result tags from a given URL to find a favicon url -async fn scrape_link_tags(url: &Url) -> Result { - let res = reqwest::get(url.clone()).await?; +async fn scrape_link_tags(url: &Url, preferred_size: u32) -> Result { + let client = reqwest_client(); + let res = client + .get(url.clone()) + .header(USER_AGENT, BOT_USER_AGENT) + .send() + .await?; let html = res.text().await?; let dom = tl::parse(&html, tl::ParserOptions::default())?; @@ -111,19 +143,23 @@ async fn scrape_link_tags(url: &Url) -> Result { if String::from(media.as_utf8_str()) .replace(' ', "") .to_ascii_lowercase() - == *"prefers-color-scheme:dark" + .contains("prefers-color-scheme:dark") { return None; } } Some(Link { href: href.as_utf8_str().into_owned(), - size: attr.get("sizes").flatten().and_then(|sizes| { - sizes - .as_utf8_str() - .split_once('x') - .and_then(|(size, _)| size.parse().ok()) - }), + size: attr + .get("sizes") + .flatten() + .and_then(|sizes| { + sizes + .as_utf8_str() + .split_once('x') + .and_then(|(size, _)| size.parse().ok()) + }) + .unwrap_or(0), }) } None => None, @@ -136,5 +172,15 @@ async fn scrape_link_tags(url: &Url) -> Result { links.sort_unstable_by_key(|link| link.size); - Ok(Url::parse(&links.get(0).unwrap().href)?) + // If an icon larger than the preferred size exists, use the closest + // to what we want instead of always using the largest image available + let filtered_links: Vec<_> = links + .iter() + .filter(|link| link.size < preferred_size as usize) + .collect(); + if !filtered_links.is_empty() { + return Ok(url.join(&filtered_links.first().unwrap().href)?); + } + + Ok(url.join(&links.last().unwrap().href)?) } diff --git a/src/main.rs b/src/main.rs index d116a85..fffce38 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,7 +29,7 @@ async fn main() { format, }) => { // Get favicon (may be a fallback) - let mut favicon = match fetch_favicon(&url).await { + let mut favicon = match fetch_favicon(&url, size.unwrap_or(DEFAULT_IMAGE_SIZE)).await { Ok(favicon) => favicon, Err(err) => { eprintln!("failed to fetch favicon: {}", err); diff --git a/src/server/mod.rs b/src/server/mod.rs index 40ad9a2..0d7dd8d 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -104,7 +104,7 @@ async fn get_favicon_handler( // Get the favicon let favicon_res = match &target_url { - Some(target_url) => fetch_favicon(target_url).await, + Some(target_url) => fetch_favicon(target_url, size.unwrap_or(DEFAULT_IMAGE_SIZE)).await, None => Err(GetFaviconError::InvalidUrl), }; From 5eff3742e10203e44522f42a1d20b4579872d7e0 Mon Sep 17 00:00:00 2001 From: Benji Grant Date: Sun, 17 Dec 2023 21:48:34 +1100 Subject: [PATCH 14/19] Implement CORS protection --- Cargo.lock | 43 +++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 3 ++- README.md | 30 +++++++++++++++++++----------- src/cli_args.rs | 2 +- src/get_favicon.rs | 1 - src/server/mod.rs | 45 ++++++++++++++++++++++++++++++++++++++++++++- 6 files changed, 107 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ac3b67f..75d0887 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + [[package]] name = "anstream" version = "0.6.5" @@ -442,6 +451,7 @@ dependencies = [ "image", "lazy_static", "mime", + "regex", "reqwest", "resvg", "strum", @@ -955,9 +965,9 @@ checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40" [[package]] name = "memchr" -version = "2.5.0" +version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" [[package]] name = "memmap2" @@ -1296,6 +1306,35 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + [[package]] name = "reqwest" version = "0.11.22" diff --git a/Cargo.toml b/Cargo.toml index d1bbdc4..b8d4a55 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ authors = ["Benji Grant", "Ewan Breakey"] [features] default = [] -server = ["dep:axum", "dep:tower-http", "dep:tower", "dep:tracing-subscriber", "dep:tracing", "dep:accept-header", "dep:mime"] +server = ["dep:axum", "dep:tower-http", "dep:tower", "dep:tracing-subscriber", "dep:tracing", "dep:accept-header", "dep:mime", "dep:regex"] [dependencies] axum = { version = "0.6.19", optional = true } @@ -33,4 +33,5 @@ url = "2.5.0" webp = "0.2.6" accept-header = { version = "0.2.3", optional = true} mime = { version = "0.3.17", optional = true } +regex = { version = "1.10.2", optional = true } lazy_static = "1.4.0" diff --git a/README.md b/README.md index 0eee743..e06462b 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ Fetch the favicon of any website. -- 🌐 Web server - ⌨️ CLI tool +- 🌐 Web server - 🛟 Fallback icons - 🦀 Rust @@ -21,27 +21,30 @@ cargo install favicon-rover Fetch the favicon for a site using the cli tool ```bash -# Usage: favicon-rover [options] +# Usage: favicon-rover get [OPTIONS] -favicon-rover https://crates.io # output the crates favicon to stdout +favicon-rover get https://crates.io # output the crates favicon to stdout -favicon-rover https://crates.io --out favicon.png # output to favicon.png +favicon-rover get https://crates.io --out favicon.png # output to favicon.png -favicon-rover https://crates.io --size 256 # set the size to 256px +favicon-rover get https://crates.io --size 256 # set the size to 256px -favicon-rover https://crates.io --type webp # set the format to webp +favicon-rover get https://crates.io --type webp # set the format to webp -favicon-rover https://crates.io -o favicons/cratesio -s 50 -t webp # all options +favicon-rover get https://crates.io -o favicons/cratesio -s 50 -t webp # all options -favicon-rover --help # show help information +favicon-rover get --help # show help information ``` ## Web Server +> [!IMPORTANT] +> You need to enable the `server` feature for this command to be available + Start the web server to expose an API that will fetch favicons ```bash -# Usage: favicon-rover serve [options] +# Usage: favicon-rover serve [OPTIONS] favicon-rover serve # start with default options @@ -59,20 +62,25 @@ favicon-rover serve --help # show help information ### API ```h -/{site url}/{size} +/{site url}?size={size} ``` `site url` is any valid url to a page that you want the favicon for. Must be URL encoded. `size` is an integer in pixels to set the returned image. It's optional, and if not included then the best available size will be returned. +Example: `http://localhost:3000/example.com?size=24` + ### CORS By default, any origin is allowed to make a request to this API. To lock it down, use the `--origin` command line options to specify any amount of origins. If an origin starts and ends with `/` it will be treated as a regexp. For example `favicon-rover serve -o http://example1.com -o /\.example2\.com$/` will accept any request from "http://example1.com" or from a subdomain of "example2.com". +> [!TIP] +> We highly recommend setting an origin so your favicon API can't be as easily abused by websites you don't control + ## Development -Run `cargo run` to test the binary. You can test the serve command with `cargo run -- serve`. +Run `cargo run` to test the binary. You can test the serve command with `cargo run --features server -- serve`. Run `cargo build` to build in release mode. diff --git a/src/cli_args.rs b/src/cli_args.rs index 88e27fd..805f89c 100644 --- a/src/cli_args.rs +++ b/src/cli_args.rs @@ -74,7 +74,7 @@ pub struct ServerOptions { #[arg(short, long, default_value_t = 3000)] pub port: u16, - /// URL or regex allowed by CORS + /// URL or regex allowed by CORS (multiple allowed) #[arg(short, long, default_values_t = [String::from("*")])] pub origin: Vec, } diff --git a/src/get_favicon.rs b/src/get_favicon.rs index a7eb46e..628a32d 100644 --- a/src/get_favicon.rs +++ b/src/get_favicon.rs @@ -11,7 +11,6 @@ use crate::favicon_image::FaviconImage; const BOT_USER_AGENT: &str = "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36"; static REQWEST_CLIENT: OnceLock = OnceLock::new(); - fn reqwest_client() -> &'static Client { REQWEST_CLIENT.get_or_init(|| Client::builder().build().unwrap()) } diff --git a/src/server/mod.rs b/src/server/mod.rs index 0d7dd8d..a0e4f9a 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -3,16 +3,19 @@ mod favicon_response; use std::collections::HashMap; use std::net::{AddrParseError, IpAddr, SocketAddr}; use std::str::FromStr; +use std::sync::OnceLock; use accept_header::Accept; use axum::extract::{Path, Query}; -use axum::http::HeaderMap; +use axum::http::{header, HeaderMap, Method}; use axum::response::IntoResponse; use axum::{routing::get, Router}; use image::ImageFormat; use lazy_static::lazy_static; use mime::Mime; +use regex::Regex; use thiserror::Error; +use tower_http::cors::{AllowOrigin, Any, CorsLayer}; use tower_http::trace::{DefaultMakeSpan, DefaultOnResponse, TraceLayer}; use tracing::Level; use url::Url; @@ -36,6 +39,27 @@ lazy_static! { }; } +enum CorsOrigin { + Regex(Regex), + String(String), +} + +static CORS_ORIGINS: OnceLock> = OnceLock::new(); +fn cors_origins(origins: &[String]) -> &'static Vec { + CORS_ORIGINS.get_or_init(|| { + origins + .iter() + .map(|o| { + if o.starts_with('/') && o.ends_with('/') { + CorsOrigin::Regex(Regex::new(o.split_at(1).1.split_at(o.len() - 2).0).unwrap()) + } else { + CorsOrigin::String(o.to_owned()) + } + }) + .collect() + }) +} + #[derive(Error, Debug)] pub enum ServerError { #[error(transparent)] @@ -50,9 +74,28 @@ pub async fn start_server(options: ServerOptions) -> Result<(), ServerError> { .compact() .init(); + // Cors + let mut cors = CorsLayer::new() + .allow_headers([header::ACCEPT, header::CONTENT_TYPE]) + .allow_methods([Method::GET, Method::OPTIONS, Method::HEAD]); + + if options.origin.len() == 1 && options.origin[0] == "*" { + cors = cors.allow_origin(Any) + } else if options.origin.len() > 1 && options.origin.iter().any(|o| o == "*") { + panic!("Wildcard (*) origin must be the only origin"); + } else { + cors = cors.allow_origin(AllowOrigin::predicate(move |origin, _| { + cors_origins(&options.origin).iter().any(|o| match o { + CorsOrigin::Regex(re) => re.is_match(origin.to_str().unwrap()), + CorsOrigin::String(o) => o == origin.to_str().unwrap(), + }) + })) + } + // Define axum app let app = Router::new() .route("/:path", get(get_favicon_handler)) + .layer(cors) .layer( TraceLayer::new_for_http() .make_span_with(DefaultMakeSpan::new().level(Level::INFO)) From 9ec134f529097d53431e67ad27b9b1e3c0974c00 Mon Sep 17 00:00:00 2001 From: Benji Grant Date: Sun, 17 Dec 2023 21:55:09 +1100 Subject: [PATCH 15/19] Use EnvFilter for tracing --- Cargo.lock | 34 +++++++++++++++++++++++++++++++--- Cargo.toml | 2 +- src/server/mod.rs | 18 ++++++++++++++---- 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 75d0887..e939d93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -957,6 +957,15 @@ version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "matchit" version = "0.7.0" @@ -1314,8 +1323,17 @@ checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.3", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", ] [[package]] @@ -1326,9 +1344,15 @@ checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.2", ] +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "regex-syntax" version = "0.8.2" @@ -2004,10 +2028,14 @@ version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ + "matchers", "nu-ansi-term", + "once_cell", + "regex", "sharded-slab", "smallvec", "thread_local", + "tracing", "tracing-core", "tracing-log", ] diff --git a/Cargo.toml b/Cargo.toml index b8d4a55..dfc4dc7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ axum = { version = "0.6.19", optional = true } tower-http = { version = "0.4.3", features = ["trace", "cors"], optional = true } tower = { version = "0.4.13", optional = true } tracing = { version = "0.1.40", optional = true } -tracing-subscriber = { version = "0.3.18", optional = true } +tracing-subscriber = { version = "0.3.18", features = ["env-filter"], optional = true } clap = { version = "4.4.11", features = ["derive"] } image = "0.24.7" diff --git a/src/server/mod.rs b/src/server/mod.rs index a0e4f9a..52aac42 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -17,7 +17,10 @@ use regex::Regex; use thiserror::Error; use tower_http::cors::{AllowOrigin, Any, CorsLayer}; use tower_http::trace::{DefaultMakeSpan, DefaultOnResponse, TraceLayer}; +use tracing::level_filters::LevelFilter; use tracing::Level; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; use url::Url; use crate::cli_args::ServerOptions; @@ -68,10 +71,17 @@ pub enum ServerError { pub async fn start_server(options: ServerOptions) -> Result<(), ServerError> { // Init tracing - tracing_subscriber::fmt() - .with_max_level(Level::INFO) - .with_target(false) - .compact() + tracing_subscriber::registry() + .with( + tracing_subscriber::fmt::layer() + .with_target(false) + .compact(), + ) + .with( + tracing_subscriber::EnvFilter::builder() + .with_default_directive(LevelFilter::INFO.into()) + .from_env_lossy(), + ) .init(); // Cors From 40efca128339caeff8cb56cd6dc0ce4d25728233 Mon Sep 17 00:00:00 2001 From: Benji Grant Date: Sun, 17 Dec 2023 22:30:23 +1100 Subject: [PATCH 16/19] Send cache control header --- src/server/favicon_response.rs | 30 ++++++++++++++---------------- src/server/mod.rs | 25 +++++++++++++++++++------ 2 files changed, 33 insertions(+), 22 deletions(-) diff --git a/src/server/favicon_response.rs b/src/server/favicon_response.rs index 7b8f163..33f203e 100644 --- a/src/server/favicon_response.rs +++ b/src/server/favicon_response.rs @@ -1,7 +1,7 @@ use crate::fallback::generate_fallback; use crate::favicon_image::FaviconImage; use crate::get_favicon::GetFaviconError; -use axum::http::{HeaderMap, HeaderName, HeaderValue}; +use axum::http::{header, HeaderMap, HeaderName}; use axum::response::IntoResponse; use image::ImageFormat; @@ -19,21 +19,19 @@ impl FaviconResponse { format: ImageFormat, ) -> Self { // Construct response headers - let headers = match &res_value { - Ok(_) => Default::default(), - Err(error) => [ - ("x-fallback", "true"), - ("x-fallback-reason", &error.to_string()), - ] - .into_iter() - .map(|(k, v)| { - ( - HeaderName::from_static(k), - HeaderValue::from_str(v).unwrap(), - ) - }) - .collect(), - }; + let mut headers = HeaderMap::new(); + headers.insert(header::CACHE_CONTROL, "max-age=604800".parse().unwrap()); + + if let Err(error) = &res_value { + headers.insert( + HeaderName::from_static("x-fallback"), + "true".parse().unwrap(), + ); + headers.insert( + HeaderName::from_static("x-fallback-reason"), + error.to_string().parse().unwrap(), + ); + } // Get image or fallback w/ correct size let mut image = match res_value { diff --git a/src/server/mod.rs b/src/server/mod.rs index 52aac42..99e9a5e 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -7,7 +7,7 @@ use std::sync::OnceLock; use accept_header::Accept; use axum::extract::{Path, Query}; -use axum::http::{header, HeaderMap, Method}; +use axum::http::{HeaderMap, Method}; use axum::response::IntoResponse; use axum::{routing::get, Router}; use image::ImageFormat; @@ -85,14 +85,27 @@ pub async fn start_server(options: ServerOptions) -> Result<(), ServerError> { .init(); // Cors - let mut cors = CorsLayer::new() - .allow_headers([header::ACCEPT, header::CONTENT_TYPE]) - .allow_methods([Method::GET, Method::OPTIONS, Method::HEAD]); + let mut cors = CorsLayer::new().allow_headers(Any).allow_methods([ + Method::GET, + Method::OPTIONS, + Method::HEAD, + ]); if options.origin.len() == 1 && options.origin[0] == "*" { cors = cors.allow_origin(Any) - } else if options.origin.len() > 1 && options.origin.iter().any(|o| o == "*") { - panic!("Wildcard (*) origin must be the only origin"); + } else if options.origin.len() > 1 + && options + .origin + .iter() + .all(|o| !o.starts_with('/') && !o.ends_with('/')) + { + cors = cors.allow_origin( + options + .origin + .iter() + .map(|o| o.parse().unwrap()) + .collect::>(), + ) } else { cors = cors.allow_origin(AllowOrigin::predicate(move |origin, _| { cors_origins(&options.origin).iter().any(|o| match o { From 75c2dbcc3ff969f3617f2f0e90ee319bbee87055 Mon Sep 17 00:00:00 2001 From: Benji Grant Date: Wed, 20 Dec 2023 19:47:00 +1100 Subject: [PATCH 17/19] Run CI with all features --- .github/workflows/check.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 90e2c1b..662b0f6 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -26,11 +26,11 @@ jobs: steps: - uses: actions/checkout@v3 - - run: cargo clippy + - run: cargo clippy --all-features test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - run: cargo test + - run: cargo test --all-features From 4bf0d4fc8b044de7d04b26547583dc83a1516970 Mon Sep 17 00:00:00 2001 From: Benji Grant Date: Wed, 20 Dec 2023 20:05:33 +1100 Subject: [PATCH 18/19] Exclude fallback code without server feature --- src/get_favicon.rs | 1 + src/main.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/get_favicon.rs b/src/get_favicon.rs index 628a32d..eeb4d25 100644 --- a/src/get_favicon.rs +++ b/src/get_favicon.rs @@ -35,6 +35,7 @@ pub enum GetFaviconError { #[error("Failed to decode image: {0}")] ImageError(#[from] image::ImageError), + #[cfg(feature = "server")] #[error("Provided URL is not a valid url")] InvalidUrl, diff --git a/src/main.rs b/src/main.rs index fffce38..8c0acd8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ mod cli_args; +#[cfg(feature = "server")] mod fallback; mod favicon_image; mod get_favicon; From cd7f20c510d5b17bd5f9b3b836d089d92f5e93ee Mon Sep 17 00:00:00 2001 From: Benji Grant Date: Wed, 20 Dec 2023 20:11:54 +1100 Subject: [PATCH 19/19] Add an important comment --- src/server/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/server/mod.rs b/src/server/mod.rs index 99e9a5e..b62a2a5 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -54,6 +54,7 @@ fn cors_origins(origins: &[String]) -> &'static Vec { .iter() .map(|o| { if o.starts_with('/') && o.ends_with('/') { + // Remove the first and last slash CorsOrigin::Regex(Regex::new(o.split_at(1).1.split_at(o.len() - 2).0).unwrap()) } else { CorsOrigin::String(o.to_owned())