From f64e30942596700d8eea6e46363c37131ee7e7b5 Mon Sep 17 00:00:00 2001 From: Allie Signet Date: Fri, 1 Jul 2022 19:27:28 -0300 Subject: [PATCH] EXTREMELY EVIL CACKLING --- Cargo.lock | 581 ++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 6 +- db/data.mdb | Bin 0 -> 94208 bytes db/lock.mdb | Bin 0 -> 8192 bytes src/lib.rs | 237 ++++++++++++++++++ src/main.rs | 195 ++------------- src/routes/get.rs | 56 +++++ src/routes/image.rs | 200 +++++++++++++++ src/routes/mod.rs | 3 + src/routes/upload.rs | 68 +++++ 10 files changed, 1160 insertions(+), 186 deletions(-) create mode 100644 db/data.mdb create mode 100644 db/lock.mdb create mode 100644 src/lib.rs create mode 100644 src/routes/get.rs create mode 100644 src/routes/image.rs create mode 100644 src/routes/mod.rs create mode 100644 src/routes/upload.rs diff --git a/Cargo.lock b/Cargo.lock index a1a8768..a955c48 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,18 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + [[package]] name = "ahash" version = "0.7.6" @@ -25,6 +37,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbf56136a5198c7b01a49e3afcbef6cf84597273d298f54432926024107b0109" +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + [[package]] name = "base64" version = "0.13.0" @@ -40,6 +58,12 @@ dependencies = [ "base64", ] +[[package]] +name = "bit_field" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb6dd1c2376d2e096796e234a70e17e94cc2d5d54ff8ce42b28cef1d0d359a4" + [[package]] name = "bitflags" version = "0.9.1" @@ -80,6 +104,12 @@ dependencies = [ "safemem", ] +[[package]] +name = "bumpalo" +version = "3.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" + [[package]] name = "bytecheck" version = "0.6.8" @@ -101,6 +131,27 @@ dependencies = [ "syn", ] +[[package]] +name = "bytemuck" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdead85bdec19c194affaeeb670c0e41fe23de31459efd1c174d049269cf02cc" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +dependencies = [ + "jobserver", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -113,6 +164,66 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c02a4d71819009c192cf4872265391563fd6a84c81ff2c0f2a7026ca4c1d85c" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07db9d94cbd326813772c968ccd25999e5f8ae22f4f8d1b11effa37ef6ce281d" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "once_cell", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d82ee10ce34d7bc12c2122495e7593a9c41347ecdd64185af4ecf72cb1a7f83" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "crow" version = "0.1.0" @@ -120,11 +231,15 @@ dependencies = [ "anyhow", "base64-url", "blake2", + "image", "lmdb-zero", + "matchit", "multipart", "rkyv", + "thiserror", "tiny_http", "url", + "webp", ] [[package]] @@ -137,6 +252,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "deflate" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c86f7e25f518f4b81808a2cf1c50996a61f5c2eb394b2393bd87f2a4780a432f" +dependencies = [ + "adler32", +] + [[package]] name = "digest" version = "0.10.3" @@ -148,6 +272,28 @@ dependencies = [ "subtle", ] +[[package]] +name = "either" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be" + +[[package]] +name = "exr" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14cc0e06fb5f67e5d6beadf3a382fec9baca1aa751c6d5368fdeee7e5932c215" +dependencies = [ + "bit_field", + "deflate", + "flume", + "half", + "inflate", + "lebe", + "smallvec", + "threadpool", +] + [[package]] name = "fastrand" version = "1.7.0" @@ -157,6 +303,29 @@ dependencies = [ "instant", ] +[[package]] +name = "flate2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.10.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ceeb589a3157cac0ab8cc585feb749bd2cea5cb55a6ee802ad72d9fd38303da" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "pin-project", + "spin", +] + [[package]] name = "form_urlencoded" version = "1.0.1" @@ -167,6 +336,18 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures-core" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" + +[[package]] +name = "futures-sink" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" + [[package]] name = "gcc" version = "0.3.55" @@ -190,10 +371,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] +[[package]] +name = "gif" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + [[package]] name = "hashbrown" version = "0.12.1" @@ -203,6 +402,15 @@ dependencies = [ "ahash", ] +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "httparse" version = "1.7.1" @@ -220,6 +428,35 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "image" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28edd9d7bc256be2502e325ac0628bde30b7001b9b52e0abe31a1a9dc2701212" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif", + "jpeg-decoder", + "num-iter", + "num-rational", + "num-traits", + "png", + "scoped_threadpool", + "tiff", +] + +[[package]] +name = "inflate" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cdb29978cc5797bd8dcc8e5bf7de604891df2a8dc576973d71a281e916db2ff" +dependencies = [ + "adler32", +] + [[package]] name = "instant" version = "0.1.12" @@ -235,6 +472,45 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" +[[package]] +name = "jobserver" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" +dependencies = [ + "libc", +] + +[[package]] +name = "jpeg-decoder" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9478aa10f73e7528198d75109c8be5cd7d15fb530238040148d5f9a22d4c5b3b" +dependencies = [ + "rayon", +] + +[[package]] +name = "js-sys" +version = "0.3.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fac17f7123a73ca62df411b1bf727ccc805daa070338fda671c86dac1bdc27" +dependencies = [ + "wasm-bindgen", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7efd1d698db0759e6ef11a7cd44407407399a910c774dd804c64c032da7826ff" + [[package]] name = "libc" version = "0.2.126" @@ -251,6 +527,15 @@ dependencies = [ "libc", ] +[[package]] +name = "libwebp-sys" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439fd1885aa28937e7edcd68d2e793cb4a22f8733460d2519fbafd2b215672bf" +dependencies = [ + "cc", +] + [[package]] name = "lmdb-zero" version = "0.4.4" @@ -263,6 +548,16 @@ dependencies = [ "supercow", ] +[[package]] +name = "lock_api" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.17" @@ -278,12 +573,27 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" +[[package]] +name = "matchit" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfc802da7b1cf80aefffa0c7b2f77247c8b32206cc83c270b61264f5b360a80" + [[package]] name = "memchr" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.16" @@ -300,10 +610,19 @@ dependencies = [ "unicase", ] +[[package]] +name = "miniz_oxide" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" +dependencies = [ + "adler", +] + [[package]] name = "multipart" version = "0.18.0" -source = "git+https://github.com/emily-signet/multipart#8144529477faf92941206320395fc3798c14fdf7" +source = "git+https://github.com/emily-signet/multipart#3ae1cd313be14b94997405fef38e29a2edb60232" dependencies = [ "buf_redux", "httparse", @@ -318,6 +637,66 @@ dependencies = [ "twoway", ] +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom", +] + +[[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-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "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.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "num_threads" version = "0.1.6" @@ -339,6 +718,38 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +[[package]] +name = "pin-project" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "png" +version = "0.17.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc38c0ad57efb786dd57b9864e5b18bae478c00c824dc55a38bbc9da95dde3ba" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "deflate", + "miniz_oxide", +] + [[package]] name = "ppv-lite86" version = "0.2.16" @@ -419,6 +830,30 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rayon" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd99e5772ead8baa5215278c9b15bf92087709e9c1b2d1f97cdb5a183c933a7d" +dependencies = [ + "autocfg", + "crossbeam-deque", + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + [[package]] name = "redox_syscall" version = "0.2.13" @@ -477,12 +912,39 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" +[[package]] +name = "scoped_threadpool" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + [[package]] name = "seahash" version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "smallvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" + +[[package]] +name = "spin" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c530c2b0d0bf8b69304b39fe2001993e267461948b890cd037d8ad4293fa1a0d" +dependencies = [ + "lock_api", +] + [[package]] name = "subtle" version = "2.4.1" @@ -520,6 +982,46 @@ dependencies = [ "winapi", ] +[[package]] +name = "thiserror" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "tiff" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cfada0986f446a770eca461e8c6566cb879682f7d687c8348aa0c857bd52286" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + [[package]] name = "time" version = "0.3.11" @@ -541,8 +1043,7 @@ checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" [[package]] name = "tiny_http" version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0d6ef4e10d23c1efb862eecad25c5054429a71958b4eeef85eb5e7170b477ca" +source = "git+https://github.com/emily-signet/tiny-http.git#6228d7b6559ef13f6a1a3f46f6f2cf0b306a386a" dependencies = [ "ascii", "chunked_transfer", @@ -604,9 +1105,9 @@ checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" [[package]] name = "unicode-normalization" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dee68f85cab8cf68dec42158baf3a79a1cdc065a8b103025965d6ccb7f6cbd" +checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" dependencies = [ "tinyvec", ] @@ -635,6 +1136,76 @@ 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.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c53b543413a17a202f4be280a7e5c62a1c69345f5de525ee64f8cfdbc954994" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5491a68ab4500fa6b4d726bd67408630c3dbe9c4fe7bda16d5c82a1fd8c7340a" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c441e177922bc58f1e12c022624b6216378e5febc2f0533e41ba443d505b80aa" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d94ac45fcf608c1f45ef53e748d35660f168490c10b23704c7779ab8f5c3048" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a89911bd99e5f3659ec4acf9c4d93b0a90fe4a2a11f15328472058edc5261be" + +[[package]] +name = "webp" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf022f821f166079a407d000ab57e84de020e66ffbbf4edde999bc7d6e371cae" +dependencies = [ + "image", + "libwebp-sys", +] + +[[package]] +name = "weezl" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c97e489d8f836838d497091de568cf16b117486d529ec5579233521065bd5e4" + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 93fe434..19374be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,8 +9,12 @@ edition = "2021" anyhow = "1.0.58" base64-url = "1.4.13" blake2 = "0.10.4" +image = "0.24.2" lmdb-zero = "0.4.4" +matchit = "0.6.0" multipart = { git = "https://github.com/emily-signet/multipart", default-features = false, features = ["server", "tiny_http"] } rkyv = { version = "0.7.39", features = ["strict", "archive_le"] } -tiny_http = "0.11.0" +thiserror = "1.0.31" +tiny_http = { git = "https://github.com/emily-signet/tiny-http.git" } url = "2.2.2" +webp = "0.2.2" diff --git a/db/data.mdb b/db/data.mdb new file mode 100644 index 0000000000000000000000000000000000000000..8abf0c62912283bc270ca18478d719903f93fe0e GIT binary patch literal 94208 zcmeFa1ymhNm+0TPL(t$5+})kv?k)*(aCZonkU(&EcL)|70s#`-9fE}5F2M-|c!%7Z zd%t-zZ{C}kZ_WCz`JaNbdY|gOcUS$YTB^Ht9{~8B5Pu0v9Y<|Yzq5WVAN}zSyg&s0 zUh}u~$7<+5Z4mxk{;~L5>u>3gHL!o`{qJ_a^Z$h#$QK9!2muHI2muHI2muHI2muHI z2muHI2muHI2!a0+fxpM)|D5qZ-oGf#-`@Y0{!sWm&d2>z|F<37zp#gVfe?TYfDnKX zfDnKXfDnKXfDnKXfDnKXfDnKX_-7FK{rnvOpaFc~4*fI6YYa(@JM@2sZ-@^F0SEyI z0SEyI0SEyI0SEyI0SEyI0SEyIf&Xt1U;`He8bAl^L2f4ICT=Fo*7hcrp#Rsh-T#^X z@Phc4m;anS9PppoIPk~T;Q>=?2NM_X|LC@1{xX0Duz)d)F^u;Ze`P@g2muHI2muHI z2muHI2muHI2muHI2muHI2muIzzX<$yj{ot0Z50vtJ%9f9^LzL|O9S5rfTQ%u0XK4f z4!R2s;R*K+QYSj0Ab~mj>ch>(%hY#-Gao&*uK$?r5B%ll;XVi!FoQ7zdH(-byFvsA z0SEyI0SEyI0SEyI0SEyI0SEyI0SJNrDFptfKL7tC+W!B37XbSH?|6G?01uGF_+$S6 z|CIKH)B{2QLI6SlLI6SlLI6SlLI6SlLI6SlLI6VGpGV-o^ZEZ@rT!g!@L!z&|9jrR z!}EU}pb29cqX{zq|1Zc80YU&m073vl073vl073vl073vl073vl07BrON8pe92mi77 z|KD){zvK6R#{v9~3;3P?=vOfj!K3>>((m{1|Nj2>@AW;5|KDSLfyDp+^BNZ72|@ru z073vl073vl073vl073vl073vl;C~c>|IX+CzsLQ*$N9g<@xPzr|DMnPTkrRMfZuw* z?SAL~eh!ibbsnRY?~bl`j>wfu*?CLalp$!@W(m?_Lpq}SS|v8 zn7|GneEwd_$SJ9kbFi?pv2b#LE!j9Y1lV{5*x1S0xdb@)1-N;@+E6)vmj!GJAOUya z+WamZ0QHCbkOB3lJOtK)2AA%E0zNn&=FgHoP=x;n8TU{5K@ST04?m<}eds@BB^vMw za6h{&ioXs9SCs03Kp7&s(^WTYg7#Kh!O zEVSelOq9gLbiDLTZ0uZITx7KT!h9S;ESy{%4^E(vkdRPOQ1Q{w@Hw6kKjHWfr~3{7 z6A>7K0#}v-fX0M^!GyZ+0?5GSgnMXdC~#r_NKnu)uyF7Qh)BpNV1@ce52RqAVPW9l zV8Oiy$`8CAfW?HvdcrOakF92cK;ewT@iI0Sk@9KvCtUUM?^K+oF0YV~@gCz75K`07 z($O<;ar5x<@e4>uN=eJe%E@bJYH91}>gk)AgDfnqtZiJ~+&w(KynTXRzX=X`8yXfD zpOBc8oRXTBmtRm=R9sT}uBNuGzM-+H`NQYVuI`@RzW#xU$*Jj?*)MbRE30ek8=G6( zJG;jxr)TFEmsdZoAL{i`&)@0atQRJ@UeK_xFt7*@^@4)-1b<*KVd0*z!()l7A(%L0 zQ*gXQ#CaN<8yYvb|r@w%9&Cw@wt zxme{nXT<1d-v?BTZ9d2ZQ>XJ4{iwuW@z<9s(sf((HuWow#KskRd6XvkybH^TE_Sft zQ-%hkv>3vWvNiG{d2BbI_|2y{=kwA`oCui7iBzG&r!BQviK)WXEKZD?(aMQL6ffgA z#WR*uUkVwhP~@QK&+^;@^v^jp6(!MwQ@aH`Kr(Mhoz=X($_g=@73bn3T;+R(N@=uU z<}({AX)-P&AFfLTn8Xt#nm&#)9JU+-uX$<(DH#9PCP)o(k$LvPG9DvO?u$|l< zAsHZ0Pu5w5C(Ad^B8rv&BaQ#r^R1%BxSJ8h-X1BMZ~BJ^ceq|{rcjfx5;JmR0Ted7 zzJ$kIA+U2#$d(fB0dbW$qfqyfs=8o1pN*uE zJcj-7M0XNE_kc3pda`MLXih`yR#PEkR{P5;<@zh7&G@Zdm##qbmigCeSIMRMS9-;A zY@zP?l-Pl#jkMqU^J5c^>mF+i%#yFP!>Hq;UAA~uX-2O^1x|Q#*qo+vMlgkD+ymdZ z(^u~SSZhLz;7QfV!$ZpeHbw#?`L(DNwGr8O>mE%jk%jbhEXA3DoUcR)y(2x2)mdhU z-E$h?kz=nkNlGbjy(#!Ly7lEG^V|FwVCq}O!o&y0G##R1fKE?7t~Onh5$m@GLQZhH zPceMr3n>~CR7rxeeZ;C-woDOxTA|O2Kt{)nVaV(F={(D%Dy~N6{?M^wj?+vpd-;6V zPP<*I?4tYnu63T29~$W|b7G&Ox64nYDeZcDSJG4xVkU|dm-R2-vP|BI{8TG`>PIkn z-Bh;o{eViG!X`cDv{kx^a#pbHGnm+T{eklA%8>ekv5?DW^H3t-JAuEkUx8?%?I3Ay z$ZbVffbDk~-7Kg{7$}x^LrY8m zet7*9&;T8iwzv+>Pv4xNqaH*LavhL%4RUt1H;k$$clc=E1Lw`g9rU6%e4`r*k6)f~ z2_E7}NBX>q3OZe~eKXmr=Ra6;eQ|1aQ}g)>bkR%jvzw*3>f_fIgKq9gM!z34F-ayn z5y$tyOyMAwvDsUj#r-pkeNp83C_DSMnxW!*;HgiwHE6{n*_=n;Ii3JnC{M|&rKX2t zC_tdO*m}0H_y8%~z>U1VE3yuwYto();cXf7`nRErn0Y2(f6{&``r2K{KVkd4Owu$) z589715Y^wJwaCxn^K?zHy|Vv_Vz=2(1dVHL`_WKty{Wvf_!OciBTyTG!hGv@H2GOZ zP$R-Bb^(|L$ojf(nn*UR#sUNi?gZZ(i!4}QDPo+!x3X2!Qmv26aJ8hKvh+4KM(uxl z-Cj4EZ-{#j%-M*qDwMrJvvsv(xbCJ;CW}Y@zBmySLS{yLZbOG0uBGE-D$eq&_qRe$|stdlR!qWeDDJz1+zn)uKQ#PTq&~rLIWL?t;wE zdXgvg>I+-P(S}p0qi|Klwh;)*VQ1Z~(tUkiA2+Zk*kS7Z=%%r1<^_dJ9eu5tokeio zx(rF1c}x=k7%#LBQD%!O4Yr78xQ~+N?-_g$dS*N3p8njMJwJM=>TXotfP_y@zs|v# z+$^i7NQN?~uSD%FpD+XHaz~F$57Pi4Gd#?aBnnRGQE`?v$%}|Fqo|0IPX+6bfYQgX z>gS~bYD9f%VlXt>k5zkLE|m@Zh+#Of@nCohwYQWi>ncCu)C@DzY~;tYD|ruiLsjOQ z|8PQpZvKV^?eN@%J%RH4UGP?Hmh_eR4JTGh0NP?f8}bxW?M+v+S+wQb*z3=>Z!1ST zs*w)d-6%zOiB4IvZ`2!Q`tkFROIMYKCnH>yqBusxRx94U!N&>o>M(?QspQQQ6NVy4 z42LsE^(7yM$qVqTNY`EceqLkzm13||kyji$;%)Ecmw+d7Eoh9deLxMd$i}w(aT`6` zcAq{`PPcy$)zF&{E_E&CIaTcZ+bj{Nw&3Nr&r=z z1@k=+@?>)Qa`#E5shrHAtUCU8&a9^X-s-{w3K2PcE*%Be74EQk>nBsNRU|nr^J52a z7;P#-Ztorw5`D^@w|UbaWl13glhWg_akV`5PHO+tXW9N?YCD^2t;*B@YvfU;m_b7M zeN|}OkMKUL`N#07=<#K8AlNwB>UVJ?KtHAM$G1gAnH5vY%sBf;m3Fk)`a{P}hs9OZ zB)#=EDf_-pUGbxOwS<9H`q9QqgN59p;@9r8TA;~qs2n2h_Wy<*CKH6@m~e8Q-r8EsMOQh<4S`18kIy=;*! z<$hjeJRzHu?7Playv(HfP)X#-;*=Ro5;owyGmJn+tQk5XBPh(}JKaeyRa$&O1(E;v z%+@eh9)r!j9ZBS18ML&R1=mFUA|&?ow*-{ijE}ou+*JAZ;ut~kCW9F>4j)gyFxq$M z;J=I7g105LqiU`AkumLV4BdRft#HPC(rj2)eJE^d5%DkM!_Gb`Z;r^c7_^jOf|&Dytt>S-fhej(U< zxL|dC;(UHya^a~xRkr7co@b;zUN;zSuyA9$XJQ}jqM16O)QY-R^kTeZ_k;ayje*>| zCk+OgXoR0{hWg5(5F8a_XD6DU@r-fUzi9{_#88XM9*He%usYsgv%`#y{+w3ua&VX= zq&W{95JQKEGCBrjl(YV0!k(-Mk@b=9j5s6e~5lhx!J*=(yhPqntOsw@KY ztetrxc83yj01sKr?(m$yw_c`fH;wsrc=(G#W0owe6$uW*d%$62|KeTy_ZYGFDU(Xd zavK?>NFyRg#lU2I*8$OC*Z>akcXWZ%ok^}KLUh|qA!D|2-$V|S&6pk-ARjmY*0OEZ zg~a)%xWmK0JkKp zUlrDUNE+B>Od+$YZrI>1)0rA(r;Hzp7QV(bm`5E-;&|VIHyfk$|m-I)~gT zW*z&??@NzwqJJt}kxgV5+2CC`efuQDA$O7Tsz)twekBOnPX<@+EA*wg%xa2n_9H_U zeuU_b*jq+oc46SBZPD?&s*5^TsJjNXZ-#;2Zn#~kIY;IDZWz$dRyzlYPLy{O9?4Lo zHp>7Lst%Fsc5TpUS=uM7l;ni!JgA4jbO5dENmo=+?3ln3)k)`x?j<+RQ94fg59Iup zie_~u0mxF!2sO~0Gd1TaoJB)jCv0LrZN>UerWH;=la-I#lbnBOL~XGMFz9+$(KsiT zu7tN&dzHyGnY}S+YQ&yX00N%c(nm6>Yz)K&I0yMlf57}6;#nze+WzT)FuKp|TH}oJ zf{{c**DRFs8N=c|(69(|g_`}j`1rL2(S)C3&^4e?j*w~QsVlXXW3bLE#$ET?qc_v)@$i)U&aePsgt=${FFF3yvg zCDZTBa=qd?v>n9veQ`l_4Aa2#t;r#JM5+rj>xyANe&zfE;~jGgsb_T^uH(r^7~KJh=9Gt+ z)q5F)wZ%q6B+^Uk*f))^YfXoun6XMz2i-|!?hYaAdc03Agu?Ka&!ro~5(bNQbo7=# zzYAar*smWhJ2sLokQxoL8}-VJ7nO*;2Xbd=JfC#Sou|+nN9c(_t-GHU+ph(U7IFx8 zS&AkK3LSsSIgtJ?oqBz&tz#b0qLLb|pO27DS?(Nc;Q(FIPty{KvNF@`pQrFf0srkD zEBF-e8@(5n{>-#Ev{262p_i_M^=4@rTiV;QkF=9}uOS=t%z*2wx6q~frCtNXol|`fP&bwMz5>}eF_yc>KyD{lTdBS1iW)`zcrTN5cbT2Ks>nPK#4`!n=m@i~GCwnTb z84G0;&WabAgVJhM)M%pg9vH)`{V6m0_6U4_s6|dbMxWjk-2q&X`UP=?+VPY^JD~mvMs_Yg32&$@k6BviZ^5RPIGy zcK8L7o^L6YoE_IMuVx9t&g|7uL{Y4DG-L#aJ-t7gGKo?8^I}FV!ek#;36yX54;)_O zs!=Ub_2i3l19R=BMAZ?ku)JQq2>%g%Jl(a9fZH@kWPB6QxAE)Q6iaTmBQB;9vw zblG=Ji#(f;+*AB9n4R1tF6j|T3~dx*skH81l7q&HqxB=)UR7{HL&+QBRkV;H4+~aI+ltF;OiTUsarP zn4?wbGudy^07BNEITgVgp}Mk+dJlX0K`QN;ETI?fV$= zHaL_VP{~_ta#Zxg`XkV^hHHBjP9KrNF=8uri{q19F10k796L$EPjqpVsP6ZQ#@P8M zM)STr9~=y&&9YVDkY?&?CZOG}AC&r>Zr|Rul$S73!;k#2Jg~obv2--u4mbM4&-RHA zT37Dg(M!(^g0|oF{$%9rCKR;B)Awr`pk^3>fr={T9w!`&uhR=JrYozH!%<=ls>BUp zmq!9aAw+bg?EUDTW3jcnq znm=tsRXU@5NG&7rx;CEeg0D)>!d1RJVnp?l%n%3JGl_lMo~pBjVV0eWBl{%+om%v^ zDl>RGIxm3qdHR| zD;W0xNWD1Ft~zOIG1?sFde(GgAXRJ81zwc!lfDd0NZWG3j(C&NHl?kfqURc{@WxjZKypKD{8qa&6j0lT-OY!-jj-Y<~oO- zeOK@SmgJza_#zyYIFT~86Fmf%t&GKd{mArVoU;wxsV;sQ&x8d0B8L38Ai1P##G`t9 ztpl#_1sS{}G7bX%?$H4to^<~)KEZ@#57auL2|27DbBjoyCql^QRYR3ghzhYSiRX-n zWT4xYz6+NTQ}hl-Hj?8khUxl`nZtYCbXJO#n0*GX1YuM@zVMDi%!@$G0}guNkBa4u zq<5FJ<;;VK`#Ew@|B6amQ{!;Quv2Be&zxwj!k9JC}?lLv6#+D3)((Bom`V3IwYe4Zb?keU4D@o8Qmnk?N0=nnie_=m{sy-O z15-$9cq=l#^AkFAteioi)y#|zFmtLTT4hy$T5PV|c$90`YjIBu%%0yk2! zXm|E#+L`fIeuB!Pl{NV!fhVe}E+OtmAJrGUB5#|US!DY+Yb_gj@eUdVr_RdaNL@7@ zw%nDDp$U9uP=E9dR>!j(6!Ci_oRwQUPz7sXXGG;(hiy}vxD~02IEjEr_EM$}^J3l` z!biUMPlRtgwESxP!()===mVOJnXfur|A8*yQZb?21;{b8@9RF@tD_Bk{?!U*_yUiH z`Dm4N+(Ku()GA#fC1spg=aXhK4l>mjIw&NEf~&;>0}J$?33M=VN{n1*L~1q@EY@i@ zSNivW#^hS|0p6^~9+QjxC;R|bgm2hdM^Ij23SWsA`W6ZsT#7}%3PtZ39tM@)0}ftE z+w8YP9vpTEZ8I{o_Wv)X^ek?0zOpX#d>+

Xsp~{(Mq_q%KNN!5->E(0OI>(&AGNZ`Y-Fi-mIX zs3TcRqg@T@&oGV252nZ*)eNI@S8i_Tk-Rde+~P!R$CB{lq+=N7b~CAXPEp;;Ph7o5pF#SxVp1w-ceM6;r1sYXi^BH4(2Q zOb5`kR`om+@Mc4i+X|^kH59ptTFRa%@76g2Vt&8x3j^35UUF*?lPBTbFH`ZZM z@?6d>iyaR2kbCU*_VHz7%i9;;rch$b!L+nzx3+>^nBMxWP1`E>fWfuaEn8!906w1v z5KmfYc;6h*UxanHf1e_JnIxTMV7+hz*E3;TlH^NVq28Qn zF3O9xCp*vlag@9Dd(4|Je2a27clAkbmM`C`OA9lOZ$7|FGX4EL5pJ>k0lPKJ-!xio{Q9+*^8@ut! z;$TCWWWKp6x6hQ>+G!Z1t#Sr!dIv_$ZAPOEJULcQ*gCweB=>(4C^}Mcl{GfEyU9c5 zLnV5(jc^WM(sxFNewAZPgw0T*#}=`C<~)UY=QC>^5NfynRvz<9K&&zLO8Q3QM;^}p z8iKwrUEia>+yn9#g9`lm_&;ZW1A-%CkUm33YqT^jPEOk5a6^0ZdoxnyrQH`V!seoo zET4QMC*AsJv8#=hUht@As65BqXI6|)RT$2a*Iun&fh#rAy-?*gC(a-`CdL!{Qnq5k zI<;`ve8Ie#C(aM9Sc6_loN~vyt~m!TFD_yNYA$E$G4G`^${AEK2bZ94Kp0R)+Mdlq0gy?E_JODE>+#gLi>M~ zMS&mDj%m(%y4rZ2J~JflXEetU|Dw)tJzJ_w94mWbqmre=Y&DoEl~0bzgjFfJ3=3vu z(HvY`o7^lWT6~t*A1AClz8)ikpX$lKoiCKdU}S+~L@nvcREiVvN(uoLYB}O`#HPR# z6kl&kJQ;O1-oKfdF{y$}J6^kOLHg0AZju5OJ6?ltt3oT4vK!n9>E+x{pTlUa``y~e zjaf#)Y0;7>QRXq(ii$~T#9*+0MAM@}7_n)s0N=3=w?tcuKzSs1gkOo?(oP&XbNur5 zXxsMnfstDq_E49v`Aj34tvl{#zwHdZ>!7rQR(libo_5M7MmbFg3qz)&PZ?k1Zu>Jf zW6FTQJLRBm%+?f_+WT7tty&xx4r^Wp4mRgWgjnPHtJO2Qwc$mmq|MU1^JI>oT_^h{Qk2{!LX~Y8^9g;sJmKu{ZnH)W zI@;zaS0a8XX)D{fU9jd0uJzPP7`qWIJ!&VN&B{<4afbVsxm(feX`>#^u#W{|4tA{M zJ<1)}NzG;Hyna^;%*lhSrD| zX&v90B-lv=rjeB8z$ZP~_4r}66m|rXZ3dQ3PiTQDjz$FGNE+E4HQAeBXZfDw=6ewCpK9G;~QZ#)b^&@Fw=y( zr$3|_!>eQ_6UcxsN-s~m%d!iad+#Ji*YxozU((1%5y)OgXzo0r6sd1_X0nUGrzc(G zPB06W2;|9DQy-9YRvaIwEQTcx+A$7Yxd(8J<_+@cPb)^3LZiQ4`{<;%Do4QcnlGDu zblBz;QXG>nZ)rB_B%T_eFJtGKDY+2Dc(gMg;f3{fUC7(TDL~}hk&kb+wez{qnMhta zO@65X`Hk?T??)IH#w0JvJ3M<-l~d9L44f51spK$szjri~iWz-=%&AU56Qs z6s#YD&2|*#`53juZI^octlhz4CP&xTMGvn$YME68?w)6rE#*cItqm<^KiLs=;eN6k zfiDDGnH`*|_mtN@lvV${)_g3ATlZQpr?DmT^D^EOgwwmkS>;GR0u>D7VrdtTJJMD7PeS(|% z;+Uw*lfKSS3i%wGF7VE~%!7p=q|>~JfugVSM6b|WGXfh&kMSrY!(X%KyK=S-ifnhn z?QEJ;K53!Y=$`R2(NOO6j2|B&KaHwZ)sYS2K2=Dy6#WOX_m z;~;OIjLGSIwB*Tu4_KvvdLvq~Ep3^c%j0V+dv2KDD@JUU!u$+7-r5RMgBL~pDC8q`)-ya*k@)uIcvw3bm0g*sb4RTj zH+GGqpn9$OT}2F z`prflJOm*q?INHSsXSG7#MRiZ^h!nTMRC(Bt`ih{rV)7X5uxCkIOVPI_#pC8s~~Oq z_Z!u4@;cZ4I?{z$46@7p8B`J6#6l$Qea!0^6w?pGSWC0wby;6&U%ccjQ`BJ2Kbc+4(u~Rap5xsT4?JlOlz8ntWn*51#jvJ@EnM zqyFpx3bt-}l+F>`8gxaZ_C}A6&DLZEW7o7=JiF=lH@ELaeBT$mUXEEKe*rTRfIL+< z=B?S(6el$zauL9SCgED5)QZgt6Y*TL+PH6Us903ruy0(|NI6)TRjxk5uQ2Gji?Lz= z1;b}!E7cX#V7%Tu2hl9i3g=lxkYaqia&3!lh64$`Ynlui%veq=izd6K_?McW-`2Dh zU=^8y78niP1O^Uh1GwpDY7oGWj6;>q1lHu?gl)U7mr^}07AmIPWn{0TyV5ofh14pP z?}66#1Ps}c(G6{11+T`(Y!N5#T(4$D?g4JThMD3**Q*-=FPxe3#}~7= zw1Qk!59gh%m(GTLRP#84zoagi76zAeh3r%p$W$jNaYgt?J z?P)DZpO=`szSPTlzD7T)A1o(!StLmKvY!@5*G%t{-NwX4?0TZ{DSglpvNkiPI@DV! z&@LOM$j0dK6!LWRVOa2vcp?%GjfOsP;l8ofkc)>&-E_*h6JqJP?$qZ+H-560C6uBc z5N0Z`vBG$p2qWz8KHa(7&_m%5Yqb!3d}h8{s0>9pk%HPn)XcT#F)w0wtmR3Ii=kUP1*LvEH|jU zFUi(&$B*?~}ofRLz{lZlxv$c@|-WNGanOmWoGPC;&ME=-}rt;D9}BmuIrmi2J~Y4|8>n)%q8 z@taeKijWI=33%B%J^bJ?xtG12gR6j-FvTzD0$}+eo0Wq6mx`OMFoiC7@|}dE3y7SH zg^Pua89WKlgM$L>!^PY}KwVP$PY>X4!W4fN)zj0H#gmi8(Z!OLou8kdm5qaygM%5Y z!R+eo;AY~*?BGiI+k+&?)y&1($<5l)f&9UviK(Nzn=l1<`+pS7-bqR6FUS8T1AF_2 z(*DwRb(8V{8~=O6Ts6I&K&Bx4F65U$#!}E_S~vU~a|=vIE(J z)m*{E+5g(q!<@vwv>z&LX>ITH%K}XHFD&Shq1X3E3++mD=stDA{~ z8R)?e*fWbY*a?V}kDZ^J3&hONWx@lt=VE8(=j1hK=HoW!=jG=&=i%Zv`E9S_Vh!#_ zCU*Z=o(DhX4}Q$Kcsb0>ESUM(Kp4iJZ#IR&}7nSivT zi@gcB9e(|mHLHyi$dZEmftP@|s+=$d2MgPuUsdf)+$_Ml!W4?u4(?um=4x8ogEZVs z9>{_H^6>EUaP#wVaPo5T{kGEvxwwM+%Y&<5b3%U=UO>VHWa8%NqUq>pCrt6zl+i!d zDgF7|-jXJ6Ah6kAGe|Xm&md)H;}l>6`v=!mK*`bE+QR$)ruwiK{c1W{Ygh1Qz5m=J zG(gUON_N)dzxtzqiP=N%7N&4D@c@AV{gG*AW#V870`I5bzVb(o^}p40+&mz23w9G` za}HBBW-fMKer9l!ae&(&#LdTH%5K4H%K6*g)zQMu)5HZNZVB#3U;^M?^lQ>AIo&V1 z^uP5ytw0axzzmq#c$wMQe=*<@VCVaj0qZ}p{2ZnhY!-ZG{LCgMoTlI|V+La8<2N&5 zW-~D{;kMxAHsRoB|6Q*CQu)ENfd8@ltPh8hKbnH|e{ZE<>;B=e1Fr6`Eby`HLHz5` zC_)ZN5CRYa5CRYa5CRYa|NkTK_uvZT0KQW51P`6=SE1j_Nk}|XRacRgQ*dAFL!b}< zOfoT8I4+v%65zVwfb+>M|3NnW2ieTZ#U8A~1=gW5w|4;hht~Z= zHh++PA7pzwcW~K$rH2}#f*iCoz)LFdhZv9nwweWY+WrL0?5D#3iT-fz%AY1 zpHhPZ+2R4wq(0Kj++01aJ#=b0u0 zKw}6vm~76;#Kq*-b{=BJpe?{bb0@_BfTjlk*kb^IZ14v+@VbZfK>jQMXo6WO4g)}H z8UWB(f_)qQAL{)OdG{B$e^=*Ee!qgxV4$HN{=o?r{11ot5XJ?EjDP@-h>DDgih_)S zf`*Ru2n`(*9R=kP-XlzG99&#nRE)>?csTf2IJh{!g3n;UHn4C=aBxUCXeekn|KW81 z8612DB?c7(0|kz|`W<{m46YEu!*29<`W<`*4&fsD9m4gm*53aa2lWUB{CXp(P2YeV zdM=&3o!TJx$zugN0rqYeq^J#4)tE^W!yjenx`X)O2TOJDO82O4A*r^M3T-fAqF;d7`B9oURlj_)n2JkayB8K z0M*~k!eRcn$#;O-B%qP@@#eL$dvcwXSO<0Ru$#xDK7ywc3NBh7=JieES2N6Sz&DVG z71_!oX+&Ny^X+r38Ehn(h*T$?)_M(lD^VZA=A%)XsAqII#~Em27!iC*=*+UYe9xXw z7;7?m{xeTFdAfCnPC`8G%Yid5E_EH&Yx=k)gCm)f3)I!t4sEFV{?$(>S8J#i$7c77#WP9QC z_Ayy@8GBnDucXF=EoC_jh_A11lTl8LgM=(;tL_9T-9lapix_EnnF^QHzF$|Q#14E+ zBv-}92On;qivh4ZlX}CI*tJ>eVo1pqHZ_;tECIH-b-A+&L<9AD=cVs?-Vi4Im}^KX zV;ujnWbiDFe2Ps@g8wIrL z_4Ei2Q3o`iL(?^zpU@`Bv!OF&S$$=R+VmG&9vod$G5GM-%?v4L9-M<6w9jSEU5+!!9F__PKS!9E}N32f_O zr@*jn?3-XeWhHENGfjS~Mf$LTr!%A(`b=(#ER0kNY|f9|;z{g#0t`F3Qtag#WSizj zIx$+dMyJI@QUzpf%u1(~6E*a7nX{B?O!-Y)qr4}7#OXuL(e?Ke8&oQ1kjd}?=wu#lmmvcZkpr|cP&rL@H=&FBmK>c+r8ORw_e+I2?N z*AtVS__avNZTZU7HJ`=V_Hg>?ApmYu;OrS^OX_t7uD~Wj{-|O@vs=W+J5Nz-q+;>& zJcCIY#lnIhQ|edx-nrCKB+?rBr}D@refk$hVMocnj?z%bWCka~xFLLJUON{(nszPy8;SF|swA?}vMH=l-glDoBz9QGR2)*X-Ev#RhHm8?5z4N8r_DwC8jPO6@HQF(K%1+ugxhBAq z#l-(XNMN^mrz#R>BVk23W36iRv{5DL>62?L@lUf_t`x+j#jAx9iQF{SS7l6eD{fp$Un2X2KjqR1bKRfYoAI&_EOEAJBv<`!R<>8iQ{Ng$Qk8@Hg`s-TR zlqwG_tDpUkNJD##Xe|*E-1tJDqC_p4RVv%$G{K)jODQErOJveN?g7);bRB4#_b~vp zBtNE@lTXbmnbeB1ie9$gBXKV8LHBNb(3nuWLcp32`(kZ*4i-H5{JO8d`?Vt$3A3l@ z;;QXD&7&F8c~Qaq&4b**NI>jV0=c?{jGh?ot ziIsEW-L}MuMP{?}WY+Fp#Z6rQ4%c1;RA6!g$_0j;xg1zFQn^0Z)HcwL7fN zpQ;ia{xJx=l|b<8222UJmfA6x&s%R) z8#}*k9qr+}?KmKmH@7i|GNPF%j~BThZLv6|mxa#jsmHksw5*%ecIKAsJrh1~@Dku6 zT+dO3HUIF!>ZnX7IW^!lEEXUD1z-h<2R^*I0NdII?i>C!D^`4dX2*=#{_5j7P3=Hp z$64ZUSY1=pg^CAo){!M$JDEmOn{`k|cN%${L)}Vj*yr>#^@q&c^bm6?GtZDR> zaJSs9!CF=j=~JrF8+@Oc+NWZ>Gam^27o8NC?Ay%gf@>bRWq;j_N+3HzD{E$klB z6Q_tvU^U>ZeYUePC#UzRoL|K=8n2dZn!?3v=7VMExo~lEf_0Lkiuw_YJuR;wsN7K! z7GP7whb2Tr1>n~NV`_)p+_oBarg{^H{GvHoX9wPsPjAhoM>mbOHAtFYZ>6shK-0(N zGMh0o(5#@hY9)t|9h`99xZ*3KC$gqaG<6oYd6SN0v)Bln*>j8>k(!DZWzOFa*QW@c3rLAf_J)CZ^~8gLhb1{6|3eyN>f0SThEP zq5`BpzLVu0j~6M+Ba%l6w)#(^`G0y(In&yX^Vz+pzRc<{ly5g(nNFhoYSu9}VS9~S z)uW`B;cwungMXwOYBIs%Gb1F7Az>4cs@M45NWio4lsIoE~d1ICIGFD?e zXj?N68nI%B{kqaGK%sP4&61KFr|O*5_2HR;-4<}{%5!!NzP?XSe>1euobY&4MQ zp{B2V1Vj@TH$5g4i>y?K&?R8N?Mm>m#r5XQhJB^26Z!D%YSv}EP8$|AGkA1Ip)n0Q za^*dj3?!}JxZ3XTCFWLD%1z_uiLH@DjuQX6W$t;}K_@U@X`cRW9X63(q1?pCp#RMB z)DaCS)=U+jI2b?$)Z7)_=c~K-PWV|XHuF~IbUL7OXnEb0t|ujL_#An^XfqpV?2$Sq zSo8ITX>#PG$#}S|Av4R+9xQh|ZqWg5b*CoM)QoW@pEoQU^$wbDxR*R#Gg|o4KFPAN zu?^YJ?|lZj)HirzXoR!R4pYdyiqo%# zZDKWu|CoD!rR!*`ZW;cHsaSDrMRuD$?(_c0IS8a|R(kr4+0Bqp(QcrUr-HZC2&Y#{ z@gOU$B-MHQbb4Z*x*mRXaw!;CUnq8f;asjKB0-Y;;to3{g$uL59au%TKMPH69Tr zeZf;>FZLXhYHD5CKy{_K^!@io3eP{66h760+99@rf9q1cYP1C6$=H*gp4EE6b|$$H zU+8P|#w7&Zy(Fzg-&(22^8H9oevHS9NNoJ>&l8l`Hi#+CU32k8haa;lh*agM;-Rrn z7dX|73%0(G(R{`#l7V#^CZxl{v|?&}#w;ZP`l1}FZ?U+Xm=T5f=6n913;>Tyjb_JSq-xYECX5G4L=L5v&CT2Yp~v z+PlEAv2LNbXDfGE{CL~oX(roFKwJiPa4fQiz9iBo+sY5N#Wu6b!&QWMw%qUNxZ#;>0Xz#~(xJ11z)6~!tWDqm!@I_+RO>uZqHdBtQ7{h^4 zL7|?HWmb_Th{Q${A2qb(Doc0ldqzK9I|irvlb^i(E$u6Q;}Tb90`cGHmho1!;MA9= zHqw+4B0_s#ubz_;fBKyB$z&})bNW@0TBgXHXGLwX`A}w3Cf|XF$juj>#7QJ9nn@4A z%CADsm^8imRQP3GA<^q{yo7yeB8*OEb_9JP;fnI4*i_|0=)BLyv{aphymFSmScrFi zSIb@dqC2*F9QX?PRqOknZ0bks7PhP-%PQHSGO0+9&#Aqh`Ww#yGu_7KT<1=pQp?o#*cp_>VbwG7AOtW!P<6yAwttF=cBCo5O9F#+QmBmW{%%qA7B^LND z7Qj2wySWV}VfZ>Rq%u3IBOVcX}E21L~%IvSlIb&m*MV`CDymoKbLLC5G9@> z%2teRki9VwX7RXp)$SW*JXw@BKW-`h@KC$a3k@y49|ZE$ZwnK&ZNK!U^z~jFaSTLf zN7e2)T7U4Qz%9I+0#5_O|zCW!OUHjpdT&>Af367| z=~6khjR6)qbhfg9x^BnDyj9aBqbEaCSUW3qR_Mu>l)J~2dEe*D`dq4|^z9zK&UnFu z;fcsis(Uphh2M=6B{aAs6E%C~8Qk|_!QoBAS!zbC$c$X0O71-n?3ZR-la<5(bd9H(Wf^C9blY5QS7~&wa_~6MZg7VESCctcLtVGpoAk0O;x+lm| zn*2q;>32Rh4CGlzZw!|Ym`Q+oMJLseX81qofLAZ(5PUf8_(<` zdIPgyjh~0$&dD2INz!t4G$(&o7SOr;q3$((5%-e*2~p*VLBKtL*7I`#g}@V*HGlmc zXx4m|e-Kcxo_|pgpla*TIKHsZH6VQuUyLWpg0mo{jL_&Ar`+v%^@<~5*O_QWUjz$T z8Jnog@aaAp6D`y|fDYX$2wT-k@;T$)<7x0B$7PRe-0Z7Hd;tvrGkRjB`0#q+>zTUbX9&!Fh7!x zdK}*$mlXd1!S-B-`;p((19|xSww;|brgi=)&4Ij|LK|XdOemk;)3>emteE!P>BOJ9 zT;u-$cMta5XZUd#!^+&XVhlAqR?g9h6Pbr__|1^B_5l<}iOPmXEdCqvs~vF|!pd2T zjZr8vbRQjSq<`Gt!Tn+1yBjQhrbDG0r|AC5C{vp4)je&0*|Ki@9eo2L>d{Qm$f zA6{SfaUB`Eo<2KA8&0JxHe_;U;w53GCvG0q*6Nl@d9OBO!0?E!YKVJu~PrW5Oz{9gdhQQd7?MA|}L_p~yzC!NVfrcqS0cwN~)64W4`8RV@jwk+g)=Hs>v>xc`syxw%hwm$cIQ%3d8W*xi-e|>J%IH;2A6;vG2>ZKAbQZoP9fgi^JUeTR}6y zCngP_CjS7{*N&5CACDWrj>~B zL*J1ul1AiT8TQaJ@y0Uv>&8tenG(zU%Tp~)$XoY%zqTnmRF-t4n`&DOj&(fRZ!`-^%PvB7ylb72dUHM3#wDX@; zeg283Q&`fL-kwQ2+s=sghE;lR*@ADT8#;WZJ`B}dt_I#5*>UD+_|F@g%Zg0C3JQCiR!bnH)!6p^u4O(#hK``TqcF zy#D}5&ZDsEDI>5M-VXcXPuY#)d54PNT4zH0{Tu6IAf7jY;F2j~%2&x@WTr(P*Y0ok z2{|hDrL=gwP|=Owf5VuYUWx{`A3R<>gl+g3kern1w=YQV=A%=s1q zR5GTWe2pu)rmQ?pLz-?t-}1a(_shpd`ZoO+sp?N+^aIZmdg|Vnke89ybHDLAvmr9% zmi~5GRh6K+(fDp!9Zv11TTYFa*-sC@H+xyb+y}8nc^vU>C}EHhxr}9#n`e4?I&k-Y z&$D6m@vN7^zYW-qR)1MpO^&8WqD61Y%+61chs7=&ziauja}+hPR*Z=43oG}2EolCy zM&6{;<*&A^7f-t0`W$P-j+92dz^%s|?DI%^@E>DnaR++T4iM@BX zlQ12#?K)-Mrt&W)kx8`jDJ9yQ(vWtXJCxDsXG60807|pHTO7U~*&o|sZ+t6ws)rZ_ zRg;ghY)H7l{MK8(y=iFlEQ9cKh4WVSpM*cj+UZI)cdN*Q_mCMmliz}CxEhq+)H|am} zh#hdng;V9a$$Rs*tbC1}swqdL-bevuM$PxT}8pX|<^{+y_4S-nfs zhRDN{%p1iy*-m?LCE4dFs@GM6<0gHe$Jvn%8p$T(0oxZJh!j}BjWtNw7siE;H;=BQ z<&=R-%~({t%Hb(_CQ3y%Ld}KUr(<2)OzGKgqdIi!?41j(^mjyvQ8!6Yzeibm-$jPh zy*PvGk#cbHlrT+XqvP2V82FW?Sy+5l5P=)<+7@8T zNgQrx*e4Z=^}w=S>U+-UZo2tK%2{4U4@X>^aoJxhN$sl^JK{y{eU|srYia$WZ7rrG z#F^9nr~9{3Eg~;uNZV-H3V*as^$_kq(0^TwxIi~ zvFun>qvEg)wjWB*{=g*9>Q>qmB+R$m(mL{bc?3~f1b0b!9oG77J8d)TtMzf{>7S0`VFn{JKyi`r|Jncu0RsU6KLJ#lI72azRha{2Cp9^ii&1K%=mI<7(oBHKZrEh?8p*WSkT-)pW;&axpnslgGMYAjlErAn15RJ5Rm>O(5b zg-NAo3qbTiSCuM6Ygd(uo%|^~LPm}X{YMZ{?F>iS@pzYfqlm=*;=49Sxr99b0HJ8TgQ;F75&4h( zM;V44UKi?ELHVQFxKa5p)$qj7M;{pYU#dnw`laorH=swi3H(#LUFYFDwIxTiu^(vT z@dxD`ROj+WX-`)zPd_Wpsmr=?UBj1$a&%>>+J6#**at=4v-YiFcZ2D7mRE>+bdGbZ zuKCHNlDj{m8J*O$tu>U_-8#Cb&nQ0YUGvY%)=+Uw^Ze3#TG4uzu$xZksC<=iqFeB_ zt!PR15PTmb$P#5z|Atu9_hBPsY-et>Zu7khr?p|19)G`Fvzfpf9~D6{3e z@FiA5(HZu$G-BWB0ceW<07M-<5du&8h^@FMbP2v;YL$8eXKxC`-=YSti1~OK1~~K! zOOa!TT!<~|gia#YbnB(uxd=%XpBZOVSaj-4i&(orSyD&c?6t(7^$}YzN0kY<#?tSt zg*9G5e^8I^Fy=QOAgtiC8435@g*UV&Hry1WTWxt|XdMctHbH7Y0Vm~0%x2TZrVS`J zjU_@WP(XaC{mKDOHh~{Xe~kYC%0Hc$L*`h2@$3yQ>VrV^f2bSeW3wAu*o z1!`*=TAWzm1+7tziaBb-kukU~^jQ88k%ii+Hc$ZwOdG%pyr=4=-*a@k!Bhe>S`xTB zQt=M@PkW@yI*o-O)#q+_W9L{yZ1jUwXWOmShP~`=qe@3vtlJQ99HR{jO^7%n08NRm z7Q3e39T#-O#x%6rbWz&jH%)Smnm~>grmamy;TH~zya5+j&ND`eb99=@15yQnJXfJh z=Q>ooR9+Ft=5ue6m#{d6t>0@=T8gr){{W(YxgGsv-W;$8(H!l@N3Ra?xuVS}Pc`Y) zrs3Z1tHS>PE5iQ(DcE<8h=Mx$Q5rS&oG8)@0`5lYT`~h@sX1W@;+#g{sna5h3|q`~K50NAg!3!Al8`(vzAwU{920Hd+Ha*@|w7oBT>5 z_+ZR(giK(!AlRhnp3M%ekgze9rGB?%q}8k(ZF%#lnW91hb{*|zvy)swv}$3&Ocj4s)yoEj-$u^ZRn`w zg@3}Gx?h_pJC)=-+LW}OGiSLTs|0-O4sDs7tztV)C^#IIH)>)f;2H|t^U4jirD;qg z8w(eSnpTG23PD>6o36-Y(ZNzm8s0WR{y?!^GQ&V7yI-fb_a~X~++Pd5knw0T95DXp zE?A69ByNy0!~4{*nGXxXpS9tCmEoKAyfYAii@1$-FAnwL+`KVf8~I)v`Cb{T!?AdFCd9^@3lu!L0!Kv1 z0;-NJlST37s2<`xTchxcnL2i_-A#k8_bK|V*nha9l1{IWc% z%Z0+>1XH@E@Ky=j?Q0WQgF>p@LXsZvQhZL2u6T27&y`AwrKc#_tf?c@(PARfY-ZUs zwv$O|wKVRV-IG{cX*IeaQ^@+l(AO@O95viqO7e1#uxNa#odoeQ%pJFo#Yt|?Os9FuXt1zBD>Q`j?()> zl;O3omkUl|Z&XSF2^i+nvUCk{I8UOyA^T0jv~@wDybn(jc&z(8`A`-%UQOXvWT5L- ztysZcv>9pRP6v-wIrG&*@HDAhugRnXgsUD z+!0cHw_xTHvq@5o29S9M!j1aHsmD1r#k~=6cVASo%@%UQpE{YP?cor9RPWSlW*F3g zVNaK3tH=dN49X!+Dci8}_fNGegViWKs$X(*ip(I)njG@fwm`8!ooY_ za6=)n%MsUnGW;B_ztFl&aBxqBuH_yR+!!rA<>vN|s|M|$S_64lje&5fQi^NxyVr^! z=GjuSL#cI5w)9>5lbxhlQb=Sw@J{KS=V%?J6+Xu7q{(#fP&ew~-T0{^5+_1g&QEw? zvZRp7=wO||-VoSP=%(1!Xc3uAl(8~E?w+7=2ErJF@}Dp*pro8Pfz>(1#?J84?uf?W z)jl(AKL*c+97DN3LUe%IOK76LCLi zPQrwZKPynpvKDktkh`f|D+8lery&cR>j6iBobm!4oimRm4$J#;ZUQFtO~XXrg{7mQ z%r>FJwX*()Gq^RO=1iXERH%J6Xg zArx)s712UO4n${p9KqfY8I<6p_iP!_O>LmX-G$v+5TOe0lVL(8{HQboL@FEPY6S9=A0qH4?Fno%KG zRu-VOElW^Zk)pLNNRX`uD+$9pLCaFqwFPR||HJ?<5dZ-L0tEyC1OfsB0RR9100963 z5d#tg5FjBj6EHzh6e2P}VIx8_P*PxVV*lCz2mt~C0Y3r%0KdY5tShGF(sJoJbX>Yc zbV@X#bkk{1DAQly~n1EcB(S;JL^ZAxK*(Ql(0jE=9z0QL8*4Y87j| zmD4`gPxiWJ+OnYFtKQyPn}}zfl+S4Q#~HV@=Z2$u*GaxqM_{x&mCyjoHeE+E?+LCB z<50=n_VJrh)=_zRhM?H*8eUkNRt>MxGIpccXBmWbIl-bzhgC9oQ6GTjhX^X?Q80L1 zi5dcSquFL*9(1|EqDl?w8Foga2E?HDkszkjYxp@e;rq23%mx?8Ww$So!kvg~U1?0J z+MFR(rTsNhzfI8z&Vee8UZ|MS61Sj#`CRd#`j2LwhiE$?{H~M6c!+IQl}giCo)#DG za8+Gn2~ogwgyeTknhuZZbU#__Go1%TLSE|v0yfVO*SYO293scms2vdrjjK)M$0*aP z?p10RMS(T7nn_&;TJL2FHCoFhK7*V=2ZV6$ak!n=NSby^_|hPi(B4Ty4jbhIVjk`u6{aTi)G{rXN^XUb4L|h?nhpM^bJX>Y5i+qKH2H82gCw4$qm12zB#*ZqO z_Box^KjL|5Sc;7U{{U7Qw$mBC*0hy{ZPg^^K?O{N4#}g=c-C!l!#e6?I-r)rQ0k4L zP!4Nh*}H@|PSzSq4UCJG%28Cv)klM)5ksmZa;VTRch#d4{vxnGa&w_xwkiVM+#u32 z#Qy+{su+PtL0>p}Upw@UHQtxZf2H#3qlvD*MPF&Po=)X6gyb}a#zew%kt$Fr#R921 zbh5K4$a48~!ugcf&1a->!_xWRrSo0seA~<7~kW$UD#=b$^_GNg+{w-ZKdxF5>kXXugK2Xxr zlbsr8y1^=gEM{))1U6HU5`{2>0LY-RRJx2-k(DqLnJ6)Eft6Ff!6iMukYb@u;~E}^ zVl$kjH;l>|VkubVVhxO*=tRl{giQBKNJDhS#zd?=J6k#&hjLV^W=5*ay{P>vuIOIT1NhrfNC@V*?ulSDxrpwMvzUq5YbV zd5r`sH^{h^c}!ubc2sE!LtmcgRQS+z)}cGKIkDZQa(FpYr01QLDngLkuzBu|m;hOFf=sk)xv7!~QWq;yk z70%^TpvEgow5U-k6d|~U4IGqS)zt%ly6!~ElzVLByy`7?DcC{b93#=g)E*Z>{+C7m zki$HtVVn@ddrhPV3e{@3L=;}tdq;)UD7=+A4}FKbX;4uL6ew1OAxKw3ys0@)Y@ys@ z8VZ6`6>O_xIadXGlxo82IR~D>{Dzd>GOr5sT%}xz%%lE}o7L*|c~=!WC$I$mEcXVH z+J$fWA&|^zf}uy{{{a8Q07?)50RsU91PBZS0|o{I0000100ILM1QH=J5F$ZQ6Cf}$ zVR3hBRB{;cu;OFL(P7By*`{|>3c|x^{{W^|774qS76*2@zjfvq&w4<$ zpJ#99KV_|IfOS-?ZLRE#pkHirhqDj=0A-ApryjxdR~*^HcbP~2s+#Am>bxEA>L0rE zaQH)b%Q60$SXn0RVPC^Ne+}OD z{n2x{CV`2omaNgAbU6JMavydFD!Nz0@54{3#?Ek)e|TTtyUy(P)W; z90MJEqf2|Kxshewv9?+dPzldu(#gT*1YkG?m8kZ$VH~DOvTTppy?+kRigZy~;b?z2 z(Btu3*vI8j%K2S^7&Oo;`*=g~0P%1tkV_3+K;Yp))~Vyrk9CBqM{9hC*o*d8tv17F z8=T+@KiaJplkC=u$=M$MnIu}aC_?@g4~o{X!sFJp?DE2Y${_uhk?@}Y?^ae__@Y}m zw8g$RR4Pjy)w@8FCl)+y3z-f70Okns`zn)D#e#2MD4p=9FDU8L;Q?J%Nd%t~56w=? z=8bs*tDL#VK#I`ABx5Hu>jhRu=^e88r_`rCz+H| z3dT0s3;zI1f|i2=L*K^71#&QimeYDulp`{(4WgQVf% z+TTgNVNv)VC}S~pC=INY?eA|J4EWVpXldRwvOnj;EMMp+=&aB6;ckj)stjN>Tm_o! z*3U{z#DBVT-s0yR$@~D87uH5KdBhx1QEouIP7=PcuEiDOb^6_=I%7& zVzA=Vi5s-|HQVM5@4-SnQucQ^tT>HhMhMvKXUk)8E zugwGFZD~?822CFZ;x7Tvgsos}9nHSr+ucjW?lenw{nGK0lNhTQyyl4oSBwc@Y+Fm8 zms(+z0>|@S!%$Rq)8^jlQ*wO5(;Rm>z~Sn(Sx>2`=b=1=^OOf+;S2&z3am8?JO?&6)#;e$zaF5&Q2b~u|$*y@`@4MTfZgB@H9e`RNg z;tR3rUOB-zlEC5!aHg$){8nh6H;dY#WxxvXaJ13X7aJpeA^ytnjPT9>NCTXtoA{Ju)N~Dbi-*f)H;|_s?`&qmHsf|T}KR)V1mU+1FsP2F(Jld-;Rv=3e{k!u~GrM ziDHvG_682tI+|MDi-U{!cr_XK(#?K%LcM!nC>zu!rzB|NXs>Gw(7Soex=mJf7^jjo z!?HwFQ?F{P<}H7!8DEW6pM;74cuTLYbf$ka;L`|q2HF!!*IK_tWxQg%0pS~S6Q4&? zXcuJ{i>q>!#-nFqqZ@`Lrtdcz(LOer1D)v)M-0N1yc@NHsCQH-0b|@_i-V@ZofwGX z?hrZ7#2T9EbQ6CKKCAjS6!=}0bXXiM4e^Mja4`B*CbdQW-FwA(EO?J@@x<_U>RKZB z-qB6z^JO&tO`lW56ge(&RH>%ri;sHeAHgzw?G|lE^tWMMS|OKwQJ<$Gc&;TlZaP@n z`MY)A$1xPf4fIX3v(4GtaCPnE!&uVi9DoY*!))#L&MV4WKTAJ#aLh=}er;&0PHsmGJAPlUaP!6@u;A5~~>=M^of z{McAtocChq8lFoHNJlzCwE%ASyE3T@J6ycw-lgYj))$s#N3yO+1OEW-YQEnqXE{H2 zUnOO?H~ZFWd)re=1J!$*-Z={K-P1#-!f|@`H#@bi1w8uHDraQro*DJzn-Ia7z1xCp zaf_{P4Q}@woe^+&&8{0pnWOZfb>Y-dR)+3#T%6Oe(~VNDg^IyrfZnh*(!=cOftw{{k)GBS zW&bS5mb2CvY&jGCIP11l}36?;JrRg(aI^-CGl%=nR!k zTbAITcvgWqqX4hFxn`RzW`tr9XQV}2(BwfBBLXzO94g^f)zrlXIuReVuvc@KsVgfU$F~rG0;KE; z$}J4I=d`#EjXn*3`RyI}DX#{2!j@uEu#s?R$bto#=#lT8lZwdkk z{gkxHnwnvU*LVrRxOyW+6(pleaHjOvwxzY$n*3VLu0ks1w|dqXBGTKb@qS>Yr#1di zcIl1wk2c(Lt|&_DL`In7Ha{w+y396F&ZPm<2VE4+_KL_h^GLxvj2_|Q(C>a{u>$6B z@AahnDm9^_aMPM|Tq+L2pDoWUd6%+pQm{$y5BJ}tlDC}42M)v@GHs?o-Wug*{{WX~ z*NMSP{5#L-JJ*ttubM%rL-FESde6wH{pHx7S5k;;VNJ^DcB$=DGB}OZLxemex!^KD zF}l;%?WU)m_8dhD&H&4lTD>wyebYVx3cL( z15=`YwBEEG)AQu7Xld!t_@r(2JWzs+XBgd!9i|um0Kzx_0FGKKIreF!-QTrR@Y=9_ zGfuqv-V_PN&cmlm+q^7uYHO)pg>oQShjKQ;`}b>0+0{AV2-@cqPSdqf?yp)U*{93- zMv7LQD!eV^(SIJ2ot_%s8lvmioO48cF-DELTcUIA9TC4ewWpBjSm-U>yL6ak!)shN z73G;&_DJ4o(HnN$w8iprZuXrH4K#HyM(NcjY9v$rh39CPZ&0^t#l)aDDuGuVlcRkv zW+=F34M%6tiLM7`gB?3iBLWbs1{;-q1lZZwn!&S36)S4X;;D2~a^{7APCJEEuHJM; zeUGhp_q1PUIB2f*+ouJ=%Fy;Z0vwFq4i#?LN%<)veyicOVh+pEmi-jANAAO{EO`n) zCBj1PZX&4E3i?ebzB;-;lJ!b7HQbJitZh|JFFBq{(Ef@dQ)6W>8s@3Ek;k~@QHyk5 z*iccor;D2K*=GAZhq2iF)1Q!Rs1z79KRp%TP?1s)M4L6^#FA1`> ze3nPMv=CIU^jvD13D=KQp~Wv@yg#a)yUPeoK{BxVuGV)}JxfusAz$iv#gp+2-tKs%WxsaG2y zFWsT7sST#Zc1>n{Le$A`P4@xU4Z#THS#IgO9IjFFBngjHYFK z!aY2ERd%W@MktL1J)4f72OsmA-ux6;Qt5j;oK;gTEk0G4Kd*XY{VHOvxC$yKV}(`< zj}=!~C^=iKcyZA|zco{m6v*vOvqk2j!>p=l@N~n{iKDw$=)1=wimI-Y(lo_5&Nqkm z6*GoH)gbSo(Mdeh^<0P;vtzfcN(2fviNuW0FCZD+On&0A?poZ{d7A9_gyCN;KQ+Fn zMdvfbvswhv-ikvRj=I^_s=Yjw3Zf%KXQN40mk&wM&EFo5lq}O6jT%mjMSlT7njo!V z!Z&e1r%x%>84c;vNycNnka*Z}tD(IdODo1aC59^XvDZrGg@y>wrolU@C!+o8hnBd* zbOaBlwPA@=a1QHL#1B;$yxnM2$|mGE>^%f>$E%58++wsx{;R`@dNbIB7K29&o^;of zWSeqEn5&d6dEcPPE2+`FQ=1UTal)syPVvi0)x?8yEw7LDFeAhgG~IFm`tkawV!aFN z?Cs?0jdE2Fx_VH1+$i11wTFe5;IGQ#O(D{f%K9TUMmrtOS6nD}FC}M^PL{Nw=>&09 z(_TQS<55@H4tLQ?LyV6uYk&+Y(;Y6|k-!bu@?{k6CxjX3ve9Rg;z9Z;HGLzJ#1X^* z^GdxNi0W5u4ZZCQ)}z7bmhCY#Z=C2zZuC7FD z1yf~vMp)1-d0>m)lkA5HF_#g9ZJ_wAW*13Xmfrg;EX{Ac%75I2=ZFhEHq+**Xt#+5 z=gU<~?vw+S(!7O@?{~CAN7Yg*EHQPGRJMstWRcgM$`s_`)2G7mJ0oWg?r{65I_j~- zWUAbBu&h?2m1}#TuPYJj3m4t%SWeZd@K>B*?Ot?tAz0;k9;jYU$i{TN)hHJchRuu4 zxO6uUs;S4OzhoIDrIEFs+M}&4mu=R9?~?LXXFI8&DnnyvZ?yDi=0)ze?yA0%Os}>V%)?x)}q3@%{}ECn~KjewphBA%&?0k5TvLP zfanO?HI*snJGGQRsZ+F6hl8vXR-|=?2JFD8UN)9-9n~eDSYv5J%uTB_aWWWOQeTzn zDo}!{nOc-zGJ_zBYf6CIS$M3jkOQQq$U7yj4vC7E^jQgAA<_}NU@AMr(^w2*uV`bW zI%Wc48|yZlOQi&9k}KIQ9xP*A!7_l z<|Vj<9zm?J!x3az7|Uq#jYy_~-U;&)zNV)hzDOVj!FzN$brc zB&QSBz?yY3!+I`=>()mEA#O2pviF!qP$rce%ZlfCH!*?i{{T~WZwvB7GG1u>vr0#e z3%L4?9)lhUyF=w4sQWCy>G5zT53}}V_38ojKPv(CFkW-$nDzc^<`!q1Ka_9Pi4TY3 zJ5_fF)Tu*7t)4%)ozwpSCN2+A;(lY1fl!#?yC_#;HQZ*oaQw zT@|5xIO0=CWZv1C>T_VN%c+iIl>98MbISwSO<=^Xj{g9Oov%x2s_(Qg5-L!FU+%r* z&}T}k&*}pT;jBbO+Q-C2d95pN${SB3@L=}dSyWg-+O(Hk$CbzMQ#(9LKO$X)ja-NF z{E0LI_Fojo^Ch!I^bgu&Jm5#u2sYjQBr#91^oouM>ux*LQtE0|LMpApD%xndSC!P! zke=VtIC5_EW5jvB!#BGsbC>2(+<=Wh=!Q(iNI zr0i*%dg_@v02IHV9 zj`1p;WfGtnGD5pX-pHzExo-0pyrA4Y8JAuI(&7VJLHsv{)e!mz?iku0a~+P*yVCOr z+NSVY>gxonFTa~`L{(@QeNR)+{{UF%BqkF^4-SX^q3Z&MpkuQHJy6WQz?ZWFnk9{& zLYL`^qA5-Ayfm7xQW3Opn7G-*Rm)7fSV1C_3TvK127On=%Y0S14yMLuJw{B{d8E zjnDb(2nK%Cq>m?QvGA5$=*GS}A)xMFT1{P@_rc84tI#s_*ANSW1W+FL7QA|yo9@JN?tC%cm^rc*_YsiLQIL?r?tJ|sJxL* z4GqkBmWvjc7H7OUTpp3AadGs3@#Mg>Pe_7GHXAEFeqh!cwL>mfui|@x7%TS~rbu$5 z<_ZlYHS8H#VaL3^7|%U}!l1(wrl}JjF^y&RREz@)k}J3G%vQn4*M?f}n_rPH&hou0 z>KU0D%@LRvZyXTC5X)+sk@zJe#H(qVh9d|xO$nu4P5593kRM_EL#0AM6aMPhh`!5i z^d&ebcEK&je~5f-n#N^sym-FM$kQB4zEl}EesF^(5>qzzj&@~SiUsErJAiGTueK(# zt`krLuW4fQfXe7S7``_St1a#iX`fOS+D2SFz_Ybs3AWj@5eGe?Fq8>E7b(4c5Bi}S zSF-vs%(BENOSz*E+FMDlabGHh3SgCXgP{}Fi0)`drOiQM{{R$7$2!37Z{YnOU*N!? z?Cl0nMO9o1vhGJUdA!BfyWS{j%)+|WN3H(=hpNsEeDFN}65rAJgrGH(#h|lz`C=EU zVlC+lCLm$ZThYXnI?c4n7S<($xP0}wbu{A-KI|rp@Pd^D2zC^r$x}d~|fa_AZFa)7xYsl+fqA8tq?@jUA(Mt+Q5C z+x$IM>-ZQu9I=)CZ@qt_CbWN8k1nnVMQnY5!$5Oj`%OLO`1=WI)pt98X{2HPg+er% zB&`C<=L~#^T${^_DxNx|n)5vwO0zP-Q?(xkUxW;=2qO87mh8>mc#GhUdUFUTKY?5X zT6~iJ<{ad{+P*){blOy9?U(iUhD=mIzkAfx{i}Oj;H!xBSs_MP=YuPwslrT?y)E_bakTAlLV_ zum1o}Vhmj}oTmh=vf)%TLy?#;dzVjnSPA0#nfg{mr^zi993R6yONRcvV&@e`MPU5Z ziELkdMqIKSU+}+arRtu;`zCD*zU%tx_|jO+jjbx(QjJ?H)*@L6LJB+n3_{Gpo7TT8 zE2cKWHO1y0qJSHJ5CA#M062vBM?(WORbA^Q4<#fzffOsT1( z^(<{@e#9_y09k`Tig3$huk*Qy_A9I@=f(D^h;T>TIv{#YosKQvUX zmdLrE(;UQ2Os3Q&6}8H;p)2l}do*x1ev+xnW{7%LFl#A?sSCC_v(h1G-4-IRa6^*@ zvcwy)ZKG8F7dN4K&Tc#4llKRx^q}T@wLYQPuEF&+rqeI#T*wV&VA}pmdD+GhCy_GG zbYQ{aSo!||v11syW+4=g>}Kryg%E^9=3O)_-my^TU?WbVK=ZM4N6{8s$h$)3wdUDb z5q6<(EUxrp>S_?h{BWaw5x;O>1wbuFv+8{LkJJXfQu>VrnO>}}d&L9)0AgOYG^u4_ z-BiMWwg7fDVaoLns$@Ol?DD*-TjSM6S$e#+Z~`~FI(U5(Uxz{aWd^Zw#@TB1%|2y( z?g_*&fvvT-ZS` z4OdiUgMK60P#WO#w8^C<JlB@$6 zd5urZv6eM%v2`v59h@EYoah+DvO!(cP?pEylXr3cw(koZI`=G z;!|$Yd*&$6EMV4lns2d{p+=uEOyu3Gg9`VSH7ctW*fjT#VyOyVsrvbpFGSZ*)XW!3 zxIoCOX&hG{Ac$3alr31*%jsltAlHYh>+0t z`_^01Y^Kv6H+Vo*%Iemz(uaZ?dOC^AkIcL&!9S1LIiX@}6F0YUH8qPvIEFK%Z3kH9 zcISy&92r);q9`qyN^>xk9g3)|VYhLi`M-uZwwGOmd&N^yoVO(vDhLxWdR*f!TziaW z_?Lrg12D@2TPQ^KTKRtE%NL#%pfKwY&NBy_cp$g0gUrRVX#5iIX?-uN9)^=zepoMl zvlwY-MFPJBHJ{&~@)*_-M$lWVa36S}ec~+SZKIjSub7WvmSoFmNT{QCDAkE<%%I!m zGI=UFtMW{uroJyQ2W(BFmuTq*4?&qhlo3f<)nWFGal%}@KRCA_+8iC3MgqBXM^krx zq4G!fq_;ynEVp)65a<(V^gVB@(Tgl*V0M%QLv)q(FS-4Tnv`bY9ZQ#FT*uQEkVxTM zy_j3|W*~`(00T0Puwv4^I%QBIU(qma=2}1_&+buyY?a)&_IJT)@jYi0I@jH{-ba!V z8DMSR08yl46QQV4tXi*thclfRT_osjUCDRNuu?>m;MQHpxz@8orpr#j#x`wV)-VvbR&5$_aWV+PdHQmi|(I$OBjgvh9`ApJr7 zFZ6#f3^27U4`+Df9M%S-16#WjDJCy?umbbEzJvZpsUDXXQx!xi8G{@nFjMDv-dK&w z`JP``t0qALp~NPI&U?^tnWg^#Qsu`pNMR~wOWMO_96AL9sxl~CtB{VP>`G)*9t-u; zUTB=Sji+c?uCXG3aFF2>sAj!xTtCSMr}2hDXACO)fOvo=iDU>zsyK+VOM`+I#0u%A zT*@~$^rn!s$dosQ38%VglJ}Mb$2SGbFg?>=P`U_fC~$g^{81KKsJl$TwYWh?5AbE( z6rBT9(XOPpf1PFlpxq74pVLjH8p$&n>r0gz-mt(NIZ`?<+z<#ADpY0+J=pfyisjT8 z&~;7>6GBs+Ca^(=-d$;8hWiJusa?`PC)}4C>Q@F2`8p8L z=+DHh6m=Lw*(2gCq5#u$H4C6zKr}!h)g}+#Sys`(c#gl|j@Y%0ZsiQhDoYtN=`_&j zOv9XbQS)$3+0tFSafS>kQpw{+3R?<%7hq^I>ycEDg|E zXi=gidtm7dQHewlp|Ce;d{2QteXby(w5@InVRA>jY-KR{_msUlx{YgGH-W{5v~&B~ z0PMKzwLws2YFtiOBx-Te7<3h76euROP{&uJjP}3FLrHC~DEGdRoLn4Bhmm7p7lv(S zDPGxs)CjrDSVET#KgD6N_eUHfR4rX*S4ny)ptA*9Hd`KP8|ybmVdSzbFu6a-%C<*k zeXPr-^@TT^MxG;m&4Dzz(0G<|9FOE84x(nGn%O9-Wc&rqbRZ6Ky-Zo3_-jI{Kffd* z%Q1r4pG#QLH=9JbjRs*cAn1zaM(r-AV-eRdU(MZ-=i>fM#E1_mpkvxNTTVBb(TK{K zrrM@D2dph+4V(8?!Ly%z(1~7X_UZXunuYJ9zxkjQs##WlfB~ z$NnJpRO+et5|Ko0+s|m+!*m7uDpB3@`AD;{3oo%3t~RsnE%_z#_)g|vFPP>l)@PZL z>N&|Qj2OJ-+0t3&0u8z$nS_ny+&-@Qi&yXXAPq{3-w9~Ugr;K$)ImJ1f__MCD(Vcu z5fQjexoF%y;&$=_>}FpD*!l)z7GK=@AX&$6xs>W>zvvUa3rE?PSBqzGR#&vOO*#Ak zihHpArJ=L3Phtj`)emH~QC|?1#ZBtybi}S3E@iA6O%j=d6Td;+8QSRR#I{~pkp+-q zqNk385a|BE<|CZucOmr)J?OXM2lUSSQQLGf6V^d0wy?Xk+2RPpeL3X_z+tX56n?rPLa$l2zrk-d)QIa>sgft5YUY+^3-939~0hmq$1zq|ujT zYY4=Ynnz}OQ-c~yQz^X6_=$5A>DmDcnnNK#3-Jo7Of$;QuiUI(DSfg8!83Z^y3HEZ z$@_{1qnK>~`@fiOK9Qx77B%k@S3+Ax2xzop?Px_;V9SOZP~?>`jADwRNsFOuGVGU9 z;#@GyItn_)sYAToW({I$Zd65xqUc&;$!W$(SR zKO-4quDRxS&NVizme>5`(J5?~wlRisI~}|o3Q7g@iSC7b+j2Rb7@agmiiqy1IthwP#hFccK4m6`P7NTDLBv@=Jq&dUF$ zs3R6*$zu$jELg34qjQcvOb{8sdV!NPPu?^$T3 zLT7Q^SD;H;8ZKF!zm>H@v@T)T_1W#C?$N8o0(frh(!;nL<|T1d)q+1<98xNGr%h+@ z*;_lRP?(9re#W!lRnG6ef{PD+QNoA#YxI95?*-26$;S^##H9FPPUUV&cau(z zIlIAF%u?b7#~WGjK>F7ZAGkX?wA&q~>QyG-CI!T<_S{a9cB@jw`_*@?nk(6?v3J3^ z_E*qT+Gn1wiPYC$e@OFKnOdi)0Vqkl)S((`W7^Fus;8vWY6aYN{;=;Rz++kW$X#_V zU2wwWfUzh*HB-qU zp;>+MGRU-_4~^9f_szNXXs;SsgD{Y9U5M8V!Xf=2jmGzeOF2d$&w7({qJ_!wDwa2H ztx(TpXMwV>XJ1eTnh=x6f_l-5&JXJ6pKE2lJt9qKapqMV=ezsnOqA_mef zWE+uTg}8htDp@(i{^(0?e9oEYkV=}A`X_Y!jawvPqzG2n(2&@gvk>~)kx?>mBOw|u zUFJL@sLKe}_>{c-n2MW>+6~1X_d{VcKsZQLE@NQ3XGlcEQhFe;N3V z=(H>kIdw&LiGROh)9o;O5HZDkp>k`wx3Hk%A=RF~ux%2>8a(%LpIG((9r2<4m0Ie? zJDLbbx%OIp`iCt9F4BiTz<>7;{;frL*g^m*>f1B>RzRXM=G#V&w@&_@o$4PMDiD_| z35%X4n+0%Wz{YLS?PWMh^THWZL!~VS)VH8i6sa_ymE6th^=^TIbk^=?tV)z&NRr;` zuqa~4ODwA}yXJRh5IcqFWbgmuoioic|4Vv~V|o7@3SXTv`C6)jsbJ#= zmxHU-3+0v7F<&{snw_xJdIZVOhSQjphq$QzPBL5Brtt(hqx# z+zOoS#yx)kL8X}079~|&c4VC+M0LB-hwEs%WY`r%!#aHdFd+5zw+deL#;l%4Vqe7~@djo`sqc_Y=zC8~T&@PiFI+`;15? z18Ov|$D7kQ+K{ABS&IGIFs=CY`>R^VXtnpaigkIg5T_FvlYu6M@!x#8W6^Gtx}Os! zNLTI%`=iORC3$3PFiq!bpTE(7Y;H08xL7fFvdC`ux0i1(o7QE-klX#POl2o}`C2y1 z@)wIPY%7^kmFT#O+X#PiU4|6%mv?61FV4AXgAEw+Y8UIgodi5>p-2;UyX!WoQx ziKky6fjZMeG#wF#jV&BhzN(`n@zIPa$e4f}Q3As*fp2yiR?`;4>S{gt&GRbLDx?;(4yv|AOn$WUdhARm=sdPl&1g-0v`rKC!oKT-a|wg#5ib!i zBd*Me*7#aT+Wm zsDOt2!jg8Qzj!#wSF~4CU6f3{soutFDo{SItS%6@d?J3xG{d}chAD}3SZ+b|2C?{S zBZyiC&;|BP4IvE80bwu^_;vP^1a3|@eCm0&;47&GI|H6A&~)5mR7X>#2rT@dP1d182;vQfD~AmLGZMto7C6o<>U$I7TXdX*5bTbZ8EqIL2{^_3Sljx$>qiVYG^0Do_unlU-1 zh19fGAMoU~Ub{U`N7^-vE4pt<;&Qu!QoV@!Qa|X>C%|v1xW#Y<)B&(s-oS`__~MWP z=NKn(s*Me;&Exb0cwQ|(Kpe+N-CyVVqS+S*NzWEuthuo1_g?S+0Dh=sllpfIu?(}I zHWQbnjl3Xr#za{*N&L!L0VQmG@re2y(QWEu_UsHc*b7^sc_!t@n+b%z-sSJl+q^S;0(5}g^=1w&`Pyf0 z`}B)5e5CnNUVqTu3(?ZUt>4Y?+7|RGFfsGo8t^qm!_k@;Z06b>gbd8}Fg$ zUROtt3+2&JL2_WKkBwby%fDIEW&xo41vWRdJwbuJ80=l0F%3T(#&ZC0ID zRP{?N{ba!rPKiK#L9J7*uSMvI`CSN^Un$mcrZyCKm<*X|?UMGwxF1AIzF}886EifL zo{TF=#RaUNa@npN?aib=6F6w47-_&zFjM4=;> zH11etxnb0rhXrPyYDDzsh$s@o<6zl6Zf^^+c3H_8Yj^v$Q4uXoN3BWw2e9Rk8co2S zYIH$rOEJ9(dsZT*pG&Vuu%p1-tr|@P)i?Fo%5W>%Ce2Ig{c1ok*_R#=iSzv-Mwo(> zfN`aM?Y;0okmLA?kCrYqr!gFqN|qSKaH>Y+Vs)GOo=+Wn0K)A#UlgH7QE5WxTfybS z9)mc4#s@SW^}iVD3t&yw_Te|)U$){5fXp*{3*`gTgLZzW77}RWY1KTA87Y}O-+aEw zQCkNC^28qdNXW%ox$B)IsmX4B;aHp`df82F*A7)Bfd%CSiLAfhp!t?`7VZ&ee{nu2*c z)a_m*9CM#a#-MKTzvz1n5ksbrYxc3HlYyHoVu{qS4^C7sQfi1D@ZzImOM)+o&2HM{xOFa=7Whddn4^zP+x7Bj`AFkl{vB zH8KD>DSa!#uCcbf@L@%gYh0i;LZwA0YftSOr)Nq<!YILIIz<`*7Ph;u~ z;GeK0nD!br5gflWGP62P^=qziK$@ZD|5mJvk4~To+41Bq=+x&2D-0YfCdJ3i=Od+s z)dF}*aWfFCRjC+J@SJD`NVFhj^@#H{u{{Be2U>v^g|>HM0(`)#H~kl^l#)JuCkMY; z8v>rK>4nbJ)_aAdEMr#BQ*`+xa&G0}u@VU#32t&h%&`j{<5L*y-TZcE9kifP;1O`T zhJ7zbNb2eP)An~CH;LQR`MCr-N2#Spf?Tx}X$CYeFX6Im+8;#1<~s;mLT%0_fv7E_ z(fr=jvwryCu%NG3D}8bs_b#l@XdQ!!Yr}AWIXmH}{67IH|5%e4fQLoZKe|?&f9Zcn zfh>Z|59TDqw<1F!s%>QJ_}}WDeAF=FEle7}4l|mS%xZ^fMd9P< z-_g&?Vs0zY`o|9C>Ym`o;d*8*Vf$fVxg6U&&kh&HwA1DOA}PMD;MgMNW8Te(05aLx z?1HW1r(OX0$!rbYzPqcI`}&i*3D z^}18$XeoZaAITtba-Xt~PZvx~1dz{pHzosK%Q8T>gRO>;&kg-X%B=5HL_01pSH8qf zrQ*In4KWJm!CZHB_0_NQ+a0;#O$yzYPExGI8XW2+B4KGHQros$`&t*abV2l#2X$bY z_>q#D!&l$@gPymz02Kx0lw-?7Mi%eW*Ky*8w9#cJx!_XfUhWyAJri!~Ux|5))>$or zj0x9~^1YDwP+@)U9{@Mm!QdZ&UjFlF zxeP$KgHTi$T@NmvyAPVqyP;dsk`bIWjx5C;ccFv*(5zD8lQ>)^_=Rc1>Bp_I@TY4 z#!9}#TXEd{M$-INz5RJPZCM)LPP+@HEI#Uw4VK%v6bW*@why_f!;{2R`A9C%e0MIN z{1yl=!qI=;xX@jt?05O%h;M@0)NiUYPUdVN+#E+h)f0-Yo2u@yh`*`y3u|0a}uvY@F#Y7G2hiUdU#+;?R{;DuSMt%cp3zmgr1e#FG6V=$H1_nlUbD+E8e z5z~3p?}OPxvFHejudCk&sz8;b5Kl^7qY4icO`kN59T5JVKW!Pl-JV8Qbcq6%ZevbG zw&*u!iij%-Zgxtz1Bg-6iAn}&PqCBM$fJ|&`!4` zWo4VJ-QKa%L$p$*DV3N*-Lum1p0V{m0RK&!6oN$+5&ayaVT&lk-RNtQm@40lA|Ef^ z_lIdMcW<}v#(l=GSnvJ-7Q%>BUxVNA2p98r`25Z@Gud?gjuT^;K`Td~(Y|fy@%+`j zI#T`=JT6OP+j1$eGlb4_YT2YrgDtW>{(+;8$G1dHArYE`4=JQgBv0odD@Kkv%E`na z`^{^6sK-XVq}mx)&0w*5k&N1wuvd7M*zR@*AADDdlh5rG1i#tQK^i4K8&71JN3rM( z-QcS~*quXo#E<2=;-5{<7jw%MkZPLXD2eWUo=!hSHld%a`8&hxcU zY&Uox_MU#S01DF{zf*&T<6N_8w*Xr;UdcnC3M?o;P0X?cPDW-k%*NThjJ z;>kRqLO;oN)E?`({SMHdf-99K7zAuE2#0R`5h1K zPAjc4FjR>QOn^B`o8H= zJRRfQzUnc+uc`FuBx3`+#fkY<+jX`$-qZ1uaFbS0MTtV>N0J(5>;%8vX-e7a_1;kl z4w9%>Vx)LA7PP6i1}IY)s{X`0wGe@B(4Q0v<))n+MfQcb`XjeTKJHFyN;&7DsY*0T zhCii@d+KIqN#9Ok)P$!^G+x`YNy>vjRZozPt}hRZ^2n!a>sw1WZszQ)C-`+0=kO@u zN+s<;?XEitzE6ZqC2}~cVGRnmYNo+ZRCQ{i?jm!gFmPzyyB?Td`GyJ=)))mJs4y8a zJ*%03@j|%`R_9xs6oMWjyi_!mC=D9mex#a-9iuvzvJtZp+o6gq?=K32p*Vwir|j%h zg9u@x)v@*&!S^g?f+@{;2Dw9RH?Z(Ckc zaxiw6*vNA1R(y1gw2*_J=Ah!}$CVgrkdPbCssa2}?_R!lZ@9J>;p;qp@W_CbBlkPf z({M>n#||p~uoQ_Y*$Iy^jTZ29>T#`GtdVa1>zrJU$;lOnmmGx;GThxLaC1x#!{4kq zBb<&Qk1ub>!Ai}hml~R9LzP%O#{X4@Ti$*Lnqxyn#!uYZ=59R=+eyOyfE2A7;rHjV z!L+9qj~z?Q;WnxFL8?c)&pvsk6r!rH6fDB?GrF>g{f0+PJ{2&MAe2XroGN_i$gq@6`yA;K zJJ7I1xn?NMRbSdK_sikP`1Wr|OcM?68PVn&gwBKfTAVlyP!LhkEJTLZYyagBL z=!t7yv(lh2Mat`Y;6Gled`A_H#vd|dRjx0(U}sWa1AA8jHIrq^LmNhxoY?g)V?NE7`$gr@#jLJtwNqp z>!Qe!XJ7nNIQeV;0J5-{Abd8t(<|QVnnOs0Uq1axNOatz0}A{yh;iti&+1d5)(+3C zm*U8P2kW+>70zyvGG)6*tU#s|o^MEnpy0A5cfsoyYDj zOZ*vXo$y(481^LKRAvOWg9=9P!7{0znQGvtLOzOmDhk9+<5aJvQrQw;fBTBYISw(c zd>~OPr|}>r^+wWuhCY<|sRp*3ush^e{(6D3L$um1WWJ0KV=A^Z9ew0logF}m7XKay z>tSjWj5g*T;vNE(rt6DdrkW%VMJ)j!Dv%_tTRaDPe7)$eBL%5UMgn7*}=K zI_;}OUik+Qr9#-D=hCD00gg3JKG5CwEp|*Kp1-EfRU0K&F*3NoaSD%8;jOV+`A8#N zkY*ZU&ifAIm-UqJR$^Htr$NCG6s~pAfPd$}T7km5`t6SYR(>KPdtLGm00~nYt>(Nl z*?e(X_*mDRbb`0gH8M-0$2J&S%F^KX2os}eg7H)Rcj%v}d?(uFuNp1t`aO8DF^9R2 zZcNs03!JwLv>u!WJcvQN&Cy?ZXFN^cvmVDbGc-&c(eeqr;z2aH&yA{12dO;lnq@a2 zOo{~Pr&tZ6&-h=cwn1@*CopuHSq+vZEX2F3Qz)l-9A}zuG?d8ILk5(r#T4p+1HZ4G zA-E2V?PF$IRLXNL0)lgTE5>tcv7DR1m^@fsHmY-gYU_i&E=4e;?FFT8xXs8W^y<-P z0uKy@*NRVw^BB?j8TjO7?S_EEsOWr)Qh`jTNpVwi^*0bEp-&|rO~1u3$&G~P+UVdu z8o+SAa@-Qe)=d$}C1dK+@>yCQ7n<`*=aN#ondA$@dsEgeS{8gK%E-kijGwE$CjTHj z4JZxjPJ8LRh8o66d@)%dqN#>{dcD1qc~%~+5KP3u@OoTVSQ zL_G@X^0bvV{FV>;isCA>eb~-u#f1te4r6$&Byn?iRp#>dCz+lJri5zq@hUUQ>%Szg z)Js=TnhGlezQO%jpi*%KBgW?M&twFD5hd*LOUWlk8~1@71d=OA5zJxf6N?UFg-^2_ zd@H{NdhU>t?PN=$!^ttweJmR76DIP|WFLg+Ka$?ramu21hQC>nHDUzQxYZ*lw}wI~ zmXz%(_`z(6=&jE+Ek0r?-Y}-0fF#YneCo4z3CjmN0znOs6|Y~!mNK}h zAr6(-Rhx-<9cw&qg)s)@7uqBa*%rd`Ms&>V^7LALPw1P?58M=jMibBf0CsxX*FNH1 z+)g;zRiBTykGnLn5)J$9;qn%?>w#YrW!P{PyqiNT_B+~3c7Aar-2L5Z~Htuc(zd__29g8#F`2(fW;l(B zxT3MFm@pe$N``lEN)X&Z^lei$A#7c&)hqo7d6?R~*AahzKg(5!Rq*c@4L1)JMt;qp zliK0N3wdWe4Yw!vX!9g4JFw7}A@3PNQbk>2hNc;#<77m>en_&87`tjUa0;+h7q39w zEm*ZIBm3u)t=0%pE6x0Caqcu)2O(j3iQQkN3;hMHp=mw&whGR|9yLBvlZ)Ja%}e4Q zXVYz8efcmGIpb7u_fVFp4zL&MtZ!$B@!};6%Fx9u{F~xW2m>hroJB>{JQuv-&O8@F^Dksfk_%;Tt;*ww{ zN?YwJDs0&sHOH4d7D6I?cK?Vho-gIeY_$MpC+6?Lwt5XRl`bpel7n5;4d1o>)+kGwI`kUHy;_Q-BUgmwm{Zkz2*Zl?|D5U zQx4)5*fSz2suc`%jkimRV@0aS8}78KLQ+p;B4pDJtOB>cV(acb9qa}2IxIKjd1JD) z_vOB3DT)P3xpShg?ggD7p<549Z|?QBQ~W)8dSc6H+^t^xn4ng@PdaP&yZw9a`IhLn zMEA_Hnu}F;CBszNNj47hy>ByUKexR^|B+C!DY-f0v{p1VIK}8thtnE|H=LB>98(d4 z)Q48i`VOjbf5~i08|Z*|j&(fzP)WOlVXhw22K8Qc`g~(j{HBOOvyE5)L|3v`juc(J^Q|xoe6^ zux_8?{2GY)ACnX8bdqSWUZuvSo{0tZI(}h<_kZ8(!#TqVuLAk(gdS zrs2CY_C__zlDRsEf%%u({>598xcy1*-D&5j5EvLoGAByJQD119q8{7uP;5Lj7qpt= zo6UzkhGAvpcLs=zsj>B%PIxl^`{>F;!eiYxR#jmxIyL#dH?@pln0YOy4za=MEAFu@ z^*q7phKp=3CQiy1RdpBq>mVapJE976bfGMjkx%9F#@=`u^CZ0)N z5cRb;j}e&@BiKJSCNsvsmHI#+mJ=i%wS&&*ECri&A20$is-IKHD ziIOzVMI3Q0d2tAF!-=>%Arn!U7@8YNIWw4(#*@Q7+)ew>QY`U{<>C_}f1Hqlzr$iU z@$P}Pb7MmgjBzn9W8BIv!n}?*5-ZN^cr1dG2q9@eH&rhpVb$g5MGDB?S)B7YhcEp zm0L6A9pT{l0O=1PES&luUYsIgwTnWn*8#lPSV*Hqe9OxMbp-TTDK4u?o7h*MLtW5U zps($5S6W{HN`DWK@<~#^XtUvm3)*{o3N^Ri%o!Rioe`qzD(yf0-j321A0owB<7KUM zi0Ck36k(evr$y}5l2~8b{UJA@H*8LAvBcJ6RgpRPV6W6<)z1`fH6_4{epy9c_DoMM z#neBKo<=&F->(`dJHEWdxqV8e<^Le}xq}iYEw9tout11^r*V0teWS56o~7*gxcn00 zKL?Rhy;FMgXnBX{Aou`LhI`BVX-{}tCNu{vX=FX9L~z~u3#et9ucwv@>7VbHi-B{7 zf6FVjPt3hPc7Md;h|hBHMM0%38&NCOMb_-BR~8!Q({)`yPS$4}8yamD&}ArPqo&v9 z(V9r7_9*)MP99c|MViC55}b5mVCgp2K z^qMDMs`K-%$gc7zkgqRWZ=sk-+`*NLz%<`j1H883!e`Xs5>ZbZnBNmBzCndvMfOdA zXEtg|k0f33xfnqh$2tj}XHLvH@q1=9qrM6^>IWRbX4!`?-&gZey6RgQi`WtZ&Uu8| z`%4tX-bSA*j&h>sjq?{q>>4k>B3)7vC6>39$7m`hpAhK*Y5uS^eBN;mjS%e?lic@sLc*%3)k zk1;k2ah6zubK_-HBw;owXLOOgQM3^@mB|Xk%jMtKZ7ZXOPi0ewZb*=wbOC9vZ779z zVHzT7HR6O0#8#zT70!)<&Z5D3AUM2zj+lzs1joaev7{ktnVHD<@odr=`3yp8?o8uW z%{bU}`6_?VV?mOuC6Ly-Q~quKo+3|e?5PAw(sGaUD}`nkT2pJxb2~Zbs|h70k*eLO zb~;+d%zN+`54MXt(o0Inm##ZCfLivAw*Vl@QjOCNFf|A|e1h8^d6GqMI}%ru>MTV0 zHEIY^o=(Hva7AM_n)Ak6l?5FvTH@s4Xhr++*bm)w@`#l5Yj>rAo}qcLzOBqFx;x#3 z9--IpOz(%)P`K6TQ!>CdBMgW<;(>ET&PRd^oGm0MZwEDjF24yTb;$^#62~Gct0709 z)R}C3kxvZZ#1EOG4S9fTCcdX@M||f86FDPi=ynGC@yF9yq?RadZg3<`oZiA=vgQv; zT2ijn6zB0N|0fz89E-$S)D=rcEi^G7->B1EXQL@OHso}t_6MEH_TXXL+9bWO$0$oQ z>`49wKAh@eLGjAjpd7wVG_7{Sl(R2F%|bIX zwsFgw2SE03@wGT;qS?$(0|%A8_oS2y?7?F=-oqoeNQwnN#}jY~>yc1cUw!PVOJHZ_8^%8^zTZi^EREl7C>b3Ylw$s)zk zWEULc=SdpbbydWOPas*XXD|0(GNs0#PY+Erc<+-A#XH?$jz-o$28;Nz_MZR)0_ z&PT+-Nj>1D!d@d&ku*4ZDY+EOGwy04!4IFuj=#I-$xKI(`=ifNV;sy&;M53H!e=AB z;*I155Y%zCzI9%eZ+D$4QATRhg48doJrwL0HIBU;O`M2X<3mIu8;~boYR@&73{vyz z02Av3Aa(;^eB;Gj0cAv;xV|)x9nJw|>+v91I92`kIJXo1CCrLPWP5NvGCkB2ugST! zIz5-Wu(`v zKydb}U%3Fi0ZC@IwhDFGj6VPc#Rb(zuLa#vpGbaV`7Kwr`VC;pf3x59 zd$Z;O?u|}WsZ(dFa7#Lw?KzAGU1+awlq79P)Twj}1D3s`1vnf=fB5T2{m!$IstPd) z8!ry~zop&ca`vb@zYNm_THu4NQ!4mo6>&X@amvGA$CU9D} zQvbx;OBo)~ry@qg6KB8BaAcHQ&J$LI)GQ#wws2t3ws8&vWnI(whpLhAm<*JbvLw|8 z5LG3|_q`4fX~jFFI^YC-_N7RaDLWZJBWxp1fwD7S=O)(YX8S3wqPvgDEjUA=RJCNe4PvIk8L>^@mSZP68_+?J3IW7Ta zEXSO?0e(<2qU>usRd0)5_>3FcM|=@fXSJAidzEpOYi7j5Zc!(pZsTAbC$48C$;SE< zYxQ-jFYhC;EgJ$|?Ri){|H!BgE1Kx;*@Md)EDO^k4ZE^PPoc73}_ zd?rFokZG+2(9^!&uumK4BXP-}+^WJm9fB{e!iFH*RWY^?B}9)>JNDRDXeDAU&PbVP z;wqaR4jwiwWtHo67$CNLmSGLo`KdBNMz>pMM9v1nMVNm8KHef&R}S5GF5b7phdTL5 z`XNcB`xWqPe7i`>G|JA2(=3j}+#d$ks@2vMg-2L?sfuv$Ctp1deu)Cm$KwMuEW)VZ zxfA~N>+H5m7?d}c61%)WXpUeA2m6~ykwUI6&gv`M9DJfFH+hEa0^&Of_4 zL3NWYMVl}i7A23Ca`6;1g3#dOA)`NpwWD?t62aNDVONJ@QT8Uy#hkR7cpPhoR9ktq z^25dNLSKCdvQ{1#$K`SrInbvVBhFAxD}29=ZF?F_s$$4JHH98{Y=14sfroL`cH5TS z3#ELq%iR4q$wZ85D8cTV%8Xk(mCIsf+nzLXa!BdNF#@p9FjV(UeNV)JUP6`Upt6D< zGbo~DBH1{6 zX=9=?zf1pD@G5S%UKtEge@TwSL<+PqLZd`#68aa+Kz>R4h1m84UUFSLK)xl^IH=j&>9qzV ziF=d!L3nI%yT_+$&r2N{1L(3qr=RXd-9Uv;D2gv~=p0ZZuhR1MzC~YrR(g6?#nv}? zfWwIM;Q+fWCIA?ljHplw?9Q8#hj2zJSRWCtLOimQ-6f&F3nF`FhMMqv2s5MzmV~Oa z+kO#QGAWC`>ZNTGRsjZ9eC7Bh>Z{0FR)US58A<;_LB6g+Ye#99HW>@#U7EfI{XbcUHCP0~~|b0%#$#n2?@` z94yqWE@^O7l%)Igb@3(*R-$@?;hG;a?y*DO5r4(#XlU=5S|hhbA~U_D@L6%xL0nyc zmwvB(2#4kn#bZhqbfTw_hdy|fJDuj^!H4tSEV~gwndpqWK<+7-2w(Nr5}1>}VHo2@ z1SPlwek`$;?(@i#y5GIfDR2LDnCu$v`f~%s+{9JT|Lk#u;5RHZRbLw3_16=di+#J% zs)jfyMyBKHHHxu7s(Um79;u61MArmajQs{*Gr^~y>ozCN`_aC1g&_!aj*Zn}&@jGK zHtY-p$3j;2TQY;zWp?l3pS~mL`OS+yfX3Bn!;*$$4=I{0oyh}TqT+bi_PJqOd64Rk zDJHNk0oCEdI+$k@S}pG#$`H`)i2HS?2K31D!;xfl;M?KRAikHsn|S41 zS%XCb?7%1e9k7lgcg~zt^9+g;Y?{rcJu^Ww!x^SNgPT1row9Xd=Nt3vU@P_PG+_BP~DrH_Pjs@!GDExu=BhG4@cy1mTP& zf->+89E_c_x8I4C@f+#cfPFcR8}8Jj&$367^bpkh&L1nMDbo?BKjecisQ}LP0&TH? zDZA!Hj%k^GCgV?#q>!#?FwkFy--;GjX?40Ff4PN6r_rlA=2q4}B)jWXYzgG=d#J#Qp40e| zi>{J8PSM)Rss49f^KyMNL zlqNW?s;FyLNhk;%Ui2E&t5Qmh_hLkSn#3ZNcNfxt7Y|9$R)CV1uedqmNbM)P1so6C zrAb-RGGVEqXVrRpQRgd%8@43Xv5BEJQ$-xT^X?=44 zZ#W;JDcTuj7Kxtz4q|xYx64~9q-0|C0l@eX?VdH$4H=v!j*<4WC{t{e1^&;(h$p-_ zk%9?EKdHAT`&Ef>PX#BPf(iB2JGFBs~NJ07>|5qB$EfZegokR3F4@+VW(f!Q1*FiemC$< z<@ywdC!vt_Y>c9uTg8kpd-)GGIt{0EwF9H~QVLw6`KG7=6)s#XFsj-z7%PiPt1|ey z1KV`Q@DZ~J)ebPd4$IB_3J#D>enxD*wlST*K+myvc0_f0@dHlcR&9-i8?t1qIcJRNb-clWeBL^h%lqT!N4?KQub&` z)}(@!?)_r;@}!PM?+KovGEzmNSB>o95AtJ_LMW~$K&dY{Tmhea^cjaJP=G3xMQmeb ziwp;J0meg5i8CKK>OaA=uKFD%^an6GuJYd3Al@T?Hw-4pqN-*?`JEGH?cod2p1(?Y~(Wc*Q2I4;UX3&WM z<=%oRDfS?<%-*%GSU8wsxSC8P_fwx~n)rIixP!JA9w?>whNTfqNT%lYMW+>fUTuK=nmNf~Ioe}ZdVPcGX!Wm*)SkUjIJnHVhX(IGj#j-(Dy zYe$Unn3RlvyLa7EXt>FBe8ZP3eMywurlUKfg<^S?U~yfi=#3!uucF}70@1yxPQYzW zRwY$c>GdoKCR8xG$lYb+LN>1A(KjIpRYHq{-?$c033mZa!XoM9ee9qlpd}E%+E_L4 z`3R$u3&4NntM3O4ICKD2~&w%VV9pB;x)jpsw*{rcpx!w-=L z_}dy$-AA0%=%)^V%G7#Uv-teQICw6_vs`?T*{moE9dOGU6(-8JAg^y+|DxQz>A?%P z4P~d--;<8I$|FDF3drI;5W?wgu{^;vri+&t}Fo&Wt>|B{vdtNZ`-{@_E3pOX{)&*T2{W&SCZ`>*!zyRiWdZeI3&q5m2E-yQ!m zH|2jXL literal 0 HcmV?d00001 diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..c73edcf --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,237 @@ +use blake2::Digest; +use lmdb::{traits::FromLmdbBytes, Database, Environment}; +use lmdb_zero as lmdb; + +use tiny_http::Request; + +use std::{str::FromStr, sync::Arc}; +use thiserror::Error; + +pub trait Handler: Send + Sync { + fn respond<'u>( + &self, + req: Request, + db: &DatabaseContext, + params: matchit::Params<'u, 'u>, + ) -> CrowResult<()>; +} + +pub mod routes; + +#[derive(Error, Debug)] +pub enum CrowError { + #[error(transparent)] + LmdbError(#[from] lmdb::Error), + #[error(transparent)] + IOError(#[from] std::io::Error), + #[error(transparent)] + ImageError(#[from] image::error::ImageError), + #[error(transparent)] + Other(#[from] anyhow::Error), +} + +pub type CrowResult = Result; + +#[derive(rkyv::Archive, rkyv::Serialize)] +pub struct FileHeader { + file_name: Option, + content_type: Option, +} + +impl FromLmdbBytes for ArchivedFileHeader { + fn from_lmdb_bytes(bytes: &[u8]) -> Result<&Self, String> { + Ok(unsafe { rkyv::archived_root::(bytes) }) + } +} + +#[derive(rkyv::Serialize, rkyv::Archive, Copy, Debug, Clone)] +#[archive(compare(PartialEq))] +#[archive_attr(derive(Copy, Clone))] +#[repr(u8)] +pub enum ImageFormat { + PNG, + WEBP, + JPG, +} + +impl FromStr for ImageFormat { + type Err = (); + + fn from_str(s: &str) -> Result { + Ok(match s { + "png" | "PNG" => ImageFormat::PNG, + "webp" | "WEBP" => ImageFormat::WEBP, + "jpg" | "JPG" | "jpeg" | "JPEG" => ImageFormat::JPG, + _ => return Err(()), + }) + } +} + +impl ImageFormat { + pub const fn to_mime(&self) -> &'static str { + match *self { + ImageFormat::PNG => "image/png", + ImageFormat::JPG => "image/jpeg", + ImageFormat::WEBP => "image/webp", + } + } +} + +impl From for image::ImageFormat { + fn from(val: ImageFormat) -> Self { + match val { + ImageFormat::PNG => image::ImageFormat::Png, + ImageFormat::WEBP => image::ImageFormat::WebP, + ImageFormat::JPG => image::ImageFormat::Jpeg, + } + } +} + +impl From for image::ImageFormat { + fn from(val: ArchivedImageFormat) -> Self { + match val { + ArchivedImageFormat::PNG => image::ImageFormat::Png, + ArchivedImageFormat::WEBP => image::ImageFormat::WebP, + ArchivedImageFormat::JPG => image::ImageFormat::Jpeg, + } + } +} + +#[derive(rkyv::Serialize, rkyv::Archive)] +pub struct ImageHeader { + store_format: ImageFormat, +} + +impl FromLmdbBytes for ArchivedImageHeader { + fn from_lmdb_bytes(bytes: &[u8]) -> Result<&Self, String> { + Ok(unsafe { rkyv::archived_root::(bytes) }) + } +} + +#[derive(Clone)] +pub struct DatabaseContext { + pub env: Arc, + pub metadata_store: Arc>, + pub binary_store: Arc>, + pub image_store: Arc>, + pub image_meta_store: Arc>, +} + +impl DatabaseContext { + pub fn create(path: &str) -> CrowResult { + let env = Arc::new(unsafe { + let mut builder = lmdb::EnvBuilder::new()?; + builder.set_maxdbs(4)?; + builder.open(path, lmdb::open::NOTLS, 0o600)? + }); + + let binary_store = Arc::new(lmdb::Database::open( + env.clone(), + Some("binary"), + &lmdb::DatabaseOptions::new(lmdb::db::CREATE), + )?); + let image_store = Arc::new(lmdb::Database::open( + env.clone(), + Some("image"), + &lmdb::DatabaseOptions::new(lmdb::db::CREATE), + )?); + let metadata_store = Arc::new(lmdb::Database::open( + env.clone(), + Some("metadata"), + &lmdb::DatabaseOptions::new(lmdb::db::CREATE), + )?); + let image_meta_store = Arc::new(lmdb::Database::open( + env.clone(), + Some("metadata-image"), + &lmdb::DatabaseOptions::new(lmdb::db::CREATE), + )?); + + Ok(DatabaseContext { + env, + binary_store, + metadata_store, + image_meta_store, + image_store, + }) + } + + #[inline] + pub fn write_txn(&self) -> CrowResult> { + lmdb::WriteTransaction::new(self.env.clone()).map_err(CrowError::from) + } + + #[inline] + pub fn read_txn(&self) -> CrowResult> { + lmdb::ReadTransaction::new(self.env.clone()).map_err(CrowError::from) + } +} + +mod macros { + macro_rules! response { + (err $status:literal $msg:literal) => { + Response::new( + tiny_http::StatusCode($status), + vec![], + $msg.as_bytes(), + None, + None, + ) + }; + } + + macro_rules! single_multipart { + ($req:expr) => { + Multipart::from_request($req) + .ok() + .and_then(|v| v.into_entry().into_result().ok()) + .flatten() + }; + } + + macro_rules! fn_to_handler { + ($f:ident : $handler_name:ident) => { + pub struct $handler_name; + + impl Handler for $handler_name { + fn respond<'u>( + &self, + req: Request, + db: &DatabaseContext, + params: matchit::Params<'u, 'u>, + ) -> CrowResult<()> { + $f(req, db, params) + } + } + }; + } + + macro_rules! parse_base64_hash { + ($fr:expr) => { + $fr.and_then(|s| { + let mut out: [u8; 32] = [0; 32]; + base64_url::decode_to_slice(s, &mut out).ok()?; + Some(out) + }) + }; + } + + macro_rules! some_or_response { + ($opt:expr, or respond to $req:ident with $response:expr) => { + match $opt { + Some(v) => v, + None => { + $req.respond($response)?; + return Ok(()); + } + } + }; + } + + pub(crate) use fn_to_handler; + pub(crate) use parse_base64_hash; + pub(crate) use response; + pub(crate) use single_multipart; + pub(crate) use some_or_response; +} + +pub(crate) use macros::*; diff --git a/src/main.rs b/src/main.rs index 961795d..aab5706 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,70 +1,7 @@ -extern crate multipart; -extern crate tiny_http; +use std::{sync::Arc, thread}; -use blake2::{Blake2s256, Digest}; -use lmdb::{Database, Environment, LmdbResultExt}; -use lmdb_zero as lmdb; -use multipart::server::Multipart; -use rkyv::option::ArchivedOption; -use std::{io::Read, sync::Arc, thread}; -use tiny_http::{Header, Request, Response}; - -#[derive(rkyv::Archive, rkyv::Serialize)] -pub struct FileHeader { - file_name: Option, - content_type: Option, -} - -#[derive(Clone)] -pub struct DatabaseContext { - env: Arc, - metadata_store: Arc>, - binary_store: Arc>, - image_store: Arc>, -} - -impl DatabaseContext { - pub fn create(path: &str) -> anyhow::Result { - let env = Arc::new(unsafe { - let mut builder = lmdb::EnvBuilder::new()?; - builder.set_maxdbs(3)?; - builder.open(path, lmdb::open::NOTLS, 0o600)? - }); - - let binary_store = Arc::new(lmdb::Database::open( - env.clone(), - Some("binary"), - &lmdb::DatabaseOptions::new(lmdb::db::CREATE), - )?); - let image_store = Arc::new(lmdb::Database::open( - env.clone(), - Some("image"), - &lmdb::DatabaseOptions::new(lmdb::db::CREATE), - )?); - let metadata_store = Arc::new(lmdb::Database::open( - env.clone(), - Some("metadata"), - &lmdb::DatabaseOptions::new(lmdb::db::CREATE), - )?); - - Ok(DatabaseContext { - env, - binary_store, - metadata_store, - image_store, - }) - } - - #[inline] - pub fn write_txn(&self) -> anyhow::Result> { - lmdb::WriteTransaction::new(self.env.clone()).map_err(anyhow::Error::from) - } - - #[inline] - pub fn read_txn(&self) -> anyhow::Result> { - lmdb::ReadTransaction::new(self.env.clone()).map_err(anyhow::Error::from) - } -} +use crow::*; +use matchit::Router; fn main() -> anyhow::Result<()> { let database_context = DatabaseContext::create("./db")?; @@ -74,22 +11,25 @@ fn main() -> anyhow::Result<()> { let mut guards = Vec::with_capacity(4); + // router.insert("/upload", &routes::upload::upload_route); + // router.insert("/image", &routes::image::image_route); + for _ in 0..4 { let server = server.clone(); let db = database_context.clone(); + let mut router: Router<&dyn Handler> = Router::new(); + router.insert("/get/:id", &routes::get::GetHandler)?; + router.insert("/upload", &routes::upload::UploadHandler)?; + router.insert("/image/upload", &routes::image::ImageUploadHandler)?; + router.insert("/image/get/:id/:format", &routes::image::ImageGetHandler)?; let guard = thread::spawn(move || loop { let request = server.recv().unwrap(); + // i don't like this alloc + let url = request.url().to_owned(); - match request.url() { - "/upload" => { - process_upload(request, &db).unwrap(); - } - s if s.starts_with("/get/") => { - process_get(request, &db).unwrap(); - } - _ => todo!(), - } + let matched = router.at(&url).unwrap(); + matched.value.respond(request, &db, matched.params).unwrap(); }); guards.push(guard); @@ -101,108 +41,3 @@ fn main() -> anyhow::Result<()> { Ok(()) } - -fn process_upload(mut request: Request, db_context: &DatabaseContext) -> anyhow::Result<()> { - if let Some(mut entry) = Multipart::from_request(&mut request) - .ok() - .and_then(|v| v.into_entry().into_result().ok()) - .flatten() - { - let mut data: Vec = Vec::with_capacity(20000); - entry.data.read_to_end(&mut data)?; - - let data_hash = Blake2s256::digest(&data); - - let txn = db_context.write_txn()?; - - let mut accessor = txn.access(); - - if accessor - .get::<[u8], [u8]>(&db_context.binary_store, data_hash.as_slice()) - .to_opt()? - .is_some() - { - request.respond(Response::from_string(base64_url::encode(&data_hash)))?; - return Ok(()); - } - - accessor.put( - &db_context.binary_store, - data_hash.as_slice(), - &data, - lmdb::put::Flags::empty(), - )?; - - let header = FileHeader { - file_name: entry.headers.filename, - content_type: entry - .headers - .content_type - .map(|v| v.essence_str().to_owned()), - }; - - accessor.put( - &db_context.metadata_store, - data_hash.as_slice(), - rkyv::to_bytes::<_, 256>(&header)?.as_slice(), - lmdb::put::Flags::empty(), - )?; - - request.respond(Response::from_string(base64_url::encode(&data_hash)))?; - - drop(accessor); - - txn.commit()?; - } - - Ok(()) -} - -fn process_get(request: Request, db_context: &DatabaseContext) -> anyhow::Result<()> { - if let Some(get_id) = request.url().strip_prefix("/get/").and_then(|s| { - let mut out: [u8; 32] = [0; 32]; - base64_url::decode_to_slice(s, &mut out).ok()?; - Some(out) - }) { - let read = db_context.read_txn()?; - let access = read.access(); - - if let Some(data) = access - .get::<[u8], [u8]>(&db_context.binary_store, &get_id) - .to_opt() - .unwrap() - { - let header_bytes = access - .get::<[u8], [u8]>(&db_context.metadata_store, &get_id) - .to_opt()? - .ok_or(anyhow::anyhow!("header missing for file"))?; - let header = unsafe { rkyv::archived_root::(header_bytes) }; - - let mut response = Response::new(200.into(), vec![], data, Some(data.len()), None); - - if let ArchivedOption::Some(ref file_name) = header.file_name { - response = response.with_header( - Header::from_bytes( - &b"Content-Disposition"[..], - format!("inline; filename=\"{file_name}\"").into_bytes(), - ) - .unwrap(), - ); - } - - if let ArchivedOption::Some(ref mime_type) = header.content_type { - response = response.with_header( - Header::from_bytes(&b"Content-Type"[..], mime_type.as_bytes()).unwrap(), - ); - } - - request.respond(response)?; - } else { - request.respond(Response::empty(404))?; - } - } else { - request.respond(Response::empty(400))?; - } - - Ok(()) -} diff --git a/src/routes/get.rs b/src/routes/get.rs new file mode 100644 index 0000000..3aea35a --- /dev/null +++ b/src/routes/get.rs @@ -0,0 +1,56 @@ +use crate::*; +use lmdb::LmdbResultExt; +use lmdb_zero as lmdb; + +use rkyv::option::ArchivedOption; + +use tiny_http::{Header, Request, Response}; + +fn get<'u>( + request: Request, + db_context: &DatabaseContext, + params: matchit::Params<'u, 'u>, +) -> CrowResult<()> { + let get_id = some_or_response!( + parse_base64_hash!(params.get("id")), or respond to request with Response::empty(400) + ); + + let read = db_context.read_txn()?; + let access = read.access(); + + let data = some_or_response!( + access + .get::<[u8], [u8]>(&db_context.binary_store, &get_id) + .to_opt()?, + or respond to request with + Response::empty(404) + ); + + let header = access + .get::<[u8], ArchivedFileHeader>(&db_context.metadata_store, &get_id) + .to_opt()? + .ok_or(anyhow::anyhow!("header missing for file"))?; + + let mut response = Response::new(200.into(), vec![], data, Some(data.len()), None); + + if let ArchivedOption::Some(ref file_name) = header.file_name { + response = response.with_header( + Header::from_bytes( + &b"Content-Disposition"[..], + format!("inline; filename=\"{file_name}\"").into_bytes(), + ) + .unwrap(), + ); + } + + if let ArchivedOption::Some(ref mime_type) = header.content_type { + response = response + .with_header(Header::from_bytes(&b"Content-Type"[..], mime_type.as_bytes()).unwrap()); + } + + request.respond(response)?; + + Ok(()) +} + +fn_to_handler!(get: GetHandler); diff --git a/src/routes/image.rs b/src/routes/image.rs new file mode 100644 index 0000000..e6852e3 --- /dev/null +++ b/src/routes/image.rs @@ -0,0 +1,200 @@ +use crate::*; +use blake2::{Blake2s256, Digest}; +use image::{ + codecs::{jpeg::JpegEncoder, png::PngEncoder}, + ColorType, ImageEncoder, +}; +use lmdb::LmdbResultExt; +use lmdb_zero as lmdb; +use multipart::server::Multipart; + +use std::{ + io::{ErrorKind, Read}, + ops::Deref, +}; +use tiny_http::{Header, Request, Response}; + +fn image_upload<'u>( + mut request: Request, + db_context: &DatabaseContext, + _: matchit::Params<'u, 'u>, +) -> CrowResult<()> { + let mut entry = some_or_response!( + single_multipart!(&mut request), or respond to request with Response::empty(400) + ); + + let mut data: Vec = Vec::with_capacity(20000); + entry.data.read_to_end(&mut data)?; + + let upload_format = image::guess_format(&data)?; + let img = image::load_from_memory_with_format(&data, upload_format)?; + + let data_hash = Blake2s256::digest(&img.as_bytes()); + + let txn = db_context.write_txn()?; + + let mut accessor = txn.access(); + + if accessor + .get::<[u8], [u8]>(&db_context.image_store, data_hash.as_slice()) + .to_opt()? + .is_some() + { + request.respond(Response::from_string(base64_url::encode(&data_hash)))?; + return Ok(()); + } + + let store_format = match upload_format { + image::ImageFormat::Jpeg => { + accessor.put( + &db_context.image_store, + data_hash.as_slice(), + &data, + lmdb::put::Flags::empty(), + )?; + + ImageFormat::JPG + } + _ => { + // TODO: allow lossy compression via option + let encoded = webp::Encoder::from_image(&img).unwrap().encode_lossless(); + accessor.put( + &db_context.image_store, + data_hash.as_slice(), + encoded.deref(), + lmdb::put::Flags::empty(), + )?; + + ImageFormat::WEBP + } + }; + + accessor.put( + &db_context.image_meta_store, + data_hash.as_slice(), + rkyv::to_bytes::<_, 256>(&ImageHeader { store_format }) + .unwrap() + .as_slice(), + lmdb::put::Flags::empty(), + )?; + + request.respond(Response::from_string(base64_url::encode(&data_hash)))?; + + drop(accessor); + + txn.commit()?; + + Ok(()) +} + +fn image_get<'u>( + mut request: Request, + db_context: &DatabaseContext, + params: matchit::Params<'u, 'u>, +) -> CrowResult<()> { + let (get_id, requested_format) = some_or_response!( + parse_base64_hash!(params.get("id")).zip( + params + .get("format") + .and_then(|s| ImageFormat::from_str(s).ok()) + ), or respond to request with Response::empty(400) + ); + + let content_type = requested_format.to_mime(); + + let read = db_context.read_txn()?; + let access = read.access(); + + let metadata: &ArchivedImageHeader = some_or_response!( + access + .get::<[u8], ArchivedImageHeader>(&db_context.image_meta_store, &get_id) + .to_opt()?, + or respond to request with + Response::empty(404) + ); + + let data = access.get::<[u8], [u8]>(&db_context.image_store, &get_id)?; + + if metadata.store_format == requested_format { + request.respond(Response::new( + 200.into(), + vec![], + data, + Some(data.len()), + None, + ))?; + + return Ok(()); + } + + let stored_image = image::load_from_memory_with_format(data, metadata.store_format.into())?; + + use ColorType::*; + + // catch unsupported transcoding (because of unsupported color format) + // and return a 500 + match (stored_image.color(), requested_format) { + (Rgb8 | Rgba8, ImageFormat::WEBP) => (), + (Rgb8 | Rgba8 | L8 | La8, ImageFormat::JPG) => (), + (Rgb8 | Rgba8 | L8 | La8 | Rgba16 | Rgb16 | L16 | La16, ImageFormat::PNG) => (), + _ => { + request.respond( + response!(err 500 "unsupported color channels for the requested image format"), + )?; + return Ok(()); + } + } + + let mut req_writer = request.extract_writer_impl(); + + let response = Response::new( + 200.into(), + vec![Header::from_bytes(&b"Content-Type"[..], content_type.as_bytes()).unwrap()], + std::io::empty(), + None, + None, + ); + Request::ignore_client_closing_errors(response.print_and_write( + &mut req_writer, + request.http_version().clone(), + request.headers(), + false, + None, + None, + |w, _| { + match requested_format { + ImageFormat::WEBP => { + // TODO: make compression quality here configurable + let mem = webp::Encoder::from_image(&stored_image) + .unwrap() + .encode(95.0); + w.write_all(mem.deref()) + } + ImageFormat::JPG => JpegEncoder::new(w) + .encode_image(&stored_image) + .map_err(|e| std::io::Error::new(ErrorKind::Other, e)), + ImageFormat::PNG => PngEncoder::new(w) + .write_image( + stored_image.as_bytes(), + stored_image.width(), + stored_image.height(), + stored_image.color(), + ) + .map_err(|e| std::io::Error::new(ErrorKind::Other, e)), + } + }, + ))?; + + Request::ignore_client_closing_errors(req_writer.flush())?; + if let Some(sender) = request.notify_when_responded.take() { + sender.send(()).unwrap(); + } + + drop(req_writer); + drop(request); + + Ok(()) +} + +fn_to_handler!(image_upload: ImageUploadHandler); +fn_to_handler!(image_get: ImageGetHandler); diff --git a/src/routes/mod.rs b/src/routes/mod.rs new file mode 100644 index 0000000..4b224e2 --- /dev/null +++ b/src/routes/mod.rs @@ -0,0 +1,3 @@ +pub mod get; +pub mod image; +pub mod upload; diff --git a/src/routes/upload.rs b/src/routes/upload.rs new file mode 100644 index 0000000..084a2e0 --- /dev/null +++ b/src/routes/upload.rs @@ -0,0 +1,68 @@ +use crate::*; +use blake2::{Blake2s256, Digest}; +use lmdb::LmdbResultExt; +use lmdb_zero as lmdb; +use multipart::server::Multipart; + +use std::io::Read; +use tiny_http::{Request, Response}; + +fn upload<'u>( + mut request: Request, + db_context: &DatabaseContext, + _: matchit::Params<'u, 'u>, +) -> CrowResult<()> { + let mut entry = some_or_response!( + single_multipart!(&mut request), or respond to request with Response::empty(400) + ); + + let mut data: Vec = Vec::with_capacity(20000); + entry.data.read_to_end(&mut data)?; + + let data_hash = Blake2s256::digest(&data); + + let txn = db_context.write_txn()?; + + let mut accessor = txn.access(); + + if accessor + .get::<[u8], [u8]>(&db_context.binary_store, data_hash.as_slice()) + .to_opt()? + .is_some() + { + request.respond(Response::from_string(base64_url::encode(&data_hash)))?; + return Ok(()); + } + + accessor.put( + &db_context.binary_store, + data_hash.as_slice(), + &data, + lmdb::put::Flags::empty(), + )?; + + let header = FileHeader { + file_name: entry.headers.filename, + content_type: entry + .headers + .content_type + .map(|v| v.essence_str().to_owned()), + }; + + accessor.put( + &db_context.metadata_store, + data_hash.as_slice(), + rkyv::to_bytes::<_, 256>(&header).unwrap().as_slice(), + lmdb::put::Flags::empty(), + )?; + + request.respond(Response::from_string(base64_url::encode(&data_hash)))?; + + drop(accessor); + + txn.commit()?; + + Ok(()) +} + +fn_to_handler!(upload: UploadHandler);