diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..2f54bc3
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "tests/data/vcard-personal-portfolio"]
+ path = tests/data/vcard-personal-portfolio
+ url = https://github.com/codewithsadee/vcard-personal-portfolio
diff --git a/.vscode/settings.json b/.vscode/settings.json
index c7bd1d1..7a3e930 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -2,5 +2,5 @@
"editor.formatOnSave": true,
"cSpell.words": [
"etag"
- ]
-}
\ No newline at end of file
+ ],
+}
diff --git a/Cargo.lock b/Cargo.lock
index d8b8605..0bb251c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4,9 +4,9 @@ version = 3
[[package]]
name = "addr2line"
-version = "0.21.0"
+version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
+checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678"
dependencies = [
"gimli",
]
@@ -17,89 +17,124 @@ 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.8.6"
+version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a"
+checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
dependencies = [
- "cfg-if",
+ "getrandom",
"once_cell",
"version_check",
- "zerocopy",
+]
+
+[[package]]
+name = "aliasable"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
+
+[[package]]
+name = "alloc-no-stdlib"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
+
+[[package]]
+name = "alloc-stdlib"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
+dependencies = [
+ "alloc-no-stdlib",
]
[[package]]
name = "anstream"
-version = "0.6.4"
+version = "0.6.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44"
+checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
+ "is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
-version = "1.0.4"
+version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87"
+checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
[[package]]
name = "anstyle-parse"
-version = "0.2.2"
+version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140"
+checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
-version = "1.0.0"
+version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
+checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391"
dependencies = [
- "windows-sys 0.48.0",
+ "windows-sys 0.52.0",
]
[[package]]
name = "anstyle-wincon"
-version = "3.0.1"
+version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628"
+checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
dependencies = [
"anstyle",
- "windows-sys 0.48.0",
+ "windows-sys 0.52.0",
]
[[package]]
name = "anyhow"
-version = "1.0.75"
+version = "1.0.86"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
+
+[[package]]
+name = "async-compression"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd066d0b4ef8ecb03a55319dc13aa6910616d0f44008a045bb1835af830abff5"
+dependencies = [
+ "brotli",
+ "flate2",
+ "futures-core",
+ "memchr",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "autocfg"
-version = "1.1.0"
+version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
[[package]]
name = "backtrace"
-version = "0.3.69"
+version = "0.3.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837"
+checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a"
dependencies = [
"addr2line",
"cc",
@@ -110,6 +145,12 @@ dependencies = [
"rustc-demangle",
]
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
[[package]]
name = "bitflags"
version = "1.3.2"
@@ -118,9 +159,21 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
-version = "2.4.1"
+version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
+checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
+
+[[package]]
+name = "bitvec"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
+dependencies = [
+ "funty",
+ "radium",
+ "tap",
+ "wyz",
+]
[[package]]
name = "block-buffer"
@@ -132,20 +185,66 @@ dependencies = [
]
[[package]]
-name = "bytes"
-version = "1.5.0"
+name = "brotli"
+version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
+checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+ "brotli-decompressor",
+]
[[package]]
-name = "cc"
-version = "1.0.83"
+name = "brotli-decompressor"
+version = "4.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
+checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362"
dependencies = [
- "libc",
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
+
+[[package]]
+name = "bytecheck"
+version = "0.6.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2"
+dependencies = [
+ "bytecheck_derive",
+ "ptr_meta",
+ "simdutf8",
+]
+
+[[package]]
+name = "bytecheck_derive"
+version = "0.6.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
]
+[[package]]
+name = "bytes"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
+
+[[package]]
+name = "cc"
+version = "1.0.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74b6a57f98764a267ff415d50a25e6e166f3831a5071af4995296ea97d210490"
+
[[package]]
name = "cfg-if"
version = "1.0.0"
@@ -154,18 +253,19 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
-version = "4.4.7"
+version = "4.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac495e00dcec98c83465d5ad66c5c4fabd652fd6686e7c6269b117e729a6f17b"
+checksum = "84b3edb18336f4df585bc9aa31dd99c036dfa5dc5e9a2939a722a188f3a8970d"
dependencies = [
"clap_builder",
+ "clap_derive",
]
[[package]]
name = "clap_builder"
-version = "4.4.7"
+version = "4.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c77ed9a32a62e6ca27175d00d29d05ca32e396ea1eb5fb01d8256b669cec7663"
+checksum = "c1c09dd5ada6c6c78075d6fd0da3f90d8080651e2d6cc8eb2f1aaa4034ced708"
dependencies = [
"anstream",
"anstyle",
@@ -173,52 +273,70 @@ dependencies = [
"strsim",
]
+[[package]]
+name = "clap_derive"
+version = "4.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085"
+dependencies = [
+ "heck 0.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+]
+
[[package]]
name = "clap_lex"
-version = "0.6.0"
+version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1"
+checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70"
[[package]]
name = "colorchoice"
-version = "1.0.0"
+version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
+checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
[[package]]
name = "colored"
-version = "2.0.4"
+version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2674ec482fbc38012cf31e6c42ba0177b431a0cb6f15fe40efa5aab1bda516f6"
+checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8"
dependencies = [
- "is-terminal",
"lazy_static",
"windows-sys 0.48.0",
]
[[package]]
-name = "core2"
-version = "0.4.0"
+name = "core-foundation"
+version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [
- "memchr",
+ "core-foundation-sys",
+ "libc",
]
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
+
[[package]]
name = "cpufeatures"
-version = "0.2.11"
+version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0"
+checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
dependencies = [
"libc",
]
[[package]]
name = "crc32fast"
-version = "1.3.2"
+version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
+checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
dependencies = [
"cfg-if",
]
@@ -233,17 +351,11 @@ dependencies = [
"typenum",
]
-[[package]]
-name = "dary_heap"
-version = "0.3.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7762d17f1241643615821a8455a0b2c3e803784b058693d990b11f2dce25a0ca"
-
[[package]]
name = "deranged"
-version = "0.3.9"
+version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3"
+checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [
"powerfmt",
]
@@ -260,18 +372,49 @@ dependencies = [
[[package]]
name = "either"
-version = "1.9.0"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
-version = "0.3.6"
+version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7c18ee0ed65a5f1f81cac6b1d213b69c35fa47d4252ad41f1486dbd8226fe36e"
+checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
dependencies = [
"libc",
- "windows-sys 0.48.0",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
+
+[[package]]
+name = "flate2"
+version = "1.0.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
]
[[package]]
@@ -280,43 +423,123 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "funty"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
+
+[[package]]
+name = "futures"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
[[package]]
name = "futures-channel"
-version = "0.3.29"
+version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb"
+checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
dependencies = [
"futures-core",
+ "futures-sink",
]
[[package]]
name = "futures-core"
-version = "0.3.29"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c"
+checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+]
[[package]]
name = "futures-sink"
-version = "0.3.29"
+version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817"
+checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
[[package]]
name = "futures-task"
-version = "0.3.29"
+version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2"
+checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
[[package]]
name = "futures-util"
-version = "0.3.29"
+version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104"
+checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
dependencies = [
+ "futures-channel",
"futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
"futures-task",
+ "memchr",
"pin-project-lite",
"pin-utils",
+ "slab",
]
[[package]]
@@ -329,23 +552,34 @@ dependencies = [
"version_check",
]
+[[package]]
+name = "getrandom"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
[[package]]
name = "gimli"
-version = "0.28.0"
+version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
+checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
[[package]]
name = "h2"
-version = "0.3.21"
+version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833"
+checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab"
dependencies = [
+ "atomic-waker",
"bytes",
"fnv",
"futures-core",
"futures-sink",
- "futures-util",
"http",
"indexmap",
"slab",
@@ -359,27 +593,39 @@ name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+dependencies = [
+ "ahash",
+]
[[package]]
name = "hashbrown"
-version = "0.13.2"
+version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
-dependencies = [
- "ahash",
-]
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
-version = "0.3.3"
+version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7"
+checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]]
name = "http"
-version = "0.2.9"
+version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
+checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
dependencies = [
"bytes",
"fnv",
@@ -388,20 +634,32 @@ dependencies = [
[[package]]
name = "http-body"
-version = "0.4.5"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
+checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
dependencies = [
"bytes",
+ "futures-util",
"http",
+ "http-body",
"pin-project-lite",
]
[[package]]
name = "httparse"
-version = "1.8.0"
+version = "1.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
+checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9"
[[package]]
name = "httpdate"
@@ -411,13 +669,12 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hyper"
-version = "0.14.27"
+version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468"
+checksum = "c4fe55fb7a772d59a5ff1dfbff4fe0258d19b89fec4b233e75d35d5d2316badc"
dependencies = [
"bytes",
"futures-channel",
- "futures-core",
"futures-util",
"h2",
"http",
@@ -426,105 +683,167 @@ dependencies = [
"httpdate",
"itoa",
"pin-project-lite",
- "socket2 0.4.10",
+ "smallvec",
"tokio",
- "tower-service",
- "tracing",
"want",
]
[[package]]
-name = "indexmap"
-version = "1.9.3"
+name = "hyper-rustls"
+version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155"
dependencies = [
- "autocfg",
- "hashbrown 0.12.3",
+ "futures-util",
+ "http",
+ "hyper",
+ "hyper-util",
+ "rustls",
+ "rustls-pki-types",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
]
[[package]]
-name = "is-terminal"
-version = "0.4.9"
+name = "hyper-tls"
+version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
+checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
dependencies = [
- "hermit-abi",
- "rustix",
- "windows-sys 0.48.0",
+ "bytes",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+ "tower-service",
]
[[package]]
-name = "itertools"
-version = "0.11.0"
+name = "hyper-util"
+version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
+checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956"
dependencies = [
- "either",
-]
-
-[[package]]
-name = "itoa"
-version = "1.0.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
-
-[[package]]
-name = "keccak"
-version = "0.1.4"
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "idna"
+version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940"
+checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
dependencies = [
- "cpufeatures",
+ "unicode-bidi",
+ "unicode-normalization",
]
[[package]]
-name = "lazy_static"
-version = "1.4.0"
+name = "include_bytes_aligned"
+version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+checksum = "e04cb7c34f1605722a91ca2ddf1fd071a9ce4fe1ac82d57fe36437331c87ec3b"
[[package]]
-name = "libc"
-version = "0.2.150"
+name = "indexmap"
+version = "2.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c"
+checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.14.5",
+]
[[package]]
-name = "libflate"
-version = "2.0.0"
+name = "ipnet"
+version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f7d5654ae1795afc7ff76f4365c2c8791b0feb18e8996a96adad8ffd7c3b2bf"
+checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
+
+[[package]]
+name = "itertools"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
dependencies = [
- "adler32",
- "core2",
- "crc32fast",
- "dary_heap",
- "libflate_lz77",
+ "either",
]
[[package]]
-name = "libflate_lz77"
-version = "2.0.0"
+name = "itertools"
+version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be5f52fb8c451576ec6b79d3f4deb327398bc05bbdbd99021a6e77a4c855d524"
+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
- "core2",
- "hashbrown 0.13.2",
- "rle-decode-fast",
+ "either",
]
+[[package]]
+name = "itoa"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
+
+[[package]]
+name = "js-sys"
+version = "0.3.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "keccak"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654"
+dependencies = [
+ "cpufeatures",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "libc"
+version = "0.2.155"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
+
[[package]]
name = "linux-raw-sys"
-version = "0.4.11"
+version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829"
+checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
[[package]]
name = "lock_api"
-version = "0.4.11"
+version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45"
+checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
dependencies = [
"autocfg",
"scopeguard",
@@ -532,15 +851,24 @@ dependencies = [
[[package]]
name = "log"
-version = "0.4.20"
+version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
+checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "memchr"
-version = "2.6.4"
+version = "2.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+
+[[package]]
+name = "memmap2"
+version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
+checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322"
+dependencies = [
+ "libc",
+]
[[package]]
name = "mime"
@@ -550,9 +878,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mime_guess"
-version = "2.0.4"
+version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
+checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
dependencies = [
"mime",
"unicase",
@@ -560,24 +888,47 @@ dependencies = [
[[package]]
name = "miniz_oxide"
-version = "0.7.1"
+version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
+checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08"
dependencies = [
"adler",
]
[[package]]
name = "mio"
-version = "0.8.9"
+version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0"
+checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
dependencies = [
"libc",
"wasi",
"windows-sys 0.48.0",
]
+[[package]]
+name = "native-tls"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466"
+dependencies = [
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+
[[package]]
name = "num_cpus"
version = "1.16.0"
@@ -590,33 +941,102 @@ dependencies = [
[[package]]
name = "num_threads"
-version = "0.1.6"
+version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
+checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
dependencies = [
"libc",
]
[[package]]
name = "object"
-version = "0.32.1"
+version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0"
+checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
-version = "1.18.0"
+version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
+checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
+
+[[package]]
+name = "openssl"
+version = "0.10.64"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f"
+dependencies = [
+ "bitflags 2.6.0",
+ "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 2.0.68",
+]
+
+[[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.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "ouroboros"
+version = "0.18.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "944fa20996a25aded6b4795c6d63f10014a7a83f8be9828a11860b08c5fc4a67"
+dependencies = [
+ "aliasable",
+ "ouroboros_macro",
+ "static_assertions",
+]
+
+[[package]]
+name = "ouroboros_macro"
+version = "0.18.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39b0deead1528fd0e5947a8546a9642a9777c25f6e1e26f34c97b204bbb465bd"
+dependencies = [
+ "heck 0.4.1",
+ "itertools 0.12.1",
+ "proc-macro2",
+ "proc-macro2-diagnostics",
+ "quote",
+ "syn 2.0.68",
+]
[[package]]
name = "parking_lot"
-version = "0.12.1"
+version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
+checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
dependencies = [
"lock_api",
"parking_lot_core",
@@ -624,22 +1044,48 @@ dependencies = [
[[package]]
name = "parking_lot_core"
-version = "0.9.9"
+version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e"
+checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
- "windows-targets",
+ "windows-targets 0.52.5",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+
+[[package]]
+name = "pin-project"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
]
[[package]]
name = "pin-project-lite"
-version = "0.2.13"
+version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
+checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
[[package]]
name = "pin-utils"
@@ -647,6 +1093,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+[[package]]
+name = "pkg-config"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
+
[[package]]
name = "powerfmt"
version = "0.2.0"
@@ -655,183 +1107,525 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "proc-macro2"
-version = "1.0.69"
+version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da"
+checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
dependencies = [
"unicode-ident",
]
[[package]]
-name = "quote"
-version = "1.0.33"
+name = "proc-macro2-diagnostics"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+ "version_check",
+ "yansi",
+]
+
+[[package]]
+name = "ptr_meta"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1"
+dependencies = [
+ "ptr_meta_derive",
+]
+
+[[package]]
+name = "ptr_meta_derive"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "radium"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd"
+dependencies = [
+ "bitflags 2.6.0",
+]
+
+[[package]]
+name = "rend"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c"
+dependencies = [
+ "bytecheck",
+]
+
+[[package]]
+name = "reqwest"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37"
+dependencies = [
+ "async-compression",
+ "base64",
+ "bytes",
+ "encoding_rs",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-tls",
+ "hyper-util",
+ "ipnet",
+ "js-sys",
+ "log",
+ "mime",
+ "native-tls",
+ "once_cell",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustls-pemfile",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "system-configuration",
+ "tokio",
+ "tokio-native-tls",
+ "tokio-util",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "winreg",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom",
+ "libc",
+ "spin",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rkyv"
+version = "0.7.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cba464629b3394fc4dbc6f940ff8f5b4ff5c7aef40f29166fd4ad12acbc99c0"
+dependencies = [
+ "bitvec",
+ "bytecheck",
+ "bytes",
+ "hashbrown 0.12.3",
+ "ptr_meta",
+ "rend",
+ "rkyv_derive",
+ "seahash",
+ "tinyvec",
+ "uuid",
+]
+
+[[package]]
+name = "rkyv_derive"
+version = "0.7.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7dddfff8de25e6f62b9d64e6e432bf1c6736c57d20323e15ee10435fbda7c65"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
+
+[[package]]
+name = "rustix"
+version = "0.38.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
+dependencies = [
+ "bitflags 2.6.0",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05cff451f60db80f490f3c182b77c35260baace73209e9cdbbe526bfe3a4d402"
+dependencies = [
+ "once_cell",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pemfile"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d"
+dependencies = [
+ "base64",
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d"
+
+[[package]]
+name = "rustls-webpki"
+version = "0.102.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "schannel"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "seahash"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
+
+[[package]]
+name = "security-framework"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0"
+dependencies = [
+ "bitflags 2.6.0",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.203"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.203"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.120"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5"
+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 = "sha3"
+version = "0.10.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60"
+dependencies = [
+ "digest",
+ "keccak",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
+checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
dependencies = [
- "proc-macro2",
+ "libc",
]
[[package]]
-name = "redox_syscall"
-version = "0.4.1"
+name = "simdutf8"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a"
+
+[[package]]
+name = "simple_logger"
+version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
+checksum = "e8c5dfa5e08767553704aa0ffd9d9794d527103c736aba9854773851fd7497eb"
dependencies = [
- "bitflags 1.3.2",
+ "colored",
+ "log",
+ "time",
+ "windows-sys 0.48.0",
]
[[package]]
-name = "rle-decode-fast"
-version = "1.0.3"
+name = "slab"
+version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422"
+checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
+dependencies = [
+ "autocfg",
+]
[[package]]
-name = "rustc-demangle"
-version = "0.1.23"
+name = "smallvec"
+version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
+checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
-name = "rustix"
-version = "0.38.21"
+name = "socket2"
+version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3"
+checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
dependencies = [
- "bitflags 2.4.1",
- "errno",
"libc",
- "linux-raw-sys",
- "windows-sys 0.48.0",
+ "windows-sys 0.52.0",
]
[[package]]
-name = "same-file"
-version = "1.0.6"
+name = "spin"
+version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
-dependencies = [
- "winapi-util",
-]
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
[[package]]
-name = "scopeguard"
-version = "1.2.0"
+name = "static_assertions"
+version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
-name = "serde"
-version = "1.0.192"
+name = "strsim"
+version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001"
-dependencies = [
- "serde_derive",
-]
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
-name = "serde_derive"
-version = "1.0.192"
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "unicode-ident",
]
[[package]]
-name = "sha3"
-version = "0.10.8"
+name = "syn"
+version = "2.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60"
+checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9"
dependencies = [
- "digest",
- "keccak",
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
]
[[package]]
-name = "signal-hook-registry"
-version = "1.4.1"
+name = "sync_wrapper"
+version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
-dependencies = [
- "libc",
-]
+checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394"
[[package]]
-name = "simple_logger"
-version = "4.2.0"
+name = "system-configuration"
+version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2230cd5c29b815c9b699fb610b49a5ed65588f3509d9f0108be3a885da629333"
+checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
dependencies = [
- "colored",
- "log",
- "time",
- "windows-sys 0.42.0",
+ "bitflags 1.3.2",
+ "core-foundation",
+ "system-configuration-sys",
]
[[package]]
-name = "slab"
-version = "0.4.9"
+name = "system-configuration-sys"
+version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
+checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
dependencies = [
- "autocfg",
+ "core-foundation-sys",
+ "libc",
]
[[package]]
-name = "smallvec"
-version = "1.11.1"
+name = "tap"
+version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a"
+checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
-name = "socket2"
-version = "0.4.10"
+name = "tempfile"
+version = "3.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d"
+checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
dependencies = [
- "libc",
- "winapi",
+ "cfg-if",
+ "fastrand",
+ "rustix",
+ "windows-sys 0.52.0",
]
[[package]]
-name = "socket2"
-version = "0.5.5"
+name = "test-case"
+version = "3.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9"
+checksum = "eb2550dd13afcd286853192af8601920d959b14c401fcece38071d53bf0768a8"
dependencies = [
- "libc",
- "windows-sys 0.48.0",
+ "test-case-macros",
]
[[package]]
-name = "strsim"
-version = "0.10.0"
+name = "test-case-core"
+version = "3.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f"
+dependencies = [
+ "cfg-if",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+]
[[package]]
-name = "syn"
-version = "2.0.39"
+name = "test-case-macros"
+version = "3.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a"
+checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb"
dependencies = [
"proc-macro2",
"quote",
- "unicode-ident",
+ "syn 2.0.68",
+ "test-case-core",
]
[[package]]
name = "time"
-version = "0.3.30"
+version = "0.3.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5"
+checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
dependencies = [
"deranged",
"itoa",
"libc",
+ "num-conv",
"num_threads",
"powerfmt",
"serde",
@@ -847,18 +1641,34 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
-version = "0.2.15"
+version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20"
+checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
dependencies = [
+ "num-conv",
"time-core",
]
+[[package]]
+name = "tinyvec"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c55115c6fbe2d2bef26eb09ad74bde02d8255476fc0c7b515ef09fbb35742d82"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
[[package]]
name = "tokio"
-version = "1.33.0"
+version = "1.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653"
+checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
dependencies = [
"backtrace",
"bytes",
@@ -868,36 +1678,77 @@ dependencies = [
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
- "socket2 0.5.5",
+ "socket2",
"tokio-macros",
"windows-sys 0.48.0",
]
[[package]]
name = "tokio-macros"
-version = "2.1.0"
+version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
+checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.68",
+]
+
+[[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-rustls"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
+dependencies = [
+ "rustls",
+ "rustls-pki-types",
+ "tokio",
]
[[package]]
name = "tokio-util"
-version = "0.7.10"
+version = "0.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15"
+checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1"
dependencies = [
"bytes",
"futures-core",
"futures-sink",
"pin-project-lite",
"tokio",
- "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",
+]
+
+[[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"
@@ -925,9 +1776,9 @@ dependencies = [
[[package]]
name = "try-lock"
-version = "0.2.4"
+version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "typenum"
@@ -944,17 +1795,61 @@ dependencies = [
"version_check",
]
+[[package]]
+name = "unicode-bidi"
+version = "0.3.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
+
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+[[package]]
+name = "unicode-normalization"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "url"
+version = "2.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+]
+
[[package]]
name = "utf8parse"
-version = "0.2.1"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "uuid"
+version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
+checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
@@ -964,9 +1859,9 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "walkdir"
-version = "2.4.0"
+version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
@@ -987,89 +1882,168 @@ 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.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
+
[[package]]
name = "web-static-pack"
-version = "0.4.4"
+version = "0.5.0-beta.1"
dependencies = [
"anyhow",
"http",
"http-body",
+ "http-body-util",
"hyper",
- "lazy_static",
- "log",
- "simple_logger",
+ "rkyv",
+ "test-case",
"tokio",
+ "web-static-pack-common",
+]
+
+[[package]]
+name = "web-static-pack-common"
+version = "0.5.0-beta.1"
+dependencies = [
+ "rkyv",
]
[[package]]
name = "web-static-pack-packer"
-version = "0.1.6"
+version = "0.5.0-beta.1"
dependencies = [
"anyhow",
- "bytes",
+ "brotli",
"clap",
- "itertools",
- "libflate",
- "log",
+ "flate2",
+ "itertools 0.13.0",
"mime_guess",
+ "rkyv",
"sha3",
- "simple_logger",
+ "test-case",
"walkdir",
+ "web-static-pack-common",
]
[[package]]
-name = "winapi"
-version = "0.3.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+name = "web-static-pack-tests"
+version = "0.5.0-beta.1"
dependencies = [
- "winapi-i686-pc-windows-gnu",
- "winapi-x86_64-pc-windows-gnu",
+ "anyhow",
+ "futures",
+ "http",
+ "hyper",
+ "hyper-util",
+ "include_bytes_aligned",
+ "log",
+ "memmap2",
+ "ouroboros",
+ "reqwest",
+ "simple_logger",
+ "test-case",
+ "tokio",
+ "web-static-pack",
+ "web-static-pack-common",
+ "web-static-pack-packer",
]
[[package]]
-name = "winapi-i686-pc-windows-gnu"
-version = "0.4.0"
+name = "web-sys"
+version = "0.3.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
[[package]]
name = "winapi-util"
-version = "0.1.6"
+version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596"
+checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b"
dependencies = [
- "winapi",
+ "windows-sys 0.52.0",
]
-[[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.42.0"
+version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
- "windows_aarch64_gnullvm 0.42.2",
- "windows_aarch64_msvc 0.42.2",
- "windows_i686_gnu 0.42.2",
- "windows_i686_msvc 0.42.2",
- "windows_x86_64_gnu 0.42.2",
- "windows_x86_64_gnullvm 0.42.2",
- "windows_x86_64_msvc 0.42.2",
+ "windows-targets 0.48.5",
]
[[package]]
name = "windows-sys"
-version = "0.48.0"
+version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
- "windows-targets",
+ "windows-targets 0.52.5",
]
[[package]]
@@ -1088,10 +2062,20 @@ dependencies = [
]
[[package]]
-name = "windows_aarch64_gnullvm"
-version = "0.42.2"
+name = "windows-targets"
+version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.5",
+ "windows_aarch64_msvc 0.52.5",
+ "windows_i686_gnu 0.52.5",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc 0.52.5",
+ "windows_x86_64_gnu 0.52.5",
+ "windows_x86_64_gnullvm 0.52.5",
+ "windows_x86_64_msvc 0.52.5",
+]
[[package]]
name = "windows_aarch64_gnullvm"
@@ -1100,10 +2084,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
-name = "windows_aarch64_msvc"
-version = "0.42.2"
+name = "windows_aarch64_gnullvm"
+version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
[[package]]
name = "windows_aarch64_msvc"
@@ -1112,10 +2096,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
-name = "windows_i686_gnu"
-version = "0.42.2"
+name = "windows_aarch64_msvc"
+version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
[[package]]
name = "windows_i686_gnu"
@@ -1124,10 +2108,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
-name = "windows_i686_msvc"
-version = "0.42.2"
+name = "windows_i686_gnu"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
[[package]]
name = "windows_i686_msvc"
@@ -1136,10 +2126,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
-name = "windows_x86_64_gnu"
-version = "0.42.2"
+name = "windows_i686_msvc"
+version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
[[package]]
name = "windows_x86_64_gnu"
@@ -1148,10 +2138,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
-name = "windows_x86_64_gnullvm"
-version = "0.42.2"
+name = "windows_x86_64_gnu"
+version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
[[package]]
name = "windows_x86_64_gnullvm"
@@ -1160,10 +2150,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
-name = "windows_x86_64_msvc"
-version = "0.42.2"
+name = "windows_x86_64_gnullvm"
+version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
[[package]]
name = "windows_x86_64_msvc"
@@ -1172,21 +2162,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
-name = "zerocopy"
-version = "0.7.25"
+name = "windows_x86_64_msvc"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
+
+[[package]]
+name = "winreg"
+version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8cd369a67c0edfef15010f980c3cbe45d7f651deac2cd67ce097cd801de16557"
+checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
dependencies = [
- "zerocopy-derive",
+ "cfg-if",
+ "windows-sys 0.48.0",
]
[[package]]
-name = "zerocopy-derive"
-version = "0.7.25"
+name = "wyz"
+version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2f140bda219a26ccc0cdb03dba58af72590c53b22642577d88a927bc5c87d6b"
+checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "tap",
]
+
+[[package]]
+name = "yansi"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
+
+[[package]]
+name = "zeroize"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
diff --git a/Cargo.toml b/Cargo.toml
index 622b494..59a6b5b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,3 +1,3 @@
[workspace]
-members = ["loader", "packer"]
+members = ["common", "loader", "packer", "tests"]
resolver = "2"
diff --git a/README.md b/README.md
index f17c715..f3c2d7f 100644
--- a/README.md
+++ b/README.md
@@ -1,97 +1,100 @@
# web-static-pack
-Embed static resources (GUI, assets, images, styles, html) within executable.
-Serve with hyper or any server of your choice.
-
-[![docs.rs](https://docs.rs/web-static-pack/badge.svg)](https://docs.rs/web-static-pack)
-
-## Usage scenario:
-- Combines given directory tree into single, fast, binary-based single-file representation, called `pack`. Use simple CLI tool `web-static-pack-packer` to create a pack.
-- Pack could be embedded into your application using `include_bytes!` single macro.
-- Super-fast, zero-copy `loader` provides by-name access to files.
-- Easy-to-use `hyper_loader` allows super-quick integration with hyper-based server.
-
-## Features:
-- Super fast, low overhead
-- 100% 'static access, zero data copy
-- 100% pack-time calculated `Content-Type`, `ETag` (using sha3)
-- 100% pack-time calculated gzip-compressed files
-- Almost no external dependencies
-
-## Limitations:
-- By default all files with guesses text/ content type are treated as utf-8
-- Packs are not guaranteed to be portable across versions / architectures
-
-## Future goals:
-- You tell me
-
-## Non-Goals:
-- Directory listings
-- automatic index.html resolving
-- Uploads
-
-## Example:
-1. Create a pack from `cargo doc`:
+web-static-pack is a set of tools for embedding static resources (GUI, assets, images, styles, html) inside your app, to be later served with a http server of your choice (like `hyper`).
+
+It consists of two parts:
+- [web-static-pack-packer](https://crates.io/crates/web-static-pack-packer) (aka "packer") - a standalone application (can be used as a library) used to serialize your assets into single file, called `pack`. It will usually be used before you build your target application (eg. in build script / CI / build.rs). During creation of a `pack` all heavy computations are done, eg. calculating `ETag`, compressed (`gzip`, `brotli`) versions, mime guessing etc. As a result a `pack` file is created, to be used by the next part.
+- [web-static-pack](https://github.com/peku33/web-static-pack) (aka "loader") - a library to include in your target application that will read the `pack` (preferably included in the application with ). Then `pack` can be used to form a `http` `service` (a function taking a request and returning response) serving files from the `pack`.
+
+## Features
+- Precomputed (in "packer") `ETag` (using `sha3`), compressed bodies (in `gzip` and `brotli` formats), `content-type`, etc. This reduces both runtime overhead and dependencies of your target application.
+- Zero-copy deserialization of a `pack` thanks to [rkyv](https://crates.io/crates/rkyv), allows the pack to be read directly from program memory, without allocating additional ram for pack contents.
+- `GET`/`HEAD` http methods support, `ETag`/`if-none-match` support, `accept-encoding`/`content-encoding` negotiation, `cache-control`, `content-length` etc.
+
+### Non goals
+- Directory listings.
+- index.html resolving.
+
+### Limitations
+- `pack` is not portable across crate versions / architectures.
+- Text files `text/*` are assumed to be utf-8 encoded.
+
+## Examples
+For this example lets assume you are building api + gui application in rust. Your gui is pre-built (like with `npm build` or similar) in `./vcard-personal-portfolio` directory (available for real in `tests/data/`) and you want to serve it from `/` of your app.
+
+### Packing your assets
+Refer to [web-static-pack-packer](https://crates.io/crates/web-static-pack-packer) for full documentation.
+
+To pack whole `./vcard-personal-portfolio` directory into `./vcard-personal-portfolio.pack` execute the following command:
```bash
-$ cargo doc --no-deps
-$ cargo run ./target/doc/ docs.pack
+$ web-static-pack-packer \
+ directory-single \
+ ./vcard-personal-portfolio \
+ ./vcard-personal-portfolio.pack
```
-
-2. Serve docs.pack from your web-application (see `examples/docs`)
+
+This will create a `./vcard-personal-portfolio.pack` file, containing all your files combined, ready to be included in your target application.
+
+### Serving from your target application
+Refer to [web-static-pack](https://github.com/peku33/web-static-pack) for full example.
+
+You will need to include the pack in your executable with . Then pack needs to be loaded from this binary slice. At the end we construct a http service, that will serve our requests.
+
```rust
-use anyhow::{Context, Error};
-use hyper::{
- service::{make_service_fn, service_fn},
- Body, Request, Response, Server,
-};
-use lazy_static::lazy_static;
-use log::LevelFilter;
-use simple_logger::SimpleLogger;
-use std::{convert::Infallible, include_bytes, net::SocketAddr};
-use web_static_pack::{hyper_loader::Responder, loader::Loader};
-
-#[tokio::main]
-async fn main() {
- SimpleLogger::new()
- .env()
- .with_level(LevelFilter::Info)
- .init()
- .unwrap();
- main_result().await.unwrap()
-}
+use include_bytes_aligned::include_bytes_aligned;
-async fn service(request: Request
) -> Result, Infallible> {
- lazy_static! {
- static ref PACK: &'static [u8] = include_bytes!("docs.pack");
- static ref LOADER: Loader = Loader::new(&PACK).unwrap();
- static ref RESPONDER: Responder<'static> = Responder::new(&LOADER);
- }
+static PACK_ARCHIVED_SERIALIZED: &[u8] =
+ include_bytes_aligned!(16, "vcard-personal-portfolio.pack");
- Ok(RESPONDER.request_respond(&request))
-}
+#[tokio::main(flavor = "current_thread")]
+async fn main() -> Result<(), Error> {
+ // load (map / cast) [common::pack::PackArchived] from included bytes
+ let pack_archived = unsafe { load(PACK_ARCHIVED_SERIALIZED).unwrap() };
+
+ // create a responder (http service) from `pack`
+ let responder = Responder::new(pack_archived);
-async fn main_result() -> Result<(), Error> {
- let address = SocketAddr::from(([0, 0, 0, 0], 8080));
- let server = Server::bind(&address).serve(make_service_fn(|_connection| async {
- Ok::<_, Infallible>(service_fn(service))
- }));
+ // hyper requires service to be static
+ // we use graceful, no connections will outlive server function
+ let responder = unsafe {
+ transmute::<
+ &Responder<'_, _>,
+ &Responder<'static, _>,
+ >(&responder)
+ };
- log::info!("Server listening on {:?}", address);
- server.await.context("server")?;
+ // make hyper service function
+ let service_fn = service_fn(|request: Request| async {
+ // you can probably filter your /api requests here
+ let (parts, _body) = request.into_parts();
+ let response = responder.respond_flatten(parts);
+ Ok::<_, Infallible>(response)
+ });
+
+ // run hyper server using service_fn, as in:
+ // https://hyper.rs/guides/1/server/graceful-shutdown/
+ todo!();
Ok(())
}
```
-# web-static-pack-packer
-Executable to build packs for web-static-pack crate
-See main crate for details
+## Migrating from 0.4.x to 0.5.x
+The 0.5.0 is almost a complete rewrite, however the general idea remains the same.
+- We still have two parts - packer and loader. There is also a `common` crate and `tests` crate, however they are not meant to be used directly.
+- Lots of internals were changed, including [rkyv](https://crates.io/crates/rkyv) for serialization / zero-copy deserialization. This of course makes packs built with previous versions incompatible with current loader and vice versa.
+- We are now built around [http](https://crates.io/crates/http) crate, which makes web-static-pack compatible with hyper 1.0 without depending on it directly.
-[![docs.rs](https://docs.rs/web-static-pack-packer/badge.svg)](https://docs.rs/web-static-pack-packer)
+### BREAKING CHANGES
+- Packer is now built around subcommands. The previous behavior was moved to `directory-single` subcommand, and `root_path` parameter was dropped. See examples.
+- Since we no longer depend on `hyper` in any way (the `http` crate is common interface), `hyper_loader` feature is no longer present in loader.
+- `let loader = loader::Loader::new(...)` is now `let pack_archived = loader::load(...)`. This value is still used for `Responder::new`.
+- `hyper_loader::Responder` is now just `responder::Responder`, and it's now built around `http` crate, compatible with `hyper` 1.0.
+- `Responder` was rewritten. It now accepts request parts (without body) not whole request. `request_respond_or_error` and `parts_respond_or_error` are now `respond`. `request_respond` and `parts_respond` are now `respond_flatten`.
-## Usage
-1. Install (`cargo install web-static-pack-packer`) or run locally (`cargo run`)
-2. Provide positional arguments:
- - `` - the directory to pack
- - `` - name of the build pack
- - `[root_pach]` - relative path to build pack paths with. use the same as `path` to have all paths in pack root
-3. Use `` file with `web-static-pack` (loader)
+### New features and improvements
+- True zero-copy deserialization with `rkyv`.
+- `brotli` compression support.
+- `cache-control` support.
+- Packer is now a lib + bin, making it usable in build.rs. Multiple useful methods were exposed.
+- Good test coverage, including integration tests.
+- Lots of internal improvements.
diff --git a/common/Cargo.toml b/common/Cargo.toml
new file mode 100644
index 0000000..88cd798
--- /dev/null
+++ b/common/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "web-static-pack-common"
+version = "0.5.0-beta.1"
+authors = ["Paweł Kubrak "]
+edition = "2021"
+license = "MIT"
+description = "Common types for web-static-pack and web-static-pack-packer crates."
+homepage = "https://github.com/peku33/web-static-pack"
+repository = "https://github.com/peku33/web-static-pack"
+readme = "README.md"
+keywords = ["web", "http", "static", "resources", "hyper"]
+categories = ["web-programming"]
+
+[dependencies]
+rkyv = { version = "0.7.44" } # features = ["copy", "copy_unsafe"]
diff --git a/common/README.md b/common/README.md
new file mode 100644
index 0000000..c8c8e60
--- /dev/null
+++ b/common/README.md
@@ -0,0 +1,25 @@
+# web-static-pack-common
+
+Common crate, containing types shared between
+[web-static-pack](https://crates.io/crates/web-static-pack) and
+[web-static-pack-packer](https://crates.io/crates/web-static-pack-packer).
+
+For a project documentation, examples, etc. see
+[web-static-pack](https://github.com/peku33/web-static-pack).
+
+The root type of this crate is [pack::Pack]. It's a collection (a hashmap)
+of files [file::File] distinguished by [pack_path::PackPath] (a custom type
+for path including some sanity checks).
+
+web-static-pack uses [rkyv] for serialization. Each module provides a rust
+native type, used during `pack` building, ex. [pack::Pack] and [rkyv]
+macro-generated zero-copy loadable (aka. mmapable) representation, eg.
+[pack::PackArchived], used by loader.
+
+#### Note
+
+There are also things called `Resolver` (eg. [pack::PackResolver]), that are
+needed internally by [rkyv], but are not used directly in this project. They
+should be hidden from docs.
+
+License: MIT
diff --git a/common/src/cache_control.rs b/common/src/cache_control.rs
new file mode 100644
index 0000000..4f43578
--- /dev/null
+++ b/common/src/cache_control.rs
@@ -0,0 +1,17 @@
+//! Cache control types used by file.
+
+use rkyv::{Archive, Serialize};
+
+/// Type representing cache control of a file. This will correspond to
+/// `cache-control` header set in http response.
+#[derive(Archive, Serialize, Clone, Copy, PartialEq, Eq, Debug)]
+#[archive(archived = "CacheControlArchived")]
+#[archive_attr(derive(Clone, Copy, PartialEq, Eq, Debug))]
+pub enum CacheControl {
+ /// No caching. This corresponds to "cache never" strategy, ex. by setting
+ /// `no-cache` header value.
+ NoCache,
+ /// Max caching. This corresponds to "cache forever" strategy, ex. by
+ /// setting `max-age=31536000, immutable` header value.
+ MaxCache,
+}
diff --git a/common/src/file.rs b/common/src/file.rs
new file mode 100644
index 0000000..0f137cb
--- /dev/null
+++ b/common/src/file.rs
@@ -0,0 +1,31 @@
+//! File represents single item of a Pack, accessible under specific path.
+
+use crate::cache_control::CacheControl;
+use rkyv::{Archive, Serialize};
+
+/// [File] represents an original file from filesystem with all fields
+/// precalculated. It contains `gzip` / `brotli` compressed content,
+/// precalculated http headers, like `content-type`, `ETag` and `cache-control`.
+///
+/// [File] is created in packing phase (once) to allow fast loading in loader
+/// without need to perform expensive computations (like calculating compressed
+/// forms) in runtime.
+#[derive(Archive, Serialize, Debug)]
+#[archive(archived = "FileArchived")]
+#[archive_attr(derive(Debug))]
+pub struct File {
+ /// Raw (not compressed) file contents.
+ pub content: Box<[u8]>,
+ /// Gzip compressed file contents, if provided, otherwise None.
+ pub content_gzip: Option>,
+ /// Brotli compressed file contents, if provided, otherwise None.
+ pub content_brotli: Option>,
+
+ /// `content-type` header contents for the file, eg. `text/html;
+ /// charset=utf-8` or `image/webp`.
+ pub content_type: String,
+ /// `ETag` header contents for the file, eg. checksum of `content`.
+ pub etag: String,
+ /// `cache-control` options for the file.
+ pub cache_control: CacheControl,
+}
diff --git a/common/src/lib.rs b/common/src/lib.rs
new file mode 100644
index 0000000..7236921
--- /dev/null
+++ b/common/src/lib.rs
@@ -0,0 +1,34 @@
+//! Common crate, containing types shared between
+//! [web-static-pack](https://crates.io/crates/web-static-pack) and
+//! [web-static-pack-packer](https://crates.io/crates/web-static-pack-packer).
+//!
+//! For a project documentation, examples, etc. see
+//! [web-static-pack](https://github.com/peku33/web-static-pack).
+//!
+//! The root type of this crate is [pack::Pack]. It's a collection (a hashmap)
+//! of files [file::File] distinguished by [pack_path::PackPath] (a custom type
+//! for path including some sanity checks).
+//!
+//! web-static-pack uses [rkyv] for serialization. Each module provides a rust
+//! native type, used during `pack` building, ex. [pack::Pack] and [rkyv]
+//! macro-generated zero-copy loadable (aka. mmapable) representation, eg.
+//! [pack::PackArchived], used by loader.
+//!
+//! ### Note
+//!
+//! There are also things called `Resolver` (eg. [pack::PackResolver]), that are
+//! needed internally by [rkyv], but are not used directly in this project. They
+//! should be hidden from docs.
+
+#![warn(missing_docs)]
+
+pub mod cache_control;
+pub mod file;
+pub mod pack;
+pub mod pack_path;
+
+/// File magic, used by loader to detect if content might not be a `pack`.
+pub const PACK_FILE_MAGIC: u64 = 0x479a01809f24813c;
+/// File version, used by loader to detect if loader and packer versions are
+/// compatible.
+pub const PACK_FILE_VERSION: u64 = 1;
diff --git a/common/src/pack.rs b/common/src/pack.rs
new file mode 100644
index 0000000..de9eed5
--- /dev/null
+++ b/common/src/pack.rs
@@ -0,0 +1,25 @@
+//! Pack is the root entity, a collection of files.
+
+use crate::{file::File, pack_path::PackPath};
+use rkyv::{Archive, Serialize};
+use std::collections::HashMap;
+
+/// Pack represents a group of files distinguished by their path.
+///
+/// [Pack] is a bit like a `zip`, single entity containing directory/file tree.
+/// [Pack] will usually be built with
+/// [web-static-pack-packer](https://crates.io/crates/web-static-pack-packer)
+/// crate, either with command line tool or by a script/build.rs. After [Pack]
+/// is built, it will be serialized (called "Archived" by [rkyv] we use for that
+/// purpose) into zero-copy deserializable representation - [PackArchived]. This
+/// representation will be then included by your target program into binary (or
+/// read/mmaped from fs) and served with
+/// [web-static-pack](https://crates.io/crates/web-static-pack)
+/// crate.
+#[derive(Archive, Serialize, Debug)]
+#[archive(archived = "PackArchived")]
+#[archive_attr(derive(Debug))]
+pub struct Pack {
+ /// List of contained files by their paths.
+ pub files_by_path: HashMap,
+}
diff --git a/common/src/pack_path.rs b/common/src/pack_path.rs
new file mode 100644
index 0000000..71cf344
--- /dev/null
+++ b/common/src/pack_path.rs
@@ -0,0 +1,51 @@
+//! Pack path contains custom type for representing path inside a `pack`.
+
+use rkyv::{Archive, Serialize};
+use std::{borrow::Borrow, ops::Deref};
+
+/// [PackPath] represents path inside a `pack`. It will correspond to http
+/// request path (aka. uri/url) directly.
+///
+/// Custom type is used to enforce some rules, eg. starts with "/", contains
+/// only valid characters, etc.
+#[derive(Archive, Serialize, PartialEq, Eq, Hash, Debug)]
+#[archive(archived = "PackPathArchived")]
+#[archive_attr(derive(PartialEq, Eq, Hash, Debug))]
+pub struct PackPath {
+ inner: String,
+}
+impl PackPath {
+ /// Construct path from string representation. Refer to [self] for details.
+ /// Providing invalid path won't result in catastrophic failure, but
+ /// such will will never be resolved.
+ pub fn from_string(inner: String) -> Self {
+ Self { inner }
+ }
+}
+
+// to allow searching in HashMap directly by http path (which is str)
+impl Deref for PackPath {
+ type Target = str;
+
+ fn deref(&self) -> &Self::Target {
+ &self.inner
+ }
+}
+impl Borrow for PackPath {
+ fn borrow(&self) -> &str {
+ self.inner.as_str()
+ }
+}
+
+impl Deref for PackPathArchived {
+ type Target = str;
+
+ fn deref(&self) -> &Self::Target {
+ &self.inner
+ }
+}
+impl Borrow for PackPathArchived {
+ fn borrow(&self) -> &str {
+ self.inner.as_str()
+ }
+}
diff --git a/loader/Cargo.toml b/loader/Cargo.toml
index 7d62f72..54c6746 100644
--- a/loader/Cargo.toml
+++ b/loader/Cargo.toml
@@ -1,29 +1,26 @@
[package]
name = "web-static-pack"
-version = "0.4.4"
+version = "0.5.0-beta.1"
authors = ["Paweł Kubrak "]
edition = "2021"
-description = "Embed static resources (GUI, assets, images, styles, html) within executable. Serve with hyper or any server of your choice."
license = "MIT"
+description = "Embed static resources (GUI, assets, images, styles, html) within executable. Serve with hyper or any server of your choice."
homepage = "https://github.com/peku33/web-static-pack"
repository = "https://github.com/peku33/web-static-pack"
-documentation = "https://docs.rs/web-static-pack"
readme = "README.md"
+keywords = ["web", "http", "static", "resources", "hyper"]
+categories = ["web-programming"]
[dependencies]
-anyhow = "1.0.75"
-log = "0.4.20"
+web-static-pack-common = { version = "0.5.0-beta.1", path = "../common" }
-# For hyper_loader
-http = { version = "0.2.9", optional = true }
-http-body = { version = "0.4.5", optional = true }
-hyper = { version = "0.14.27", features = ["full"], optional = true }
+anyhow = "1.0.86"
+http = "1.1.0"
+http-body = "1.0.0"
+rkyv = { version = "0.7.44" } # features = ["copy", "copy_unsafe"]
[dev-dependencies]
-lazy_static = "1.4.0"
-simple_logger = "4.2.0"
-tokio = { version = "1.33.0", features = ["full"] }
-
-[features]
-default = ["hyper_loader"]
-hyper_loader = ["hyper", "http-body", "http"]
+http-body-util = "0.1.2"
+hyper = { version = "1.4.0", features = ["full"] }
+test-case = "3.3.1"
+tokio = { version = "1.38.0", features = ["full"] }
diff --git a/loader/README.md b/loader/README.md
index fa8a293..cf832a9 100644
--- a/loader/README.md
+++ b/loader/README.md
@@ -1,83 +1,96 @@
# web-static-pack
-Embed static resources (GUI, assets, images, styles, html) within executable.
-Serve with hyper or any server of your choice.
-
-[![docs.rs](https://docs.rs/web-static-pack/badge.svg)](https://docs.rs/web-static-pack)
-
-## Usage scenario:
-- Combines given directory tree into single, fast, binary-based single-file representation, called `pack`. Use simple CLI tool `web-static-pack-packer` to create a pack.
-- Pack could be embedded into your application using `include_bytes!` single macro.
-- Super-fast, zero-copy `loader` provides by-name access to files.
-- Easy-to-use `hyper_loader` allows super-quick integration with hyper-based server.
-
-## Features:
-- Super fast, low overhead
-- 100% 'static access, zero data copy
-- 100% pack-time calculated `Content-Type`, `ETag` (using sha3)
-- 100% pack-time calculated gzip-compressed files
-- Almost no external dependencies
-
-## Limitations:
-- By default all files with guesses text/ content type are treated as utf-8
-- Packs are not guaranteed to be portable across versions / architectures
-
-## Future goals:
-- You tell me
-
-## Non-Goals:
-- Directory listings
-- automatic index.html resolving
-- Uploads
-
-## Example:
-1. Create a pack from `cargo doc`:
-```bash
-$ cargo doc --no-deps
-$ cargo run ./target/doc/ docs.pack
-```
-
-2. Serve docs.pack from your web-application (see `examples/docs`)
+
+web-static-pack is the "loader" (2nd stage) part of the
+[web-static-pack](https://github.com/peku33/web-static-pack)
+project. See project page for a general idea how two parts cooperate.
+
+Once a `pack` is created with build script / CI / build.rs using
+[web-static-pack-packer](https://crates.io/crates/web-static-pack-packer)
+it will usually be included in your target application with
+ .
+Then it will be loaded with a [loader::load], utilizing zero-copy
+deserialization (so file contents will be sent from executable contents
+directly). The pack is then possibly wrapped with [responder::Responder]
+http service and used with a web server like hyper.
+
+The main part of this crate is [responder::Responder]. Its
+[responder::Responder::respond_flatten] method makes a [http] service - a
+function taking [http::Request] (actually the [http::request::Parts], as we
+don't need the body) and returning [http::Response].
+
+To make a [responder::Responder], a [common::pack::Pack] is needed. It can
+be obtained by [loader::load] function by passing (possibly included in
+binary) contents of a `pack` created with the packer.
+
+## Examples
+
+### Creating and calling responder
```rust
-use anyhow::{Context, Error};
-use hyper::{
- service::{make_service_fn, service_fn},
- Body, Request, Response, Server,
-};
-use lazy_static::lazy_static;
-use log::LevelFilter;
-use simple_logger::SimpleLogger;
-use std::{convert::Infallible, include_bytes, net::SocketAddr};
-use web_static_pack::{hyper_loader::Responder, loader::Loader};
-
-#[tokio::main]
-async fn main() {
- SimpleLogger::new()
- .env()
- .with_level(LevelFilter::Info)
- .init()
- .unwrap();
- main_result().await.unwrap()
-}
+use anyhow::Error;
+use include_bytes_aligned::include_bytes_aligned;
+use http::StatusCode;
+use web_static_pack::{loader::load, responder::Responder};
+
+// assume we have a vcard-personal-portfolio.pack available from packer examples
+static PACK_ARCHIVED_SERIALIZED: &[u8] =
+ include_bytes_aligned!(16, "vcard-personal-portfolio.pack");
-async fn service(request: Request) -> Result, Infallible> {
- lazy_static! {
- static ref PACK: &'static [u8] = include_bytes!("docs.pack");
- static ref LOADER: Loader = Loader::new(&PACK).unwrap();
- static ref RESPONDER: Responder<'static> = Responder::new(&LOADER);
- }
+fn main() -> Result<(), Error> {
+ // load (map / cast) [common::pack::PackArchived] from included bytes
+ let pack_archived = unsafe { load(PACK_ARCHIVED_SERIALIZED).unwrap() };
- Ok(RESPONDER.request_respond(&request))
+ // create a responder from `pack`
+ let responder = Responder::new(pack_archived);
+
+ // do some checks on the responder
+ assert_eq!(
+ responder.respond_flatten().status(),
+ StatusCode::OK
+ );
+
+ Ok(())
}
+```
+
+### Adapting to hyper service
+This example is based on
+
+which is a bit complicated.
-async fn main_result() -> Result<(), Error> {
- let address = SocketAddr::from(([0, 0, 0, 0], 8080));
- let server = Server::bind(&address).serve(make_service_fn(|_connection| async {
- Ok::<_, Infallible>(service_fn(service))
- }));
+You can run full working example from
+`tests/examples/vcard_personal_portfolio_server.rs`
- log::info!("Server listening on {:?}", address);
- server.await.context("server")?;
+```rust
+use anyhow::Error;
+use web_static_pack::responder::Responder;
+use std::{
+ convert::Infallible,
+ mem::transmute,
+};
+
+#[tokio::main(flavor = "current_thread")]
+async fn main() -> Result<(), Error> {
+ // lets assume we have a `responder: Responder` object available from previous example
+ // hyper requires service to be static
+ // we use graceful, no connections will outlive server function
+ let responder = unsafe {
+ transmute::<
+ &Responder<'_, _>,
+ &Responder<'static, _>,
+ >(&responder)
+ };
+ // make hyper service
+ let service_fn = service_fn(|request: Request| async {
+ // you can probably filter your /api requests here
+ let (parts, _body) = request.into_parts();
+ let response = responder.respond_flatten(parts);
+ Ok::<_, Infallible>(response)
+ });
+
+ // use service_fn like in hyper example
Ok(())
}
```
+
+License: MIT
diff --git a/loader/examples/docs/.gitignore b/loader/examples/docs/.gitignore
deleted file mode 100644
index bf1d193..0000000
--- a/loader/examples/docs/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-docs.pack
diff --git a/loader/examples/docs/main.rs b/loader/examples/docs/main.rs
deleted file mode 100644
index e8443eb..0000000
--- a/loader/examples/docs/main.rs
+++ /dev/null
@@ -1,50 +0,0 @@
-//! Serves this package docs using tokio + hyper + web_static_pack.
-//!
-//! 1. Run local or install packer executable: `cargo run` or `cargo install web-static-pack-packer`
-//! 2. Create docs in `target/doc` directory: `cargo doc --no-deps`.
-//! 3. Run packer `web-static-pack-packer ./target/doc/ ./loader/examples/docs/docs.pack`.
-//! 4. Build & run this example `cargo run --example docs`.
-//! 5. Open http://localhost:8080/ in your browser.
-
-use anyhow::{Context, Error};
-use hyper::{
- service::{make_service_fn, service_fn},
- Body, Request, Response, Server,
-};
-use lazy_static::lazy_static;
-use log::LevelFilter;
-use simple_logger::SimpleLogger;
-use std::{convert::Infallible, include_bytes, net::SocketAddr};
-use web_static_pack::{hyper_loader::Responder, loader::Loader};
-
-#[tokio::main]
-async fn main() {
- SimpleLogger::new()
- .env()
- .with_level(LevelFilter::Info)
- .init()
- .unwrap();
- main_result().await.unwrap()
-}
-
-async fn service(request: Request) -> Result, Infallible> {
- lazy_static! {
- static ref PACK: &'static [u8] = include_bytes!("docs.pack");
- static ref LOADER: Loader = Loader::new(&PACK).unwrap();
- static ref RESPONDER: Responder<'static> = Responder::new(&LOADER);
- }
-
- Ok(RESPONDER.request_respond(&request))
-}
-
-async fn main_result() -> Result<(), Error> {
- let address = SocketAddr::from(([0, 0, 0, 0], 8080));
- let server = Server::bind(&address).serve(make_service_fn(|_connection| async {
- Ok::<_, Infallible>(service_fn(service))
- }));
-
- log::info!("Server listening on {:?}", address);
- server.await.context("server")?;
-
- Ok(())
-}
diff --git a/loader/src/body.rs b/loader/src/body.rs
new file mode 100644
index 0000000..a0cd1ab
--- /dev/null
+++ b/loader/src/body.rs
@@ -0,0 +1,85 @@
+//! [http] / [http_body] crate abstractions. Provides [Body], implementing
+//! [HttpBody] for raw bytes slice.
+
+use http_body::{Body as HttpBody, Frame, SizeHint};
+use std::{
+ convert::Infallible,
+ pin::Pin,
+ task::{Context, Poll},
+};
+
+/// [HttpBody] implementation for body consisting of a single in-memory slice.
+/// Implementation is based on `http_body_util::Full`.
+///
+/// Please note that once body is used, eg. polled with [Self::poll_frame], it
+/// will become empty, eg. [Self::data] will return empty slice.
+#[derive(Debug)]
+pub struct Body<'a> {
+ // None(empty slice) is not allowed.
+ data: Option<&'a [u8]>,
+}
+impl<'a> Body<'a> {
+ /// Creates [self] from data.
+ pub fn new(data: &'a [u8]) -> Self {
+ let data = if !data.is_empty() { Some(data) } else { None };
+ Self { data }
+ }
+
+ /// Creates empty [self].
+ pub fn empty() -> Self {
+ let data = None;
+ Self { data }
+ }
+
+ /// Returns remaining data.
+ ///
+ /// This will return original content until polled with [Self::poll_frame],
+ /// then it will return empty slice.
+ pub fn data(&self) -> &'a [u8] {
+ self.data.unwrap_or(b"")
+ }
+}
+impl<'a> HttpBody for Body<'a> {
+ type Data = &'a [u8];
+ type Error = Infallible;
+
+ fn poll_frame(
+ self: Pin<&mut Self>,
+ _cx: &mut Context<'_>,
+ ) -> Poll, Self::Error>>> {
+ let self_ = unsafe { self.get_unchecked_mut() };
+ let data = self_.data.take();
+
+ match data {
+ Some(data) => Poll::Ready(Some(Ok(Frame::data(data)))),
+ None => Poll::Ready(None),
+ }
+ }
+
+ fn is_end_stream(&self) -> bool {
+ self.data.is_none()
+ }
+
+ fn size_hint(&self) -> SizeHint {
+ match self.data {
+ Some(data) => SizeHint::with_exact(data.len() as u64),
+ None => SizeHint::with_exact(0),
+ }
+ }
+}
+
+#[cfg(test)]
+mod test_body {
+ use super::Body as Body_;
+ use http_body::Body;
+ use http_body_util::combinators::BoxBody;
+
+ /// we want to keep our body to be compatible with [BoxBody]
+ #[test]
+ fn body_converts_to_box_body() {
+ let body = Body_::new(b"foo");
+ let box_body = BoxBody::new(body);
+
+ assert_eq!(box_body.size_hint().lower(), 3);
+ }
+}
diff --git a/loader/src/cache_control.rs b/loader/src/cache_control.rs
new file mode 100644
index 0000000..9b941db
--- /dev/null
+++ b/loader/src/cache_control.rs
@@ -0,0 +1,40 @@
+//! Cache control related types. Provides [CacheControl].
+
+use crate::common::cache_control::{CacheControl as CacheControl_, CacheControlArchived};
+use http::HeaderValue;
+
+/// Cache control enumeration, used to generate `cache-control` header content.
+/// This will be usually built from [CacheControlArchived] or [CacheControl_]
+/// (via [From]).
+#[derive(Debug)]
+pub enum CacheControl {
+ /// Prevents caching file, by setting `no-cache` in `cache-control`.
+ NoCache,
+ /// Sets value to make resource cached for as long as possible.
+ MaxCache,
+}
+impl CacheControl {
+ /// Creates http [HeaderValue] from [self].
+ pub fn cache_control(&self) -> HeaderValue {
+ match self {
+ CacheControl::NoCache => HeaderValue::from_static("no-cache"),
+ CacheControl::MaxCache => HeaderValue::from_static("max-age=31536000, immutable"),
+ }
+ }
+}
+impl From for CacheControl {
+ fn from(value: CacheControl_) -> Self {
+ match value {
+ CacheControl_::NoCache => Self::NoCache,
+ CacheControl_::MaxCache => Self::MaxCache,
+ }
+ }
+}
+impl From for CacheControl {
+ fn from(value: CacheControlArchived) -> Self {
+ match value {
+ CacheControlArchived::NoCache => Self::NoCache,
+ CacheControlArchived::MaxCache => Self::MaxCache,
+ }
+ }
+}
diff --git a/loader/src/content_encoding.rs b/loader/src/content_encoding.rs
new file mode 100644
index 0000000..4ef66ff
--- /dev/null
+++ b/loader/src/content_encoding.rs
@@ -0,0 +1,274 @@
+//! Content encoding negotiation and content resolver types.
+
+use crate::file::File;
+use anyhow::{bail, Error};
+use http::{header, HeaderMap, HeaderValue};
+use std::cell::Cell;
+
+/// Describes accepted content encodings.
+///
+/// Should be created by parsing `accept-encoding` header, through one of
+/// `from_` methods.
+///
+/// `identity` is always considered to be accepted.
+#[derive(PartialEq, Eq, Debug)]
+pub struct EncodingAccepted {
+ /// Whether `gzip` encoding is accepted.
+ pub gzip: bool,
+ /// Whether `brotli` encoding is accepted.
+ pub brotli: bool,
+}
+impl EncodingAccepted {
+ /// Constructs [self] with none encoding (except for always available
+ /// identity) enabled.
+ pub fn none() -> Self {
+ Self {
+ gzip: false,
+ brotli: false,
+ }
+ }
+
+ /// Constructs [self] from [HeaderMap]. Inside it looks only for
+ /// `accept-encoding` header. May return error if header contains
+ /// invalid string.
+ pub fn from_headers(headers: &HeaderMap) -> Result {
+ let accept_encoding = match headers.get(header::ACCEPT_ENCODING) {
+ Some(accept_encoding) => accept_encoding,
+ None => return Ok(Self::none()),
+ };
+
+ let self_ = Self::from_accept_encoding_header_raw(accept_encoding)?;
+
+ Ok(self_)
+ }
+ /// Constructs [self] from [HeaderValue] for `accept-encoding` header. May
+ /// return error if header contains invalid string.
+ pub fn from_accept_encoding_header_raw(accept_encoding: &HeaderValue) -> Result {
+ let accept_encoding = match accept_encoding.to_str() {
+ Ok(accept_encoding) => accept_encoding,
+ Err(_) => bail!("unable to parse accept encoding as string"),
+ };
+
+ let self_ = Self::from_accept_encoding_header_str(accept_encoding);
+
+ Ok(self_)
+ }
+ /// Constructs [self] from `accept-encoding` header value.
+ pub fn from_accept_encoding_header_str(accept_encoding: &str) -> Self {
+ let mut gzip = false;
+ let mut brotli = false;
+
+ for accept_encoding in accept_encoding.split(", ") {
+ let accept_encoding = Self::extract_algorithm_from_value(accept_encoding);
+
+ match accept_encoding {
+ "gzip" => {
+ gzip = true;
+ }
+ "br" => {
+ brotli = true;
+ }
+ _ => {}
+ }
+ }
+
+ Self { gzip, brotli }
+ }
+
+ /// Removes `quality` or `preference` from header value.
+ /// eg. changes `gzip;q=0.5` to `gzip`
+ pub fn extract_algorithm_from_value(mut value: &str) -> &str {
+ if let Some((algorithm, _)) = value.split_once(";q=") {
+ value = algorithm;
+ }
+ value
+ }
+}
+
+#[cfg(test)]
+mod test_encoding_accepted {
+ use super::EncodingAccepted;
+ use http::{HeaderMap, HeaderName, HeaderValue};
+ use test_case::test_case;
+
+ #[test_case(&[], Some(EncodingAccepted::none()))]
+ #[test_case(&[("accept-encoding", "gzip")], Some(EncodingAccepted { gzip: true, brotli: false }))]
+ fn from_headers_returns_expected(
+ headers: &[(&'static str, &'static str)],
+ expected: Option,
+ ) {
+ let headers_map = headers
+ .iter()
+ .copied()
+ .map(|(key, value)| {
+ (
+ HeaderName::from_static(key),
+ HeaderValue::from_static(value),
+ )
+ })
+ .collect::();
+
+ assert_eq!(EncodingAccepted::from_headers(&headers_map).ok(), expected);
+ }
+
+ #[test_case(HeaderValue::from_bytes(b"\xff").unwrap(), None)]
+ #[test_case(HeaderValue::from_static(""), Some(EncodingAccepted { gzip: false, brotli: false }))]
+ #[test_case(HeaderValue::from_static("gzip, compress, br"), Some(EncodingAccepted { gzip: true, brotli: true }))]
+ fn from_accept_encoding_header_raw_returns_expected(
+ header_value: HeaderValue,
+ expected: Option,
+ ) {
+ assert_eq!(
+ EncodingAccepted::from_accept_encoding_header_raw(&header_value).ok(),
+ expected
+ );
+ }
+
+ #[test_case("", EncodingAccepted { gzip: false, brotli: false })]
+ #[test_case("gzip", EncodingAccepted { gzip: true, brotli: false })]
+ #[test_case("br", EncodingAccepted { gzip: false, brotli: true })]
+ #[test_case("deflate, gzip;q=1.0", EncodingAccepted { gzip: true, brotli: false })]
+ fn from_accept_encoding_header_str_returns_expected(
+ accept_encoding: &str,
+ expected: EncodingAccepted,
+ ) {
+ assert_eq!(
+ EncodingAccepted::from_accept_encoding_header_str(accept_encoding),
+ expected
+ );
+ }
+
+ #[test_case("", "")]
+ #[test_case("gzip", "gzip")]
+ #[test_case("gzip;q=1.0", "gzip")]
+ fn extract_algorithm_from_value_returns_expected(
+ value: &str,
+ expected: &str,
+ ) {
+ assert_eq!(
+ EncodingAccepted::extract_algorithm_from_value(value),
+ expected
+ );
+ }
+}
+
+/// Represents content in resolved content encoding. This should be created by
+/// calling [Self::resolve], providing [EncodingAccepted] from request header
+/// and [File].
+#[derive(PartialEq, Eq, Debug)]
+pub struct ContentContentEncoding<'c> {
+ /// content (body) that should be sent in response
+ pub content: &'c [u8],
+ /// `content-encoding` header value that should be sent in response
+ pub content_encoding: HeaderValue,
+}
+impl<'c> ContentContentEncoding<'c> {
+ /// Based on accepted encodings from [EncodingAccepted] and available from
+ /// [File] resolves best (currently *smallest*) content.
+ pub fn resolve(
+ encoding_accepted: &EncodingAccepted,
+ file: &'c impl File,
+ ) -> Self {
+ let mut best = Cell::new(ContentContentEncoding {
+ content: file.content(),
+ content_encoding: HeaderValue::from_static("identity"),
+ });
+
+ // gzip
+ if encoding_accepted.gzip
+ && let Some(content_gzip) = file.content_gzip()
+ && content_gzip.len() <= best.get_mut().content.len()
+ {
+ best.set(ContentContentEncoding {
+ content: content_gzip,
+ content_encoding: HeaderValue::from_static("gzip"),
+ });
+ }
+
+ // brotli
+ if encoding_accepted.brotli
+ && let Some(content_brotli) = file.content_brotli()
+ && content_brotli.len() <= best.get_mut().content.len()
+ {
+ best.set(ContentContentEncoding {
+ content: content_brotli,
+ content_encoding: HeaderValue::from_static("br"),
+ });
+ }
+
+ best.into_inner()
+ }
+}
+
+#[cfg(test)]
+mod test_content_content_encoding {
+ use super::{ContentContentEncoding, EncodingAccepted};
+ use crate::{cache_control::CacheControl, file::File};
+ use http::HeaderValue;
+ use test_case::test_case;
+
+ #[derive(Debug)]
+ pub struct FileMock {
+ pub content: &'static [u8],
+ pub content_gzip: Option<&'static [u8]>,
+ pub content_brotli: Option<&'static [u8]>,
+ }
+ impl File for FileMock {
+ fn content(&self) -> &[u8] {
+ self.content
+ }
+ fn content_gzip(&self) -> Option<&[u8]> {
+ self.content_gzip
+ }
+ fn content_brotli(&self) -> Option<&[u8]> {
+ self.content_brotli
+ }
+
+ fn content_type(&self) -> HeaderValue {
+ unimplemented!()
+ }
+
+ fn etag(&self) -> HeaderValue {
+ unimplemented!()
+ }
+
+ fn cache_control(&self) -> CacheControl {
+ unimplemented!()
+ }
+ }
+
+ #[test_case(
+ EncodingAccepted { gzip: false, brotli: false },
+ FileMock { content: b"content-identity", content_gzip: None, content_brotli: None },
+ ContentContentEncoding {content: b"content-identity", content_encoding: HeaderValue::from_static("identity") } ;
+ "nothing provided, nothing accepted"
+ )]
+ #[test_case(
+ EncodingAccepted { gzip: false, brotli: false },
+ FileMock { content: b"content-identity", content_gzip: Some(b"content-gzip"), content_brotli: Some(b"content-brotli") },
+ ContentContentEncoding {content: b"content-identity", content_encoding: HeaderValue::from_static("identity") } ;
+ "all provided, nothing accepted"
+ )]
+ #[test_case(
+ EncodingAccepted { gzip: true, brotli: true },
+ FileMock { content: b"content-identity", content_gzip: None, content_brotli: None },
+ ContentContentEncoding {content: b"content-identity", content_encoding: HeaderValue::from_static("identity") } ;
+ "all accepted, nothing provided"
+ )]
+ #[test_case(
+ EncodingAccepted { gzip: true, brotli: true },
+ FileMock { content: b"content-aaa", content_gzip: Some(b"content-bb"), content_brotli: Some(b"content-c") },
+ ContentContentEncoding {content: b"content-c", content_encoding: HeaderValue::from_static("br") } ;
+ "brotli should win as the shortest"
+ )]
+ fn resolve_returns_expected(
+ encoding_accepted: EncodingAccepted,
+ content: FileMock,
+ expected: ContentContentEncoding,
+ ) {
+ assert_eq!(
+ ContentContentEncoding::resolve(&encoding_accepted, &content),
+ expected
+ );
+ }
+}
diff --git a/loader/src/file.rs b/loader/src/file.rs
new file mode 100644
index 0000000..3a249c2
--- /dev/null
+++ b/loader/src/file.rs
@@ -0,0 +1,76 @@
+//! Single file related types. Provides [File] trait.
+
+use crate::{
+ cache_control::CacheControl,
+ common::file::{File as File_, FileArchived},
+};
+use http::HeaderValue;
+
+/// Trait for single file inside a `pack`. Consists of body in different encodings
+/// (`identity` aka `normal`, `gzip`, `brotli`), some precomputed header values
+/// etc.
+///
+/// Most users will indirectly use [FileArchived] implementation, obtained from
+/// [crate::pack::Pack::get_file_by_path] (implemented by
+/// [crate::common::pack::PackArchived]).
+/// This trait is also implemented for non-archived [File_], mostly for testing
+/// purposes.
+pub trait File {
+ // content with different types
+ /// Accesses file content in original (`identity`) encoding.
+ fn content(&self) -> &[u8];
+ /// Accesses file content in `gzip` encoding if available.
+ fn content_gzip(&self) -> Option<&[u8]>;
+ /// Accesses file content in `brotli` encoding if available.
+ fn content_brotli(&self) -> Option<&[u8]>;
+
+ // headers
+ /// Accesses `content-type` header contents for this file.
+ fn content_type(&self) -> HeaderValue;
+ /// Accesses `ETag` header contents for this file.
+ fn etag(&self) -> HeaderValue;
+ /// Accesses [CacheControl] for this file.
+ fn cache_control(&self) -> CacheControl;
+}
+impl File for File_ {
+ fn content(&self) -> &[u8] {
+ &self.content
+ }
+ fn content_gzip(&self) -> Option<&[u8]> {
+ self.content_gzip.as_deref()
+ }
+ fn content_brotli(&self) -> Option<&[u8]> {
+ self.content_brotli.as_deref()
+ }
+
+ fn content_type(&self) -> HeaderValue {
+ HeaderValue::from_str(&self.content_type).unwrap()
+ }
+ fn etag(&self) -> HeaderValue {
+ HeaderValue::from_str(&self.etag).unwrap()
+ }
+ fn cache_control(&self) -> CacheControl {
+ CacheControl::from(self.cache_control)
+ }
+}
+impl File for FileArchived {
+ fn content(&self) -> &[u8] {
+ &self.content
+ }
+ fn content_gzip(&self) -> Option<&[u8]> {
+ self.content_gzip.as_deref()
+ }
+ fn content_brotli(&self) -> Option<&[u8]> {
+ self.content_brotli.as_deref()
+ }
+
+ fn content_type(&self) -> HeaderValue {
+ HeaderValue::from_str(&self.content_type).unwrap()
+ }
+ fn etag(&self) -> HeaderValue {
+ HeaderValue::from_str(&self.etag).unwrap()
+ }
+ fn cache_control(&self) -> CacheControl {
+ CacheControl::from(self.cache_control)
+ }
+}
diff --git a/loader/src/hyper_loader.rs b/loader/src/hyper_loader.rs
deleted file mode 100644
index e26329b..0000000
--- a/loader/src/hyper_loader.rs
+++ /dev/null
@@ -1,165 +0,0 @@
-//! Hyper integration.
-//! See examples/docs/main.rs for usage sample.
-//!
-//! Entry point for this module is `Responder`.
-//! Create `Responder` providing reference to `Loader`.
-//! Use `request_respond()` method to serve file in response to request.
-
-use super::loader::Loader;
-use http::{header, HeaderMap, Method, StatusCode, Uri};
-use hyper::{Body, Request, Response};
-
-/// Possible errors during `Responder` handling
-pub enum ResponderError {
- /// Not supported HTTP Method, this maps to HTTP `METHOD_NOT_ALLOWED`.
- HttpMethodNotSupported,
-
- /// Request URI was not found in `Loader`. This maps to HTTP `NOT_FOUND`.
- LoaderPathNotFound,
-
- /// Error while parsing HTTP `Accept-Encoding`. This maps to HTTP `BAD_REQUEST`.
- UnparsableAcceptEncoding,
-}
-impl ResponderError {
- /// Converts error into best matching HTTP error code
- pub fn as_http_status_code(&self) -> StatusCode {
- match self {
- ResponderError::HttpMethodNotSupported => StatusCode::METHOD_NOT_ALLOWED,
- ResponderError::LoaderPathNotFound => StatusCode::NOT_FOUND,
- ResponderError::UnparsableAcceptEncoding => StatusCode::BAD_REQUEST,
- }
- }
-
- /// Creates default response (status code + empty body) for this error.
- pub fn as_default_response(&self) -> Response {
- Response::builder()
- .status(self.as_http_status_code())
- .body(Body::default())
- .unwrap()
- }
-}
-
-/// Main class for hyper integration.
-/// Given `Loader`, responds to incoming requests serving files from `Loader`.
-pub struct Responder<'l> {
- loader: &'l Loader,
-}
-impl<'l> Responder<'l> {
- /// Creates instance, using provided `Loader`.
- pub fn new(loader: &'l Loader) -> Self {
- Self { loader }
- }
-
- /// Given basic hyper request, responds to it, or returns `ResponderError`.
- /// To automatically cast `ResponderError` to response, use `request_respond` instead.
- pub fn request_respond_or_error(
- &self,
- request: &Request,
- ) -> Result, ResponderError> {
- self.parts_respond_or_error(request.method(), request.uri(), request.headers())
- }
-
- /// Given set of parts (`method`, `uri` and `headers`), responds to it, or returns `ResponderError`.
- /// To automatically cast `ResponderError` to response, use `parts_respond` instead.
- pub fn parts_respond_or_error(
- &self,
- method: &Method,
- uri: &Uri,
- headers: &HeaderMap,
- ) -> Result, ResponderError> {
- // Only GET requests are allowed.
- // TODO: Handle HEAD requests.
- match *method {
- Method::GET => (),
- _ => {
- return Err(ResponderError::HttpMethodNotSupported);
- }
- };
-
- // Find file for given request.
- let file_descriptor = match self.loader.get(uri.path()) {
- Some(file_descriptor) => file_descriptor,
- None => {
- return Err(ResponderError::LoaderPathNotFound);
- }
- };
-
- // Check for possible ETag.
- // If ETag exists and matches current file, return 304.
- if let Some(etag_request) = headers.get(header::IF_NONE_MATCH) {
- if etag_request.as_bytes() == file_descriptor.etag().as_bytes() {
- return Ok(Response::builder()
- .status(StatusCode::NOT_MODIFIED)
- .body(Body::default())
- .unwrap());
- }
- };
-
- // Check accepted encodings
- let mut accepted_encoding_gzip = false;
- if let Some(accept_encoding) = headers.get(header::ACCEPT_ENCODING) {
- let accept_encoding = match accept_encoding.to_str() {
- Ok(accept_encoding) => accept_encoding,
- Err(_) => {
- return Err(ResponderError::UnparsableAcceptEncoding);
- }
- };
-
- #[allow(clippy::single_match)]
- accept_encoding
- .split(", ")
- .for_each(|accept_encoding| match accept_encoding {
- "gzip" => {
- accepted_encoding_gzip = true;
- }
- _ => {}
- });
- }
-
- // Select data based on accepted encoding
- // (chunk, content_encoding)
- let (content, content_encoding) =
- if accepted_encoding_gzip && file_descriptor.content_gzip().is_some() {
- (file_descriptor.content_gzip().unwrap(), "gzip")
- } else {
- (file_descriptor.content(), "identity")
- };
-
- // Provide response.
- let response = Response::builder()
- .header(header::CONTENT_TYPE, file_descriptor.content_type())
- .header(header::CONTENT_LENGTH, content.len())
- .header(header::CONTENT_ENCODING, content_encoding)
- .header(header::ETAG, file_descriptor.etag())
- .body(Body::from(content))
- .unwrap();
-
- Ok(response)
- }
-
- /// Given basic hyper request, responds to it.
- /// In case of error creates default http response. If specific error control is needed, use `request_respond_or_error` instead.
- pub fn request_respond(
- &self,
- request: &Request,
- ) -> Response {
- match self.request_respond_or_error(request) {
- Ok(response) => response,
- Err(error) => error.as_default_response(),
- }
- }
-
- /// Given set of parts (`method`, `uri` and `headers`), responds to it.
- /// In case of error creates default http response. If specific error control is needed, use `parts_respond_or_error` instead.
- pub fn parts_respond(
- &self,
- method: &Method,
- uri: &Uri,
- headers: &HeaderMap,
- ) -> Response {
- match self.parts_respond_or_error(method, uri, headers) {
- Ok(response) => response,
- Err(error) => error.as_default_response(),
- }
- }
-}
diff --git a/loader/src/lib.rs b/loader/src/lib.rs
index 6c98029..20dd9c3 100644
--- a/loader/src/lib.rs
+++ b/loader/src/lib.rs
@@ -1,88 +1,106 @@
-//! # web-static-pack
-//! Embed static resources (GUI, assets, images, styles, html) within executable.
-//! Serve with hyper or any server of your choice.
-//!
-//! [![docs.rs](https://docs.rs/web-static-pack/badge.svg)](https://docs.rs/web-static-pack)
-//!
-//! ## Usage scenario:
-//! - Combines given directory tree into single, fast, binary-based single-file representation, called `pack`. Use simple CLI tool `web-static-pack-packer` to create a pack.
-//! - Pack could be embedded into your application using `include_bytes!` single macro.
-//! - Super-fast, zero-copy `loader` provides by-name access to files.
-//! - Easy-to-use `hyper_loader` allows super-quick integration with hyper-based server.
-//!
-//! ## Features:
-//! - Super fast, low overhead
-//! - 100% 'static access, zero data copy
-//! - 100% pack-time calculated `Content-Type`, `ETag` (using sha3)
-//! - 100% pack-time calculated gzip-compressed files
-//! - Almost no external dependencies
-//!
-//! ## Limitations:
-//! - By default all files with guesses text/ content type are treated as utf-8
-//! - Packs are not guaranteed to be portable across versions / architectures
-//!
-//! ## Future goals:
-//! - You tell me
-//!
-//! ## Non-Goals:
-//! - Directory listings
-//! - automatic index.html resolving
-//! - Uploads
-//!
-//! ## Example:
-//! 1. Create a pack from `cargo doc`:
-//! ```
-//! $ cargo doc --no-deps
-//! $ cargo run ./target/doc/ docs.pack
-//! ```
-//!
-//! 2. Serve docs.pack from your web-application (see `examples/docs`)
-//! ```
-//! use anyhow::{Context, Error};
-//! use hyper::{
-//! service::{make_service_fn, service_fn},
-//! Body, Request, Response, Server,
-//! };
-//! use lazy_static::lazy_static;
-//! use log::LevelFilter;
-//! use simple_logger::SimpleLogger;
-//! use std::{convert::Infallible, include_bytes, net::SocketAddr};
-//! use web_static_pack::{hyper_loader::Responder, loader::Loader};
-//!
-//! #[tokio::main]
-//! async fn main() {
-//! SimpleLogger::new()
-//! .env()
-//! .with_level(LevelFilter::Info)
-//! .init()
-//! .unwrap();
-//! main_result().await.unwrap()
-//! }
+//! web-static-pack is the "loader" (2nd stage) part of the
+//! [web-static-pack](https://github.com/peku33/web-static-pack)
+//! project. See project page for a general idea how two parts cooperate.
+//!
+//! Once a `pack` is created with build script / CI / build.rs using
+//! [web-static-pack-packer](https://crates.io/crates/web-static-pack-packer)
+//! it will usually be included in your target application with
+//! .
+//! Then it will be loaded with a [loader::load], utilizing zero-copy
+//! deserialization (so file contents will be sent from executable contents
+//! directly). The pack is then possibly wrapped with [responder::Responder]
+//! http service and used with a web server like hyper.
+//!
+//! The main part of this crate is [responder::Responder]. Its
+//! [responder::Responder::respond_flatten] method makes a [http] service - a
+//! function taking [http::Request] (actually the [http::request::Parts], as we
+//! don't need the body) and returning [http::Response].
+//!
+//! To make a [responder::Responder], a [common::pack::Pack] is needed. It can
+//! be obtained by [loader::load] function by passing (possibly included in
+//! binary) contents of a `pack` created with the packer.
+//!
+//! # Examples
+//!
+//! ## Creating and calling responder
+//! ```ignore
+//! use anyhow::Error;
+//! use include_bytes_aligned::include_bytes_aligned;
+//! use http::StatusCode;
+//! use web_static_pack::{loader::load, responder::Responder};
+//!
+//! // assume we have a vcard-personal-portfolio.pack available from packer examples
+//! static PACK_ARCHIVED_SERIALIZED: &[u8] =
+//! include_bytes_aligned!(16, "vcard-personal-portfolio.pack");
+//!
+//! fn main() -> Result<(), Error> {
+//! // load (map / cast) [common::pack::PackArchived] from included bytes
+//! let pack_archived = unsafe { load(PACK_ARCHIVED_SERIALIZED).unwrap() };
//!
-//! async fn service(request: Request) -> Result, Infallible> {
-//! lazy_static! {
-//! static ref PACK: &'static [u8] = include_bytes!("docs.pack");
-//! static ref LOADER: Loader = Loader::new(&PACK).unwrap();
-//! static ref RESPONDER: Responder<'static> = Responder::new(&LOADER);
-//! }
+//! // create a responder from `pack`
+//! let responder = Responder::new(pack_archived);
//!
-//! Ok(RESPONDER.request_respond(&request))
+//! // do some checks on the responder
+//! assert_eq!(
+//! responder.respond_flatten().status(),
+//! StatusCode::OK
+//! );
+//!
+//! Ok(())
//! }
+//! ```
//!
-//! async fn main_result() -> Result<(), Error> {
-//! let address = SocketAddr::from(([0, 0, 0, 0], 8080));
-//! let server = Server::bind(&address).serve(make_service_fn(|_connection| async {
-//! Ok::<_, Infallible>(service_fn(service))
-//! }));
+//! ## Adapting to hyper service
+//! This example is based on
+//!
+//! which is a bit complicated.
+//!
+//! You can run full working example from
+//! `tests/examples/vcard_personal_portfolio_server.rs`
+//!
+//! ```ignore
+//! use anyhow::Error;
+//! use web_static_pack::responder::Responder;
+//! use std::{
+//! convert::Infallible,
+//! mem::transmute,
+//! };
//!
-//! log::info!("Server listening on {:?}", address);
-//! server.await.context("server")?;
-//!
+//! #[tokio::main(flavor = "current_thread")]
+//! async fn main() -> Result<(), Error> {
+//! // lets assume we have a `responder: Responder` object available from previous example
+//! // hyper requires service to be static
+//! // we use graceful, no connections will outlive server function
+//! let responder = unsafe {
+//! transmute::<
+//! &Responder<'_, _>,
+//! &Responder<'static, _>,
+//! >(&responder)
+//! };
+//!
+//! // make hyper service
+//! let service_fn = service_fn(|request: Request| async {
+//! // you can probably filter your /api requests here
+//! let (parts, _body) = request.into_parts();
+//! let response = responder.respond_flatten(parts);
+//! Ok::<_, Infallible>(response)
+//! });
+//!
+//! // use service_fn like in hyper example
//! Ok(())
//! }
//! ```
-pub mod loader;
+#![feature(let_chains)]
+#![allow(clippy::new_without_default)]
+#![warn(missing_docs)]
-#[cfg(feature = "hyper_loader")]
-pub mod hyper_loader;
+pub use web_static_pack_common as common;
+
+pub mod body;
+pub mod cache_control;
+pub mod content_encoding;
+pub mod file;
+pub mod loader;
+pub mod pack;
+pub mod responder;
diff --git a/loader/src/loader.rs b/loader/src/loader.rs
index 5be2b36..00d9ddb 100644
--- a/loader/src/loader.rs
+++ b/loader/src/loader.rs
@@ -1,148 +1,88 @@
-//! Main loader module.
-//! This is the part you should include in you target project if you want to read packs directly.
-//! After creating a pack with cli packer tool, include this into your program with `include_bytes!` macro. Then pass it to `Loader::new()`.
-//! Files may be retrieved using `get()` method.
-
-use anyhow::{bail, Context, Error};
-use std::{collections::HashMap, str};
-
-/// File descriptor, retrieved from loader.
-pub struct FileDescriptor {
- content_type: &'static str,
- etag: &'static str,
- content: &'static [u8],
- content_gzip: Option<&'static [u8]>,
-}
-impl FileDescriptor {
- /// Returns HTTP Content-Type.
- pub fn content_type(&self) -> &'static str {
- self.content_type
- }
-
- /// Returns quoted http ETag precalculated for this file.
- pub fn etag(&self) -> &'static str {
- self.etag
- }
-
- /// Returns original file content.
- pub fn content(&self) -> &'static [u8] {
- self.content
- }
-
- /// Returns gzipped file content (if available)
- pub fn content_gzip(&self) -> Option<&'static [u8]> {
- self.content_gzip
- }
-}
-
-/// Main loader. Create using `::new()` providing reference to result of `include_bytes!`.
-/// Call `get()` to access files.
-pub struct Loader {
- files: HashMap<&'static str, FileDescriptor>,
-}
-impl Loader {
- fn read_u8(rest: &mut &'static [u8]) -> Result<&'static [u8], Error> {
- #[allow(clippy::len_zero)]
- if rest.len() < 1 {
- bail!("Premature length termination");
- }
- let length = u8::from_ne_bytes(unsafe { [*rest.get_unchecked(0)] }) as usize;
-
- if rest.len() - 1 < length {
- bail!("Premature data termination");
- }
- let data = &rest[1..(1 + length)];
-
- *rest = &rest[1 + length..];
-
- Ok(data)
- }
- fn read_u16(rest: &mut &'static [u8]) -> Result<&'static [u8], Error> {
- if rest.len() < 2 {
- bail!("Premature length termination");
- }
- let length = u16::from_ne_bytes(unsafe { [*rest.get_unchecked(0), *rest.get_unchecked(1)] })
- as usize;
-
- if rest.len() - 2 < length {
- bail!("Premature data termination");
- }
- let data = &rest[2..(2 + length)];
-
- *rest = &rest[2 + length..];
-
- Ok(data)
- }
- fn read_u32(rest: &mut &'static [u8]) -> Result<&'static [u8], Error> {
- if rest.len() < 4 {
- bail!("Premature length termination");
- }
- let length = u32::from_ne_bytes(unsafe {
- [
- *rest.get_unchecked(0),
- *rest.get_unchecked(1),
- *rest.get_unchecked(2),
- *rest.get_unchecked(3),
- ]
- }) as usize;
-
- if rest.len() - 4 < length {
- bail!("Premature data termination");
- }
- let data = &rest[4..(4 + length)];
-
- *rest = &rest[4 + length..];
-
- Ok(data)
- }
-
- /// Creates a loader.
- /// Pass result of `include_bytes!` macro here.
- /// Create pack (for inclusion) with `web-static-pack-packer`.
- pub fn new(included_bytes: &'static [u8]) -> Result {
- let mut rest = included_bytes;
- let mut files = HashMap::<&'static str, FileDescriptor>::new();
- while !rest.is_empty() {
- // Extract.
- let path =
- unsafe { str::from_utf8_unchecked(Self::read_u16(&mut rest).context("path")?) };
- let content_type = unsafe {
- str::from_utf8_unchecked(Self::read_u8(&mut rest).context("content_type")?)
- };
- let etag =
- unsafe { str::from_utf8_unchecked(Self::read_u8(&mut rest).context("etag")?) };
- let content = Self::read_u32(&mut rest).context("content")?;
- let content_gzip = Self::read_u32(&mut rest).context("content_gzip")?;
- let content_gzip = if !content_gzip.is_empty() {
- Some(content_gzip)
- } else {
- None
- };
-
- // Build FileDescriptor.
- let file_descriptor = FileDescriptor {
- content_type,
- etag,
- content,
- content_gzip,
- };
-
- // Push to collection.
- if files.insert(path, file_descriptor).is_some() {
- bail!("File corrupted, duplicated path: {}", path);
- }
- }
- log::info!("Loaded total {} files", files.len());
- Ok(Self { files })
- }
-
- /// Retrieves file from pack.
- /// The path should usually start with `/`, exactly as in URL.
- /// Returns `Some(&FileDescriptor)` if file is found, `None` otherwise.
- pub fn get(
- &self,
- path: &str,
- ) -> Option<&FileDescriptor> {
- self.files.get(path)
- }
+//! Module containing [load] function used to convert (map) serialized `pack`
+//! into [PackArchived] object.
+
+use crate::common::{
+ pack::{Pack, PackArchived},
+ PACK_FILE_MAGIC, PACK_FILE_VERSION,
+};
+use anyhow::{ensure, Error};
+use rkyv::archived_root;
+
+/// Alignment value for `serialized` in [load].
+pub const ALIGN_BYTES: usize = 16;
+
+/// Loads [PackArchived] from serialized bytes created by
+/// [web-static-pack-packer](https://crates.io/crates/web-static-pack-packer).
+///
+/// This method is lightweight, as it does some pre-checks and then casts
+/// (zero-copy deserialization) [rkyv] archived [PackArchived] to the output.
+///
+/// In a typical scenario the [Pack] will be created with packer in build
+/// pipeline (or build.rs), then serialized to a file and stored in the working
+/// / build directory.
+///
+/// Target application will have serialized `pack` embedded with
+///
+/// (or alternatively read from fs) and then passed to this function, that will
+/// "map" it to [PackArchived].
+///
+/// Loaded [PackArchived] will be typically used to create
+/// [crate::responder::Responder].
+///
+/// Please note that `serialized` must be aligned to [ALIGN_BYTES], either by
+/// `include_bytes_aligned` crate (if embedding into executable) or
+/// [rkyv::util::AlignedVec] (if loading from fs in runtime).
+///
+/// # Examples
+///
+/// ```ignore
+/// // from workspace root:
+/// // $ cargo run -- directory-single ./tests/data/vcard-personal-portfolio/ vcard-personal-portfolio.pack
+///
+/// // then in your application
+/// static PACK_ARCHIVED_SERIALIZED: &[u8] = include_bytes_aligned!(
+/// 16, // == web_static_pack::loader::ALIGN_BYTES,
+/// "vcard-personal-portfolio.pack"
+/// );
+///
+/// fn main() {
+/// let pack = unsafe { web_static_pack::loader::load(PACK_ARCHIVED_SERIALIZED).unwrap() };
+/// // create responder from pack
+/// // pass http requests to responder
+/// // see crate documentation for full example
+/// }
+/// ```
+///
+/// # Safety
+/// `serialized` must point to valid `pack` created with matching version of
+/// packer. Underlying loader (rkyv) relies on correct file content. If invalid
+/// content is provided it is going to cause undefined behavior.
+pub unsafe fn load(serialized: &[u8]) -> Result<&PackArchived, Error> {
+ ensure!(
+ serialized.as_ptr() as usize % ALIGN_BYTES == 0,
+ "invalid alignment, serialized must be aligned to {ALIGN_BYTES} bytes"
+ );
+ ensure!(serialized.len() > 16, "premature file end");
+
+ // check file magic
+ let file_magic_bytes: [u8; 8] = serialized[0..8].try_into()?;
+ let file_magic = u64::from_ne_bytes(file_magic_bytes);
+ ensure!(
+ file_magic == PACK_FILE_MAGIC,
+ "file magic mismatch, probably not a pack file"
+ );
+
+ // check file version
+ let file_version_bytes: [u8; 8] = serialized[8..16].try_into()?;
+ let file_version = u64::from_ne_bytes(file_version_bytes);
+ ensure!(
+ file_version == PACK_FILE_VERSION,
+ "file version mismatch (got {file_version}, expected: {PACK_FILE_VERSION}) (probably pack created with different version)"
+ );
+
+ // deserialize content
+ // NOTE: value passed to [archived_root] must be 16-aligned
+ let pack = unsafe { archived_root::(&serialized[16..]) };
+
+ Ok(pack)
}
diff --git a/loader/src/pack.rs b/loader/src/pack.rs
new file mode 100644
index 0000000..d421218
--- /dev/null
+++ b/loader/src/pack.rs
@@ -0,0 +1,50 @@
+//! Pack related types. Provides [Pack] trait.
+
+use crate::{
+ common::{
+ file::{File as File_, FileArchived},
+ pack::{Pack as Pack_, PackArchived},
+ },
+ file::File,
+};
+
+/// Trait representing Pack, a container for files identified by path.
+///
+/// Most users will use [PackArchived] implementation, returned by
+/// [crate::loader::load].
+/// This trait is also implemented for non-archived [Pack_], mostly for testing
+/// purposes.
+pub trait Pack {
+ /// File type returned by this `pack`.
+ type File: File;
+
+ /// Given `pack` relative path, eg. `/dir1/dir2/file.html` returns file
+ /// associated with this path. Returns [None] if file for given path
+ /// does not exist.
+ fn get_file_by_path(
+ &self,
+ path: &str,
+ ) -> Option<&Self::File>;
+}
+impl Pack for Pack_ {
+ type File = File_;
+
+ fn get_file_by_path(
+ &self,
+ path: &str,
+ ) -> Option<&Self::File> {
+ let file = self.files_by_path.get(path)?;
+ Some(file)
+ }
+}
+impl Pack for PackArchived {
+ type File = FileArchived;
+
+ fn get_file_by_path(
+ &self,
+ path: &str,
+ ) -> Option<&Self::File> {
+ let file = self.files_by_path.get(path)?;
+ Some(file)
+ }
+}
diff --git a/loader/src/responder.rs b/loader/src/responder.rs
new file mode 100644
index 0000000..4014c89
--- /dev/null
+++ b/loader/src/responder.rs
@@ -0,0 +1,394 @@
+//! Module containing [Responder] - service taking http requests and returning
+//! http responses.
+
+use crate::{
+ body::Body,
+ content_encoding::{ContentContentEncoding, EncodingAccepted},
+ file::File,
+ pack::Pack,
+};
+use http::{
+ header,
+ request::Parts as RequestParts,
+ response::{Builder as ResponseBuilder, Response as HttpResponse},
+ Method, StatusCode,
+};
+
+/// Http response type specialization.
+pub type Response<'a> = HttpResponse>;
+
+/// Responder service, providing http response for requests, looking for
+/// [File] in [Pack].
+///
+/// There are two main methods for this type:
+/// - [Self::respond] - generates http response for successful requests and lets
+/// user handle errors manually.
+/// - [Self::respond_flatten] - like above, but generates default responses also
+/// for errors.
+///
+/// # Examples
+///
+/// ```ignore
+/// # use http::StatusCode;
+///
+/// let pack_archived = web_static_pack::loader::load(...).unwrap();
+/// let responder = web_static_pack::responder::Responder::new(pack_archived);
+///
+/// assert_eq!(
+/// responder.respond_flatten().status(),
+/// StatusCode::OK
+/// );
+/// assert_eq!(
+/// responder.respond_flatten().status(),
+/// StatusCode::NOT_FOUND
+/// );
+///
+/// assert_eq!(
+/// responder.respond(),
+/// Err(ResponderRespondError::PackPathNotFound)
+/// );
+/// ```
+///
+/// For full example, including making a hyper server, see crate level
+/// documentation.
+#[derive(Debug)]
+pub struct Responder<'p, P>
+where
+ P: Pack,
+{
+ pack: &'p P,
+}
+impl<'p, P> Responder<'p, P>
+where
+ P: Pack,
+{
+ /// Creates new instance, based on [Pack].
+ pub const fn new(pack: &'p P) -> Self {
+ Self { pack }
+ }
+
+ /// Returns http response for given request or rust error to be handled by
+ /// user.
+ ///
+ /// Inside this method:
+ /// - Checks http method (accepts GET or HEAD).
+ /// - Looks for file inside `pack` passed in constructor.
+ /// - Checks for `ETag` match (and returns 304).
+ /// - Negotiates content encoding.
+ /// - Builds final http response containing header and body (if method is
+ /// not HEAD).
+ ///
+ /// For alternative handling errors with default http responses see
+ /// [Self::respond_flatten].
+ pub fn respond(
+ &self,
+ request: RequestParts,
+ ) -> Result, ResponderRespondError> {
+ // only GET and HEAD are supported
+ let body_in_response = match request.method {
+ Method::GET => true,
+ Method::HEAD => false,
+ _ => {
+ return Err(ResponderRespondError::HttpMethodNotSupported);
+ }
+ };
+
+ // find file for given request
+ let file = match self.pack.get_file_by_path(request.uri.path()) {
+ Some(file_descriptor) => file_descriptor,
+ None => {
+ return Err(ResponderRespondError::PackPathNotFound);
+ }
+ };
+
+ // check for possible `ETag`
+ // if `ETag` exists and matches current file, return 304
+ if let Some(etag_request) = request.headers.get(header::IF_NONE_MATCH)
+ && etag_request.as_bytes() == file.etag().as_bytes()
+ {
+ let response = ResponseBuilder::new()
+ .status(StatusCode::NOT_MODIFIED)
+ .header(header::ETAG, file.etag()) // https://stackoverflow.com/a/4226409/1658328
+ .body(Body::empty())
+ .unwrap();
+ return Ok(response);
+ };
+
+ // resolve content and content-encoding header
+ let content_content_encoding = ContentContentEncoding::resolve(
+ &match EncodingAccepted::from_headers(&request.headers) {
+ Ok(content_encoding_encoding_accepted) => content_encoding_encoding_accepted,
+ Err(_) => return Err(ResponderRespondError::UnparsableAcceptEncoding),
+ },
+ file,
+ );
+
+ // build final response
+ let response = ResponseBuilder::new()
+ .header(header::CONTENT_TYPE, file.content_type())
+ .header(header::ETAG, file.etag())
+ .header(header::CACHE_CONTROL, file.cache_control().cache_control())
+ .header(
+ header::CONTENT_LENGTH,
+ content_content_encoding.content.len(),
+ )
+ .header(
+ header::CONTENT_ENCODING,
+ content_content_encoding.content_encoding,
+ )
+ .body(if body_in_response {
+ Body::new(content_content_encoding.content)
+ } else {
+ Body::empty()
+ })
+ .unwrap();
+
+ Ok(response)
+ }
+
+ /// Like [Self::respond], but generates "default" (proper http
+ /// status code and empty body) responses also for errors. This will for
+ /// example generate HTTP 404 response for request uri not found in path.
+ ///
+ /// For manual error handling, see [Self::respond].
+ pub fn respond_flatten(
+ &self,
+ request: RequestParts,
+ ) -> Response<'p> {
+ match self.respond(request) {
+ Ok(response) => response,
+ Err(responder_error) => responder_error.into_response(),
+ }
+ }
+}
+
+/// Possible errors during [Responder::respond] handling.
+#[derive(PartialEq, Eq, Debug)]
+pub enum ResponderRespondError {
+ /// Not supported HTTP Method, this maps to HTTP `METHOD_NOT_ALLOWED`.
+ HttpMethodNotSupported,
+
+ /// Request URI was not found in [Pack]. This maps to HTTP `NOT_FOUND`.
+ PackPathNotFound,
+
+ /// Error while parsing HTTP `Accept-Encoding`. This maps to HTTP
+ /// `BAD_REQUEST`.
+ UnparsableAcceptEncoding,
+}
+impl ResponderRespondError {
+ /// Converts error into best matching HTTP error code.
+ pub fn status_code(&self) -> StatusCode {
+ match self {
+ ResponderRespondError::HttpMethodNotSupported => StatusCode::METHOD_NOT_ALLOWED,
+ ResponderRespondError::PackPathNotFound => StatusCode::NOT_FOUND,
+ ResponderRespondError::UnparsableAcceptEncoding => StatusCode::BAD_REQUEST,
+ }
+ }
+
+ /// Creates default response (status code + empty body) for this error.
+ pub fn into_response(&self) -> Response<'static> {
+ let response = ResponseBuilder::new()
+ .status(self.status_code())
+ .body(Body::empty())
+ .unwrap();
+ response
+ }
+}
+
+#[cfg(test)]
+mod test_responder {
+ use super::{Responder, ResponderRespondError};
+ use crate::{cache_control::CacheControl, file::File, pack::Pack};
+ use anyhow::anyhow;
+ use http::{
+ header, method::Method, request::Builder as RequestBuilder, status::StatusCode, HeaderMap,
+ HeaderName, HeaderValue,
+ };
+
+ struct FileMock;
+ impl File for FileMock {
+ fn content(&self) -> &[u8] {
+ b"content-identity"
+ }
+ fn content_gzip(&self) -> Option<&[u8]> {
+ None
+ }
+ fn content_brotli(&self) -> Option<&[u8]> {
+ Some(b"content-br")
+ }
+
+ fn content_type(&self) -> HeaderValue {
+ HeaderValue::from_static("text/plain; charset=utf-8")
+ }
+ fn etag(&self) -> HeaderValue {
+ HeaderValue::from_static("\"etagvalue\"")
+ }
+ fn cache_control(&self) -> CacheControl {
+ CacheControl::MaxCache
+ }
+ }
+
+ struct PackMock;
+ impl Pack for PackMock {
+ type File = FileMock;
+
+ fn get_file_by_path(
+ &self,
+ path: &str,
+ ) -> Option<&Self::File> {
+ match path {
+ "/present" => Some(&FileMock),
+ _ => None,
+ }
+ }
+ }
+
+ static RESPONDER: Responder<'static, PackMock> = Responder::new(&PackMock);
+
+ fn header_as_string(
+ headers: &HeaderMap,
+ name: HeaderName,
+ ) -> &str {
+ headers
+ .get(&name)
+ .ok_or_else(|| anyhow!("missing header {name}"))
+ .unwrap()
+ .to_str()
+ .unwrap()
+ }
+
+ #[test]
+ fn resolves_typical_request() {
+ let (request, _) = RequestBuilder::new()
+ .method(Method::GET)
+ .uri("/present")
+ .header(header::ACCEPT_ENCODING, "br, gzip")
+ .header(header::IF_NONE_MATCH, "\"invalidetag\"")
+ .body(())
+ .unwrap()
+ .into_parts();
+
+ let response = RESPONDER.respond(request).unwrap();
+ let headers = response.headers();
+
+ assert_eq!(response.status(), StatusCode::OK);
+
+ assert_eq!(
+ header_as_string(headers, header::CONTENT_TYPE),
+ "text/plain; charset=utf-8"
+ );
+ assert_eq!(
+ header_as_string(headers, header::ETAG), // line break
+ "\"etagvalue\""
+ );
+ assert_eq!(
+ header_as_string(headers, header::CACHE_CONTROL), // line break
+ "max-age=31536000, immutable"
+ );
+ assert_eq!(
+ header_as_string(headers, header::CONTENT_LENGTH), // line break
+ "10"
+ );
+ assert_eq!(
+ header_as_string(headers, header::CONTENT_ENCODING), // line break
+ "br"
+ );
+
+ assert_eq!(response.body().data(), b"content-br");
+ }
+
+ #[test]
+ fn resolves_no_body_for_head_request() {
+ let (request, _) = RequestBuilder::new()
+ .method(Method::HEAD)
+ .uri("/present")
+ .body(())
+ .unwrap()
+ .into_parts();
+
+ let response = RESPONDER.respond(request).unwrap();
+ let headers = response.headers();
+
+ assert_eq!(response.status(), StatusCode::OK);
+
+ assert_eq!(
+ header_as_string(headers, header::CONTENT_TYPE),
+ "text/plain; charset=utf-8"
+ );
+ assert_eq!(
+ header_as_string(headers, header::ETAG), // line break
+ "\"etagvalue\""
+ );
+ assert_eq!(
+ header_as_string(headers, header::CONTENT_LENGTH), // line break
+ "16"
+ );
+ assert_eq!(
+ header_as_string(headers, header::CONTENT_ENCODING),
+ "identity"
+ );
+
+ assert_eq!(response.body().data(), b"");
+ }
+
+ #[test]
+ fn resolves_not_modified_for_matching_etag() {
+ let (request, _) = RequestBuilder::new()
+ .method(Method::GET)
+ .uri("/present")
+ .header(header::IF_NONE_MATCH, "\"etagvalue\"")
+ .body(())
+ .unwrap()
+ .into_parts();
+
+ let response = RESPONDER.respond(request).unwrap();
+ let headers = response.headers();
+
+ assert_eq!(response.status(), StatusCode::NOT_MODIFIED);
+
+ // `ETag` should be resent, others should be missing
+ assert_eq!(
+ header_as_string(headers, header::ETAG), // line break
+ "\"etagvalue\""
+ );
+ assert!(headers.get(header::CONTENT_TYPE).is_none());
+
+ // of course no body
+ assert_eq!(response.body().data(), b"");
+ }
+
+ #[test]
+ fn resolves_error_for_invalid_method() {
+ let (request, _) = RequestBuilder::new()
+ .method(Method::POST)
+ .uri("/present")
+ .body(())
+ .unwrap()
+ .into_parts();
+
+ let response_error = RESPONDER.respond(request).unwrap_err();
+ assert_eq!(
+ response_error,
+ ResponderRespondError::HttpMethodNotSupported
+ );
+
+ let response_flatten = response_error.into_response();
+ assert_eq!(response_flatten.status(), StatusCode::METHOD_NOT_ALLOWED);
+ }
+
+ #[test]
+ fn resolves_error_for_file_not_found() {
+ let (request, _) = RequestBuilder::new()
+ .method(Method::GET)
+ .uri("/missing")
+ .body(())
+ .unwrap()
+ .into_parts();
+
+ let response_error = RESPONDER.respond(request).unwrap_err();
+ assert_eq!(response_error, ResponderRespondError::PackPathNotFound);
+
+ let response_flatten = response_error.into_response();
+ assert_eq!(response_flatten.status(), StatusCode::NOT_FOUND);
+ }
+}
diff --git a/packer/Cargo.toml b/packer/Cargo.toml
index b3384e0..7d2fd2d 100644
--- a/packer/Cargo.toml
+++ b/packer/Cargo.toml
@@ -1,23 +1,28 @@
[package]
name = "web-static-pack-packer"
-version = "0.1.6"
+version = "0.5.0-beta.1"
authors = ["Paweł Kubrak "]
edition = "2021"
-description = "Installable web-static-pack-packer tool for web-static-pack crate"
license = "MIT"
+description = "Installable web-static-pack-packer tool for web-static-pack crate"
homepage = "https://github.com/peku33/web-static-pack"
repository = "https://github.com/peku33/web-static-pack"
-documentation = "https://docs.rs/web-static-pack"
readme = "README.md"
+keywords = ["web", "http", "static", "resources", "hyper"]
+categories = ["web-programming"]
[dependencies]
-bytes = "1.5.0"
-clap = "4.4.7"
-anyhow = "1.0.75"
-itertools = "0.11.0"
-libflate = "2.0.0"
-log = "0.4.20"
-mime_guess = "2.0.4"
+web-static-pack-common = { version = "0.5.0-beta.1", path = "../common" }
+
+anyhow = "1.0.86"
+brotli = "6.0.0"
+clap = { version = "4.5.8", features = ["derive"] }
+flate2 = "1.0"
+itertools = "0.13.0"
+mime_guess = "2.0.5"
+rkyv = { version = "0.7.44" }
sha3 = "0.10.8"
-simple_logger = "4.2.0"
-walkdir = "2.4.0"
+walkdir = "2.5.0"
+
+[dev-dependencies]
+test-case = "3.3.1"
diff --git a/packer/README.md b/packer/README.md
index 5b9aa15..4e1b8d4 100644
--- a/packer/README.md
+++ b/packer/README.md
@@ -1,13 +1,129 @@
# web-static-pack-packer
-Executable to build packs for web-static-pack crate
-See main crate for details
-
-[![docs.rs](https://docs.rs/web-static-pack-packer/badge.svg)](https://docs.rs/web-static-pack-packer)
-
-## Usage
-1. Install (`cargo install web-static-pack-packer`) or run locally (`cargo run`)
-2. Provide positional arguments:
- - `` - the directory to pack
- - `` - name of the build pack
- - `[root_pach]` - relative path to build pack paths with. use the same as `path` to have all paths in pack root
-3. Use `` file with `web-static-pack` (loader)
+
+web-static-pack-packer is the "builder" (1st stage) part of the
+[web-static-pack](https://github.com/peku33/web-static-pack)
+project. See project page for a general idea how two parts cooperate.
+
+The goal of the packer part is to collect your directories / files / memory
+slices, precalculate things like `ETag`, compressed versions (`gzip`,
+`brotli`) and store them as a single file (called `pack`). Your target
+application will include (ex. with
+
+) and "load" / "parse" `pack` during runtime using
+[web-static-pack](https://crates.io/crates/web-static-pack)
+(the loader part) and (possibly) serve it with a web server of your choice.
+
+This crate is usually used in build script / CI / build.rs stage, not in
+your target application. It's used to create a `pack` from list of files
+(like your GUI app / images / other assets) to be later loaded by your app
+and served with a web server.
+
+This crate can be used in two ways:
+- As a standalone application, installed with `cargo install`, this is the
+ preferred way if you are using build scripts, CI pipeline etc.
+- As a library, imported to your project, this is a way to go if you want to
+ use it in build.rs of your target application or go with some really
+ custom approach
+
+## Using as a standalone application
+
+### Install (or update to matching version)
+- Either install it with `$ cargo install web-static-pack-packer` and use
+ shell command `$ web-static-pack-packer [PARAMS]...`
+- Or clone repo, go into `packer` directory, `$ cargo run --release --
+ [PARAMS]...`. (please note `--` which marks end of arguments for cargo run
+ and beginning of arguments for the application).
+
+For the purpose of this example, the first option is assumed, with
+`web-static-pack-packer` command available.
+
+### Create a `pack`
+`web-static-pack-packer` provides up to date documentation with `$
+web-static-pack-packer --help`. Application is built around subcommands to
+cover basic scenarios:
+- `directory-single [OPTIONS] `
+ will create a `pack` from a single directory. This is the most common
+ scenario, for example when you have a web application built into
+ `./gui/build` directory and you want to have it served with your app.
+- `files-cmd [OPTIONS]
+ [INPUT_FILE_PATHS]...` lets you specify all files from command line in
+ `xargs` style. base directory path is used as a root for building relative
+ paths inside a `pack`.
+- `files-stdin [OPTIONS] `
+ lets you provide list of files from stdin.
+
+#### Examples
+Let's say you have a `vcard-personal-portfolio` directory containing your
+web project (available in tests/data/ in repository). Directory structure
+looks like:
+```
+vcard-personal-portfolio
+| index.html
+| index.txt
++---assets
+| +---css
+| | style.css
+| +---images
+| | .png
+| \---js
+| script.js
+\---website-demo-image
+ desktop.png
+ mobile.png
+```
+By running:
+```
+$ web-static-pack-packer \
+ directory-single \
+ ./vcard-personal-portfolio \
+ ./vcard-personal-portfolio.pack
+```
+a new file `vcard-personal-portfolio.pack` will be created, containing all
+files, so that `GET /index.html` or `GET /assets/css/tyle.css` or `GET
+/website-demo-image/mobile.png` will be correctly resolved.
+
+In the next step, the `vcard-personal-portfolio.pack` should be used by
+[web-static-pack](https://crates.io/crates/web-static-pack)
+(the loader part) to serve it from your app.
+
+## Using as a library
+When using as a library, you are most likely willing to create a loadable
+(by the loader) `pack`, by using [pack::Builder].
+
+You will need to add [file_pack_path::FilePackPath] (file + path) objects to
+the builder, which you can obtain by:
+- Manually constructing the object from [common::pack_path::PackPath] and
+ [common::file::File] (obtained from fs [file::build_from_path] or memory
+ slice [file::build_from_content]).
+- Reading single file with [file_pack_path::FilePackPath::build_from_path].
+- Automatic search through fs with [directory::search].
+
+When all files are added to the builder, you will need to finalize it and
+either write to fs (to have it included in your target application) with
+[pack::store_file] or (mostly for test purposes) serialize to memory with
+[pack::store_memory].
+
+#### Examples
+This example will do exactly the same as one for application scenario:
+```rust
+
+// start with empty pack builder
+let mut pack = Builder::new();
+
+// add files with directory search and default options
+pack.file_pack_paths_add(search(
+ &PathBuf::from("vcard-personal-portfolio"),
+ &SearchOptions::default(),
+ &BuildFromPathOptions::default(),
+)?)?;
+
+// finalize the builder, obtain pack
+let pack = pack.finalize();
+
+// store (serialize `pack` to the fs) to be included in the target app
+store_file(&pack, &PathBuf::from("vcard-personal-portfolio.pack"))?;
+```
+
+For more examples browse through modules of this crate.
+
+License: MIT
diff --git a/packer/src/directory.rs b/packer/src/directory.rs
new file mode 100644
index 0000000..820a45e
--- /dev/null
+++ b/packer/src/directory.rs
@@ -0,0 +1,104 @@
+//! Directory helpers. Contains [search] function, used to gather files from
+//! directory recursively.
+
+use crate::{file, file_pack_path};
+use anyhow::{Context, Error};
+use std::path::Path;
+use walkdir::WalkDir;
+
+/// Settings for [search] function.
+///
+/// If not sure what to set here, use [Default].
+#[derive(Debug)]
+pub struct SearchOptions {
+ /// Whether to follow links while traversing directories.
+ pub follow_links: bool,
+}
+impl Default for SearchOptions {
+ fn default() -> Self {
+ Self { follow_links: true }
+ }
+}
+
+/// Searches fs recursively and builds [file_pack_path::FilePackPath] for each
+/// file.
+///
+/// Traverses directory specified in `path` using [SearchOptions]. Builds all
+/// found files as [file_pack_path::FilePackPath] using
+/// [file::BuildFromPathOptions]. Paths are created by stripping `path` from
+/// full file path.
+///
+/// # Examples
+///
+/// ```
+/// # use anyhow::Error;
+/// # use std::{collections::HashMap, path::PathBuf};
+/// # use web_static_pack_packer::{
+/// # directory::{search, SearchOptions},
+/// # file::BuildFromPathOptions,
+/// # };
+/// #
+/// # fn main() -> Result<(), Error> {
+/// #
+/// // traverse directory from tests
+/// let files = search(
+/// &PathBuf::from(env!("CARGO_MANIFEST_DIR"))
+/// .parent()
+/// .unwrap()
+/// .join("tests")
+/// .join("data")
+/// .join("vcard-personal-portfolio")
+/// .join("assets"),
+/// &SearchOptions::default(),
+/// &BuildFromPathOptions::default(),
+/// )?;
+///
+/// // group files into hashmap {pack_path: file}
+/// let files_by_path = files
+/// .into_vec()
+/// .into_iter()
+/// .map(|file_pack_path| (file_pack_path.pack_path, file_pack_path.file))
+/// .collect::>();
+///
+/// // verify necessary files were added
+/// assert!(files_by_path.contains_key("/css/style.css"));
+/// assert!(files_by_path.contains_key("/js/script.js"));
+/// assert!(!files_by_path.contains_key("/index.html"));
+/// #
+/// # Ok(())
+/// # }
+/// ```
+pub fn search(
+ path: &Path,
+ options: &SearchOptions,
+ file_build_options: &file::BuildFromPathOptions,
+) -> Result, Error> {
+ let file_paths = WalkDir::new(path)
+ .follow_links(options.follow_links)
+ .into_iter()
+ .map(|file_entry| {
+ // detect search errors
+ let file_entry = file_entry?;
+
+ // we are interested in files only
+ // if follow_links is true, this will be resolved as link target
+ if !file_entry.file_type().is_file() {
+ return Ok(None);
+ }
+
+ // build file
+ let file_pack_path = file_pack_path::FilePackPath::build_from_path(
+ file_entry.path(),
+ path,
+ file_build_options,
+ )
+ .with_context(|| file_entry.path().to_string_lossy().into_owned())?;
+
+ // yield for processing
+ Ok(Some(file_pack_path))
+ })
+ .filter_map(|entry_result| entry_result.transpose()) // strips Ok(None)
+ .collect::, Error>>()?;
+
+ Ok(file_paths)
+}
diff --git a/packer/src/file.rs b/packer/src/file.rs
new file mode 100644
index 0000000..26c699e
--- /dev/null
+++ b/packer/src/file.rs
@@ -0,0 +1,379 @@
+//! File helpers. Contains [build_from_path] and [build_from_content] functions
+//! to create a [File] from fs / memory content.
+
+use crate::common::{cache_control::CacheControl, file::File};
+use anyhow::Error;
+use brotli::enc::BrotliEncoderParams;
+use flate2::{write::GzEncoder, Compression};
+use sha3::{Digest, Sha3_256};
+use std::{
+ fs,
+ io::{Cursor, Write},
+ path::Path,
+};
+
+/// Options when preparing file in [build_from_path].
+///
+/// If not sure what to set here, use [Default].
+#[derive(Debug)]
+pub struct BuildFromPathOptions {
+ /// Try adding gzipped version of file. If set to true, it may still not be
+ /// added (ex. in case gzipped version is larger than raw).
+ pub use_gzip: bool,
+ /// Try adding brotli version of file. If set to true, it may still not be
+ /// added (ex. in case gzipped version is larger than raw).
+ pub use_brotli: bool,
+
+ /// Override `content-type` header for this file.
+ pub content_type_override: Option,
+ /// Override [CacheControl] for this file.
+ pub cache_control_override: Option,
+}
+impl Default for BuildFromPathOptions {
+ fn default() -> Self {
+ Self {
+ use_gzip: true,
+ use_brotli: true,
+ content_type_override: None,
+ cache_control_override: None,
+ }
+ }
+}
+
+/// Creates a [File] by reading file from fs, specified by `path`.
+///
+/// Inside file will be read, `content-type` determined
+/// from extension and then passed to [build_from_content].
+///
+/// # Examples
+///
+/// ```
+/// # use anyhow::{anyhow, Error};
+/// # use std::path::PathBuf;
+/// # use web_static_pack_packer::file::{build_from_path, BuildFromPathOptions};
+/// #
+/// # fn main() -> Result<(), Error> {
+/// #
+/// let file = build_from_path(
+/// &PathBuf::from(env!("CARGO_MANIFEST_DIR"))
+/// .parent()
+/// .ok_or_else(|| anyhow!("missing parent"))?
+/// .join("tests")
+/// .join("data")
+/// .join("vcard-personal-portfolio")
+/// .join("index.html"),
+/// &BuildFromPathOptions::default(),
+/// )?;
+/// assert_eq!(file.content_type, "text/html; charset=utf-8");
+/// #
+/// # Ok(())
+/// # }
+/// ```
+pub fn build_from_path(
+ path: &Path,
+ options: &BuildFromPathOptions,
+) -> Result {
+ // read content
+ let content = content_from_path(path)?;
+
+ // use user provided content type if set, otherwise guess from path
+ let content_type = if let Some(content_type) = &options.content_type_override {
+ content_type.clone()
+ } else {
+ content_type_from_path(path)
+ };
+
+ // pass to inner builder
+ let file = build_from_content(
+ content,
+ content_type,
+ &BuildFromContentOptions {
+ use_gzip: options.use_gzip,
+ use_brotli: options.use_brotli,
+ cache_control_override: options.cache_control_override,
+ },
+ );
+
+ Ok(file)
+}
+
+/// Options when preparing file in [build_from_content].
+///
+/// If not sure what to set here, use [Default].
+#[derive(Debug)]
+pub struct BuildFromContentOptions {
+ /// Try adding gzipped version of content. If set to true, it may still not
+ /// be added (ex. in case gzipped version is larger than raw).
+ pub use_gzip: bool,
+ /// Try adding brotli version of content. If set to true, it may still not
+ /// be added (ex. in case gzipped version is larger than raw).
+ pub use_brotli: bool,
+
+ /// Override [CacheControl] for this file.
+ pub cache_control_override: Option,
+}
+impl Default for BuildFromContentOptions {
+ fn default() -> Self {
+ Self {
+ use_gzip: true,
+ use_brotli: true,
+ cache_control_override: None,
+ }
+ }
+}
+
+/// Creates a [File] from provided raw content and `content-type`.
+///
+/// Inside compressed versions will be created (according to options), `ETag`
+/// calculated and [CacheControl] set.
+///
+/// When setting `content_type` remember to set charset for text files, eg.
+/// `text/plain; charset=utf-8`.
+///
+/// # Examples
+///
+/// ```
+/// # use anyhow::Error;
+/// # use std::path::PathBuf;
+/// # use web_static_pack_packer::file::{build_from_content, BuildFromContentOptions};
+/// #
+/// # fn main() -> Result<(), Error> {
+/// #
+/// let file = build_from_content(
+/// Box::new(*b"Hello World!"),
+/// "text/html; charset=utf-8".to_owned(),
+/// &BuildFromContentOptions::default(),
+/// );
+/// assert!(file.content_gzip.is_none()); // too short for gzip
+/// assert!(file.content_brotli.is_none()); // too short for gzip
+/// assert_eq!(&*file.content, b"Hello World!");
+/// assert_eq!(file.content_type, "text/html; charset=utf-8");
+/// #
+/// # Ok(())
+/// # }
+/// ```
+pub fn build_from_content(
+ content: Box<[u8]>,
+ content_type: String,
+ options: &BuildFromContentOptions,
+) -> File {
+ let content_gzip = if options.use_gzip {
+ content_gzip_from_content(&content)
+ } else {
+ None
+ };
+ let content_brotli = if options.use_brotli {
+ content_brotli_from_content(&content)
+ } else {
+ None
+ };
+
+ let etag = etag_from_content(&content);
+ let cache_control = if let Some(cache_control) = &options.cache_control_override {
+ *cache_control
+ } else {
+ // we assume, that content is "static" and provide max caching opportunity
+ CacheControl::MaxCache
+ };
+
+ File {
+ content,
+ content_gzip,
+ content_brotli,
+ content_type,
+ etag,
+ cache_control,
+ }
+}
+
+/// Builds content by reading given file.
+fn content_from_path(path: &Path) -> Result, Error> {
+ let content = fs::read(path)?.into_boxed_slice();
+
+ Ok(content)
+}
+/// Builds gzip compressed version of `content`.
+///
+/// Returns [None] if there is no sense in having compressed version in `pack`
+/// (eg. compressed is larger than raw).
+fn content_gzip_from_content(content: &[u8]) -> Option> {
+ // no sense in compressing empty files
+ if content.is_empty() {
+ return None;
+ }
+
+ let mut content_gzip = GzEncoder::new(Vec::new(), Compression::best());
+ content_gzip.write_all(content).unwrap();
+ let content_gzip = content_gzip.finish().unwrap().into_boxed_slice();
+
+ // if gzip is longer then original value - it makes no sense to store it
+ if content_gzip.len() >= content.len() {
+ return None;
+ }
+
+ Some(content_gzip)
+}
+/// Builds brotli compressed version of `content`.
+///
+/// Returns [None] if there is no sense in having compressed version in `pack`
+/// (eg. compressed is larger than raw).
+fn content_brotli_from_content(content: &[u8]) -> Option> {
+ // no sense in compressing empty files
+ if content.is_empty() {
+ return None;
+ }
+
+ let mut content_cursor = Cursor::new(content);
+ let mut content_brotli = Vec::new();
+ let content_brotli_length = brotli::BrotliCompress(
+ &mut content_cursor,
+ &mut content_brotli,
+ &BrotliEncoderParams::default(),
+ )
+ .unwrap();
+ let content_brotli = content_brotli.into_boxed_slice();
+ assert!(content_brotli.len() == content_brotli_length);
+
+ // if brotli is longer then original value - it makes no sense to store it
+ if content_brotli.len() >= content.len() {
+ return None;
+ }
+
+ Some(content_brotli)
+}
+
+/// Guesses `content-type` from file path.
+///
+/// Only path is used, file content is not read. If file type cannot be guessed,
+/// returns "application/octet-stream". For text files (eg. plain, html, css,
+/// js, etc) it assumes utf-8 encoding.
+fn content_type_from_path(path: &Path) -> String {
+ let mut content_type = mime_guess::from_path(path)
+ .first_or_octet_stream()
+ .as_ref()
+ .to_owned();
+
+ // NOTE: temporary workaround for https://github.com/abonander/mime_guess/issues/90
+ if content_type == "application/javascript" {
+ content_type = "text/javascript".to_owned();
+ }
+
+ if content_type.starts_with("text/") {
+ content_type.push_str("; charset=utf-8");
+ }
+ content_type
+}
+/// Calculates `ETag` header from file contents.
+fn etag_from_content(content: &[u8]) -> String {
+ let mut etag = Sha3_256::new();
+ etag.update(content);
+ let etag = etag.finalize();
+ let etag = format!("\"{:x}\"", &etag); // `ETag` as "quoted" hex sha3. Quote is required by standard
+ etag
+}
+
+#[cfg(test)]
+mod test {
+ use super::{
+ build_from_content, content_brotli_from_content, content_gzip_from_content,
+ content_type_from_path, etag_from_content, BuildFromContentOptions,
+ };
+ use crate::common::file::File;
+ use std::path::{Path, PathBuf};
+ use test_case::test_case;
+
+ #[test]
+ fn build_from_content_returns_expected() {
+ let content_original = b"lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum";
+ let content_type_original = "text/plain; charset=utf-8";
+
+ let file = build_from_content(
+ Box::new(*content_original),
+ content_type_original.to_owned(),
+ &BuildFromContentOptions::default(),
+ );
+
+ let File {
+ content,
+ content_gzip,
+ content_brotli,
+ content_type,
+ // implementation dependant
+ // etag,
+ // cache_control,
+ ..
+ } = file;
+ assert_eq!(&*content, content_original);
+ assert_eq!(&*content_gzip.unwrap(), b"\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xff\x95\xc6\x41\x09\x00\x00\x08\x03\xc0\x2a\x2b\xe7\x43\xd8\x50\x14\xfb\x9b\x61\xbf\x63\x4d\x08\xd9\x7b\x02\x3d\x3f\x1e\x08\x7c\xb8\x3b\x00\x00\x00");
+ assert_eq!(&*content_brotli.unwrap(), b"\x1b\x3a\x00\xf8\x1d\xa9\x53\x9f\xbb\x70\x9d\xc6\xf6\x06\xa7\xda\xe4\x1a\xa4\x6c\xae\x4e\x18\x15\x0b\x98\x56\x70\x03");
+ assert_eq!(content_type, content_type_original);
+
+ // implementation dependant
+ // assert_eq!(etag, "");
+ // assert_eq!(cache_control, CacheControl::MaxCache);
+ }
+
+ #[test]
+ fn empty_should_not_be_compressed() {
+ assert!(content_gzip_from_content(&[]).is_none());
+ assert!(content_brotli_from_content(&[]).is_none());
+ }
+
+ #[test]
+ fn content_gzip_from_content_returns_expected() {
+ assert_eq!(
+ content_gzip_from_content(b"lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum").as_deref(),
+ Some(b"\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xff\x95\xc6\x41\x09\x00\x00\x08\x03\xc0\x2a\x2b\xe7\x43\xd8\x50\x14\xfb\x9b\x61\xbf\x63\x4d\x08\xd9\x7b\x02\x3d\x3f\x1e\x08\x7c\xb8\x3b\x00\x00\x00".as_slice())
+ );
+ }
+
+ #[test]
+ fn content_brotli_from_content_returns_expected() {
+ assert_eq!(
+ content_brotli_from_content(b"lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum").as_deref(),
+ Some(b"\x1b\x3a\x00\xf8\x1d\xa9\x53\x9f\xbb\x70\x9d\xc6\xf6\x06\xa7\xda\xe4\x1a\xa4\x6c\xae\x4e\x18\x15\x0b\x98\x56\x70\x03".as_slice())
+ );
+ }
+
+ #[test]
+ fn etag_from_content_returns_expected() {
+ // two identical payloads should produce identical `ETag`
+ // two different payloads should produce different `ETag`
+
+ assert_eq!(
+ etag_from_content(b"lorem ipsum"),
+ etag_from_content(b"lorem ipsum")
+ );
+ assert_ne!(
+ etag_from_content(b"lorem ipsum"),
+ etag_from_content(b"ipsum lorem")
+ );
+ }
+
+ #[test_case(
+ &PathBuf::from("a.html"),
+ "text/html; charset=utf-8";
+ "html file"
+ )]
+ #[test_case(
+ &PathBuf::from("directory/styles.css"),
+ "text/css; charset=utf-8";
+ "css file in directory"
+ )]
+ #[test_case(
+ &PathBuf::from("/root/dir/script.00ff00.js"),
+ "text/javascript; charset=utf-8";
+ "js file, full path, with some hex in stem"
+ )]
+ #[test_case(
+ &PathBuf::from("C:\\Users\\example\\Images\\SomeImage.webp"),
+ "image/webp";
+ "webp image in windows style path format"
+ )]
+ fn content_type_from_path_returns_expected(
+ path: &Path,
+ expected: &str,
+ ) {
+ assert_eq!(content_type_from_path(path), expected);
+ }
+}
diff --git a/packer/src/file_pack_path.rs b/packer/src/file_pack_path.rs
new file mode 100644
index 0000000..eac9b96
--- /dev/null
+++ b/packer/src/file_pack_path.rs
@@ -0,0 +1,98 @@
+//! Pack path (file and pack path combined) helpers. Contains [FilePackPath], a
+//! combination of [File] and [PackPath].
+
+use crate::{
+ common::{file::File, pack_path::PackPath},
+ file, pack_path,
+};
+use anyhow::{Context, Error};
+use std::path::Path;
+
+/// [File] (describing the content) + its [PackPath] (describing uri that the
+/// file will be accessible at).
+///
+/// This is the main item added to the `pack`.
+///
+/// It can be manually created by passing `file` and `pack_path` fields or using
+/// associated helpers methods.
+#[derive(Debug)]
+pub struct FilePackPath {
+ /// The file.
+ pub file: File,
+
+ /// The path inside the `pack`, corresponding to http path parameter.
+ pub pack_path: PackPath,
+}
+impl FilePackPath {
+ /// Creates [self] by reading file relative to given base directory.
+ ///
+ /// Given file path (`path`) to read and base directory path creates a
+ /// [self] by preparing:
+ /// - [File] with [file::build_from_path] using
+ /// [file::BuildFromPathOptions].
+ /// - [PackPath] with [pack_path::from_file_base_relative_path] (as relative
+ /// path between `path` and `base_directory_path`).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use anyhow::{anyhow, Error};
+ /// # use std::path::PathBuf;
+ /// # use web_static_pack_packer::{file::BuildFromPathOptions, file_pack_path::FilePackPath};
+ /// #
+ /// # fn main() -> Result<(), Error> {
+ /// #
+ /// // base directory
+ /// let base_directory = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
+ /// .parent()
+ /// .ok_or_else(|| anyhow!("missing parent"))?
+ /// .join("tests")
+ /// .join("data")
+ /// .join("vcard-personal-portfolio");
+ ///
+ /// // file in base/directory/path/index.html should end up as /index.html
+ /// let file_pack_path_1 = FilePackPath::build_from_path(
+ /// &base_directory.join("index.html"),
+ /// &base_directory,
+ /// &BuildFromPathOptions::default(),
+ /// )?;
+ /// assert_eq!(
+ /// file_pack_path_1.file.content_type,
+ /// "text/html; charset=utf-8"
+ /// );
+ /// assert_eq!(&*file_pack_path_1.pack_path, "/index.html");
+ ///
+ /// // file in base/directory/path/website-demo-image/desktop.png should end up as /website-demo-image/desktop.png
+ /// let file_pack_path_2 = FilePackPath::build_from_path(
+ /// &base_directory.join("website-demo-image").join("desktop.png"),
+ /// &base_directory,
+ /// &BuildFromPathOptions::default(),
+ /// )?;
+ /// assert_eq!(
+ /// file_pack_path_2.file.content_type,
+ /// "image/png"
+ /// );
+ /// assert_eq!(&*file_pack_path_2.pack_path, "/website-demo-image/desktop.png");
+ /// #
+ /// # Ok(())
+ /// # }
+ /// ```
+ pub fn build_from_path(
+ path: &Path,
+ base_directory_path: &Path,
+ file_options: &file::BuildFromPathOptions,
+ ) -> Result {
+ // strip prefix, so entry_path is relative to search root
+ let file_base_relative_path = path
+ .strip_prefix(base_directory_path)
+ .context("resolve file_base_relative_path")?;
+
+ // create path prefix
+ let pack_path = pack_path::from_file_base_relative_path(file_base_relative_path)?;
+
+ // read and build file
+ let file = file::build_from_path(path, file_options)?;
+
+ Ok(Self { file, pack_path })
+ }
+}
diff --git a/packer/src/lib.rs b/packer/src/lib.rs
new file mode 100644
index 0000000..203e567
--- /dev/null
+++ b/packer/src/lib.rs
@@ -0,0 +1,146 @@
+//! web-static-pack-packer is the "builder" (1st stage) part of the
+//! [web-static-pack](https://github.com/peku33/web-static-pack)
+//! project. See project page for a general idea how two parts cooperate.
+//!
+//! The goal of the packer part is to collect your directories / files / memory
+//! slices, precalculate things like `ETag`, compressed versions (`gzip`,
+//! `brotli`) and store them as a single file (called `pack`). Your target
+//! application will include (ex. with
+//!
+//! ) and "load" / "parse" `pack` during runtime using
+//! [web-static-pack](https://crates.io/crates/web-static-pack)
+//! (the loader part) and (possibly) serve it with a web server of your choice.
+//!
+//! This crate is usually used in build script / CI / build.rs stage, not in
+//! your target application. It's used to create a `pack` from list of files
+//! (like your GUI app / images / other assets) to be later loaded by your app
+//! and served with a web server.
+//!
+//! This crate can be used in two ways:
+//! - As a standalone application, installed with `cargo install`, this is the
+//! preferred way if you are using build scripts, CI pipeline etc.
+//! - As a library, imported to your project, this is a way to go if you want to
+//! use it in build.rs of your target application or go with some really
+//! custom approach
+//!
+//! # Using as a standalone application
+//!
+//! ## Install (or update to matching version)
+//! - Either install it with `$ cargo install web-static-pack-packer` and use
+//! shell command `$ web-static-pack-packer [PARAMS]...`
+//! - Or clone repo, go into `packer` directory, `$ cargo run --release --
+//! [PARAMS]...`. (please note `--` which marks end of arguments for cargo run
+//! and beginning of arguments for the application).
+//!
+//! For the purpose of this example, the first option is assumed, with
+//! `web-static-pack-packer` command available.
+//!
+//! ## Create a `pack`
+//! `web-static-pack-packer` provides up to date documentation with `$
+//! web-static-pack-packer --help`. Application is built around subcommands to
+//! cover basic scenarios:
+//! - `directory-single [OPTIONS] `
+//! will create a `pack` from a single directory. This is the most common
+//! scenario, for example when you have a web application built into
+//! `./gui/build` directory and you want to have it served with your app.
+//! - `files-cmd [OPTIONS]
+//! [INPUT_FILE_PATHS]...` lets you specify all files from command line in
+//! `xargs` style. base directory path is used as a root for building relative
+//! paths inside a `pack`.
+//! - `files-stdin [OPTIONS] `
+//! lets you provide list of files from stdin.
+//!
+//! ### Examples
+//! Let's say you have a `vcard-personal-portfolio` directory containing your
+//! web project (available in tests/data/ in repository). Directory structure
+//! looks like:
+//! ```text
+//! vcard-personal-portfolio
+//! | index.html
+//! | index.txt
+//! +---assets
+//! | +---css
+//! | | style.css
+//! | +---images
+//! | | .png
+//! | \---js
+//! | script.js
+//! \---website-demo-image
+//! desktop.png
+//! mobile.png
+//! ```
+//! By running:
+//! ```text
+//! $ web-static-pack-packer \
+//! directory-single \
+//! ./vcard-personal-portfolio \
+//! ./vcard-personal-portfolio.pack
+//! ```
+//! a new file `vcard-personal-portfolio.pack` will be created, containing all
+//! files, so that `GET /index.html` or `GET /assets/css/tyle.css` or `GET
+//! /website-demo-image/mobile.png` will be correctly resolved.
+//!
+//! In the next step, the `vcard-personal-portfolio.pack` should be used by
+//! [web-static-pack](https://crates.io/crates/web-static-pack)
+//! (the loader part) to serve it from your app.
+//!
+//! # Using as a library
+//! When using as a library, you are most likely willing to create a loadable
+//! (by the loader) `pack`, by using [pack::Builder].
+//!
+//! You will need to add [file_pack_path::FilePackPath] (file + path) objects to
+//! the builder, which you can obtain by:
+//! - Manually constructing the object from [common::pack_path::PackPath] and
+//! [common::file::File] (obtained from fs [file::build_from_path] or memory
+//! slice [file::build_from_content]).
+//! - Reading single file with [file_pack_path::FilePackPath::build_from_path].
+//! - Automatic search through fs with [directory::search].
+//!
+//! When all files are added to the builder, you will need to finalize it and
+//! either write to fs (to have it included in your target application) with
+//! [pack::store_file] or (mostly for test purposes) serialize to memory with
+//! [pack::store_memory].
+//!
+//! ### Examples
+//! This example will do exactly the same as one for application scenario:
+//! ```no_run
+//! # use anyhow::Error;
+//! # use std::path::PathBuf;
+//! # use web_static_pack_packer::{
+//! # directory::{search, SearchOptions},
+//! # file::BuildFromPathOptions,
+//! # pack::{store_file, Builder},
+//! # };
+//!
+//! # fn main() -> Result<(), Error> {
+//! // start with empty pack builder
+//! let mut pack = Builder::new();
+//!
+//! // add files with directory search and default options
+//! pack.file_pack_paths_add(search(
+//! &PathBuf::from("vcard-personal-portfolio"),
+//! &SearchOptions::default(),
+//! &BuildFromPathOptions::default(),
+//! )?)?;
+//!
+//! // finalize the builder, obtain pack
+//! let pack = pack.finalize();
+//!
+//! // store (serialize `pack` to the fs) to be included in the target app
+//! store_file(&pack, &PathBuf::from("vcard-personal-portfolio.pack"))?;
+//! # Ok(())
+//! # }
+//! ```
+//!
+//! For more examples browse through modules of this crate.
+
+#![allow(clippy::new_without_default)]
+#![warn(missing_docs)]
+
+pub use web_static_pack_common as common;
+
+pub mod directory;
+pub mod file;
+pub mod file_pack_path;
+pub mod pack;
+pub mod pack_path;
diff --git a/packer/src/main.rs b/packer/src/main.rs
index 83675e3..e94df8b 100644
--- a/packer/src/main.rs
+++ b/packer/src/main.rs
@@ -1,64 +1,191 @@
-//! # web-static-pack-packer
-//! Executable to build packs for web-static-pack crate
-//! See main crate for details
-//!
-//! [![docs.rs](https://docs.rs/web-static-pack-packer/badge.svg)](https://docs.rs/web-static-pack-packer)
-//!
-//! ## Usage
-//! 1. Install (`cargo install web-static-pack-packer`) or run locally (`cargo run`)
-//! 2. Provide positional arguments:
-//! - `` - the directory to pack
-//! - `` - name of the build pack
-//! - `[root_pach]` - relative path to build pack paths with. use the same as `path` to have all paths in pack root
-//! 3. Use `` file with `web-static-pack` (loader)
-
-mod packer;
+//! Main packer executable, to be used as cli tool. For help run this command
+//! with `-h`.
+
+#![warn(missing_docs)]
use anyhow::{Context, Error};
-use log::LevelFilter;
-use packer::Pack;
-use simple_logger::SimpleLogger;
-use std::path::PathBuf;
+use clap::{Args, Parser, Subcommand};
+use std::{io::stdin, path::PathBuf};
+use web_static_pack_packer::{directory, file, file_pack_path, pack};
+
+#[derive(Parser, Debug)]
+#[command(version, about)]
+struct Arguments {
+ #[command(subcommand)]
+ pub command: Command,
+}
+
+#[derive(Args, Debug)]
+struct FileGlobalOptions {
+ /// Add gzipped version of file to the `pack`. If not set, uses sane
+ /// defaults.
+ #[arg(long)]
+ pub use_gzip: Option,
+ /// Add brotli compressed version of file to the `pack`. If not set, uses
+ /// sane defaults.
+ #[arg(long)]
+ pub use_brotli: Option,
+}
+impl FileGlobalOptions {
+ pub fn into_file_build_from_path_options(self) -> file::BuildFromPathOptions {
+ let mut file_build_from_path_options = file::BuildFromPathOptions::default();
+
+ if let Some(use_gzip) = self.use_gzip {
+ file_build_from_path_options.use_gzip = use_gzip;
+ }
+
+ if let Some(use_brotli) = self.use_brotli {
+ file_build_from_path_options.use_brotli = use_brotli;
+ }
+
+ file_build_from_path_options
+ }
+}
+
+#[derive(Subcommand, Debug)]
+enum Command {
+ /// Creates a single `pack` from recursively searching through single
+ /// directory.
+ ///
+ /// Please note that all found files are added, including hidden files
+ /// (starting with `.` on unix and with certain flags on windows).
+ DirectorySingle {
+ #[command(flatten)]
+ file_global_options: FileGlobalOptions,
+
+ /// Whether to follow links while traversing directories. If not set,
+ /// uses sane defaults.
+ #[arg(long)]
+ follow_links: Option,
+
+ /// The directory to be added to the `pack`.
+ input_directory_path: PathBuf,
+
+ /// Output `pack` path.
+ output_file_path: PathBuf,
+ },
+ /// Creates `pack` from options and list of files supplied through command
+ /// line.
+ FilesCmd {
+ #[command(flatten)]
+ file_global_options: FileGlobalOptions,
+
+ /// Output `pack` path.
+ output_file_path: PathBuf,
+
+ /// Base directory path, used to resolve relative for file inside
+ /// `pack`. All added files must be inside this directory.
+ input_base_directory_path: PathBuf,
+
+ /// List of files to be added to the `pack`.
+ input_file_paths: Vec,
+ },
+ /// Creates `pack` from options supplied through command line and list of
+ /// files from stdin.
+ FilesStdin {
+ #[command(flatten)]
+ file_global_options: FileGlobalOptions,
+
+ /// Base directory path, used to resolve relative for file inside
+ /// `pack`. All added files must be inside this directory.
+ input_base_directory_path: PathBuf,
+
+ /// Output `pack` path.
+ output_file_path: PathBuf,
+ },
+}
fn main() -> Result<(), Error> {
- SimpleLogger::new()
- .env()
- .with_level(LevelFilter::Info)
- .init()
- .unwrap();
-
- let matches = clap::builder::Command::new("web-static-pack packer")
- .arg(
- clap::builder::Arg::new("path")
- .help("the directory to pack")
- .required(true)
- .value_parser(clap::builder::PathBufValueParser::new()),
- )
- .arg(
- clap::builder::Arg::new("output_file")
- .help("name of the build pack")
- .required(true)
- .value_parser(clap::builder::PathBufValueParser::new())
- ,
- )
- .arg(
- clap::builder::Arg::new("root_path")
- .help("relative path to build pack paths with. use the same as `path` to have all paths in pack root")
- .required(false)
- .value_parser(clap::builder::PathBufValueParser::new())
- )
- .get_matches();
-
- let path = matches.get_one::("path").cloned().unwrap();
- let output_file = matches.get_one::("output_file").cloned().unwrap();
- let root_path = matches
- .get_one::("root_path")
- .cloned()
- .unwrap_or_else(PathBuf::new);
-
- let mut pack = Pack::new();
- pack.directory_add(&path, &root_path)
- .context("directory_add")?;
- pack.store(&output_file).context("store")?;
+ let arguments = Arguments::parse();
+
+ match arguments.command {
+ Command::DirectorySingle {
+ file_global_options,
+ follow_links,
+ input_directory_path,
+ output_file_path,
+ } => {
+ let mut directory_search_options = directory::SearchOptions::default();
+ if let Some(follow_links) = follow_links {
+ directory_search_options.follow_links = follow_links;
+ }
+
+ let file_build_from_path_options =
+ file_global_options.into_file_build_from_path_options();
+
+ let mut pack_builder = pack::Builder::new();
+ for file_pack_path in directory::search(
+ &input_directory_path,
+ &directory_search_options,
+ &file_build_from_path_options,
+ )? {
+ // TODO: provide information which file produced error
+ pack_builder.file_pack_path_add(file_pack_path)?
+ }
+
+ let pack = pack_builder.finalize();
+ pack::store_file(&pack, &output_file_path)?;
+ }
+ Command::FilesCmd {
+ file_global_options,
+ output_file_path,
+ input_base_directory_path,
+ input_file_paths,
+ } => {
+ let file_build_from_path_options =
+ file_global_options.into_file_build_from_path_options();
+
+ let mut pack_builder = pack::Builder::new();
+ for input_file_path in input_file_paths {
+ // TODO: move this into try block with shared context
+ let input_file_error_context = || input_file_path.to_string_lossy().into_owned();
+
+ let file_pack_path = file_pack_path::FilePackPath::build_from_path(
+ &input_file_path,
+ &input_base_directory_path,
+ &file_build_from_path_options,
+ )
+ .with_context(input_file_error_context)?;
+
+ pack_builder
+ .file_pack_path_add(file_pack_path)
+ .with_context(input_file_error_context)?;
+ }
+
+ let pack = pack_builder.finalize();
+ pack::store_file(&pack, &output_file_path)?;
+ }
+ Command::FilesStdin {
+ file_global_options,
+ input_base_directory_path,
+ output_file_path,
+ } => {
+ let file_build_from_path_options =
+ file_global_options.into_file_build_from_path_options();
+
+ let mut pack_builder = pack::Builder::new();
+ for input_file_path in stdin().lines() {
+ let input_file_path = PathBuf::from(input_file_path?);
+
+ // TODO: move this into try block with shared context
+ let input_file_error_context = || input_file_path.to_string_lossy().into_owned();
+
+ let file_pack_path = file_pack_path::FilePackPath::build_from_path(
+ &input_file_path,
+ &input_base_directory_path,
+ &file_build_from_path_options,
+ )
+ .with_context(input_file_error_context)?;
+
+ pack_builder
+ .file_pack_path_add(file_pack_path)
+ .with_context(|| input_file_path.to_string_lossy().into_owned())?;
+ }
+
+ let pack = pack_builder.finalize();
+ pack::store_file(&pack, &output_file_path)?;
+ }
+ }
+
Ok(())
}
diff --git a/packer/src/pack.rs b/packer/src/pack.rs
new file mode 100644
index 0000000..301e865
--- /dev/null
+++ b/packer/src/pack.rs
@@ -0,0 +1,113 @@
+//! Pack helpers. Contains [Builder], builder for [Pack].
+
+use crate::{
+ common::{file::File, pack::Pack, pack_path::PackPath, PACK_FILE_MAGIC, PACK_FILE_VERSION},
+ file_pack_path::FilePackPath,
+};
+use anyhow::{bail, Error};
+use rkyv::{
+ ser::{
+ serializers::{AllocScratch, CompositeSerializer, WriteSerializer},
+ Serializer,
+ },
+ AlignedVec, Infallible,
+};
+use std::{
+ collections::{hash_map, HashMap},
+ fs, io,
+ path::Path,
+};
+
+/// Main builder for `pack`. Inside it keeps list of [File] under respective
+/// [PackPath].
+#[derive(Debug)]
+pub struct Builder {
+ files_by_pack_path: HashMap,
+}
+impl Builder {
+ /// Creates empty [self] to be filled with files.
+ pub fn new() -> Self {
+ let files_by_pack_path = HashMap::::new();
+
+ Self { files_by_pack_path }
+ }
+
+ /// Adds file to the `pack`.
+ pub fn file_pack_path_add(
+ &mut self,
+ file_pack_path: FilePackPath,
+ ) -> Result<(), Error> {
+ let entry = match self.files_by_pack_path.entry(file_pack_path.pack_path) {
+ hash_map::Entry::Occupied(_entry) => {
+ bail!("file on specified path already exist");
+ }
+ hash_map::Entry::Vacant(entry) => entry,
+ };
+
+ entry.insert(file_pack_path.file);
+
+ Ok(())
+ }
+
+ /// Adds collection of files to the `pack`.
+ pub fn file_pack_paths_add(
+ &mut self,
+ file_pack_paths: impl IntoIterator- ,
+ ) -> Result<(), Error> {
+ file_pack_paths
+ .into_iter()
+ .try_for_each(|file_pack_path| self.file_pack_path_add(file_pack_path))?;
+
+ Ok(())
+ }
+
+ /// Finalizes to builder, returning built [Pack].
+ pub fn finalize(self) -> Pack {
+ Pack {
+ files_by_path: self.files_by_pack_path,
+ }
+ }
+}
+
+fn store(
+ pack: &Pack,
+ mut writer: impl io::Write,
+) -> Result<(), Error> {
+ // NOTE: we rely on `pack` being 16-aligned two u64 at the beginning keeps
+ // alignment unchanged, but adding anything here will break it.
+ writer.write_all(&PACK_FILE_MAGIC.to_ne_bytes())?;
+ writer.write_all(&PACK_FILE_VERSION.to_ne_bytes())?;
+
+ let mut inner_serializer = CompositeSerializer::new(
+ WriteSerializer::new(&mut writer),
+ AllocScratch::new(),
+ Infallible,
+ );
+ inner_serializer.serialize_value(pack)?;
+
+ Ok(())
+}
+
+/// Serializes `pack` to [AlignedVec]. Serialized data can be used with `load`
+/// method of loader.
+pub fn store_memory(pack: &Pack) -> Result
{
+ let mut buffer = AlignedVec::new();
+ store(pack, &mut buffer)?;
+ Ok(buffer)
+}
+
+/// Serializes `pack` to given file path Serialized data can be used with `load`
+/// method of loader.
+pub fn store_file(
+ pack: &Pack,
+ path: &Path,
+) -> Result<(), Error> {
+ let mut file = fs::File::create(path)?;
+
+ store(pack, &mut file)?;
+
+ file.sync_all()?;
+ drop(file);
+
+ Ok(())
+}
diff --git a/packer/src/pack_path.rs b/packer/src/pack_path.rs
new file mode 100644
index 0000000..9e7ad3a
--- /dev/null
+++ b/packer/src/pack_path.rs
@@ -0,0 +1,94 @@
+//! Pack path helpers. Contains [from_file_base_relative_path] that creates pack
+//! paths from fs paths.
+
+use crate::common::pack_path::PackPath;
+use anyhow::{anyhow, ensure, Error};
+use std::{
+ iter,
+ path::{Component, Path},
+};
+
+/// Creates pack path (eg. "/dir1/dir2/file.html") from relative fs path (eg.
+/// "workdir\\dir1\\dir2\\file.html").
+///
+/// # Examples
+///
+/// ```
+/// # use anyhow::Error;
+/// # use std::path::PathBuf;
+/// # use web_static_pack_packer::{
+/// # common::pack_path::PackPath, pack_path::from_file_base_relative_path,
+/// # };
+/// #
+/// # fn main() -> Result<(), Error> {
+/// #
+/// assert_eq!(
+/// from_file_base_relative_path(&PathBuf::from("path\\to\\file.txt"))?,
+/// PackPath::from_string("/path/to/file.txt".to_owned()),
+/// );
+/// #
+/// # Ok(())
+/// # }
+/// ```
+pub fn from_file_base_relative_path(file_base_relative_path: &Path) -> Result {
+ assert!(file_base_relative_path.is_relative());
+
+ // list of path components, eg. ["dir1", "dir2", "file.bin"]
+ let file_base_relative_path_components = file_base_relative_path
+ .components()
+ .map(|component| {
+ // we cannot handle things like '/' or '.' or '..' here
+ ensure!(
+ matches!(component, Component::Normal(_)),
+ "relative path must not contain only standard path items, got {:?}",
+ component
+ );
+
+ component
+ .as_os_str()
+ .to_str()
+ .ok_or_else(|| anyhow!("cannot convert path component to string"))
+ })
+ .collect::, Error>>()?;
+
+ // we add empty element at the beginning to have path starting with /
+ let pack_path_string = itertools::join(
+ iter::once("").chain(file_base_relative_path_components),
+ "/",
+ );
+
+ // convert into pack path
+ let pack_path = PackPath::from_string(pack_path_string);
+
+ Ok(pack_path)
+}
+
+#[cfg(test)]
+mod test {
+ use super::from_file_base_relative_path;
+ use crate::common::pack_path::PackPath;
+ use std::path::{Path, PathBuf};
+ use test_case::test_case;
+
+ #[test_case(
+ &PathBuf::from("somefile"),
+ &PackPath::from_string("/somefile".to_owned());
+ "base file path without prefix"
+ )]
+ #[test_case(
+ &PathBuf::from("linux/like/relative/path.html"),
+ &PackPath::from_string("/linux/like/relative/path.html".to_owned());
+ "linux like relative path"
+ )]
+ #[test_case(
+ &PathBuf::from("Project\\MyApp\\Application.js"),
+ &PackPath::from_string("/Project/MyApp/Application.js".to_owned());
+ "windows relative path"
+ )]
+ fn from_file_base_relative_path_returns_expected(
+ path: &Path,
+ expected: &PackPath,
+ ) {
+ assert_eq!(&from_file_base_relative_path(path).unwrap(), expected);
+ }
+}
diff --git a/packer/src/packer.rs b/packer/src/packer.rs
deleted file mode 100644
index 9873e16..0000000
--- a/packer/src/packer.rs
+++ /dev/null
@@ -1,226 +0,0 @@
-use anyhow::{anyhow, Context, Error};
-use bytes::Bytes;
-use libflate::gzip;
-use sha3::{Digest, Sha3_256};
-use std::{
- collections::LinkedList,
- convert::TryInto,
- fs,
- fs::File,
- io::Write,
- path::{Component, Path, PathBuf},
-};
-use walkdir::WalkDir;
-
-struct FileDescriptor {
- pack_path: String,
- content_type: String,
- etag: String,
- content: Bytes,
- content_gzip: Option,
-}
-impl FileDescriptor {
- fn serialize_into(
- &mut self,
- write: &mut W,
- ) -> Result<(), Error> {
- // pack_path
- let pack_path_bytes = self.pack_path.as_bytes();
- // pack_path length, u16, 2 bytes
- let pack_path_bytes_length: u16 = pack_path_bytes
- .len()
- .try_into()
- .context("pack_path_bytes_length")?;
- write
- .write_all(&pack_path_bytes_length.to_ne_bytes())
- .context("pack_path_bytes_length")?;
- // pack_path, length as above
- write
- .write_all(pack_path_bytes)
- .context("pack_path_bytes")?;
-
- // content_type
- let content_type_bytes = self.content_type.as_bytes();
- // content_type length, u8, 1 byte
- let content_type_bytes_length: u8 = content_type_bytes
- .len()
- .try_into()
- .context("content_type_bytes_length")?;
- write
- .write_all(&content_type_bytes_length.to_ne_bytes())
- .context("content_type_bytes_length")?;
- // content_type, length as above
- write
- .write_all(content_type_bytes)
- .context("content_type_bytes")?;
-
- // etag
- let etag_bytes = self.etag.as_bytes();
- // etag length, u8, 1 byte
- let etag_bytes_length: u8 = etag_bytes.len().try_into().context("etag_bytes_length")?;
- write
- .write_all(&etag_bytes_length.to_ne_bytes())
- .context("etag_bytes_length")?;
- // etag, length as above
- write.write_all(etag_bytes).context("etag_bytes")?;
-
- // content
- // size as u32, should be enough
- let content_bytes_length: u32 = self
- .content
- .len()
- .try_into()
- .context("content_bytes_length")?;
- write
- .write_all(&content_bytes_length.to_ne_bytes())
- .context("content_bytes_length")?;
- // content, length as above
- write.write_all(&self.content).context("content_bytes")?;
-
- // content_gzip, optional
- // size as u32, should be enough
- let content_bytes_gzip_length: u32 = match self.content_gzip.as_ref() {
- Some(content_gzip) => content_gzip.len().try_into().context("content_gzip")?,
- None => 0,
- };
- write
- .write_all(&content_bytes_gzip_length.to_ne_bytes())
- .context("content_bytes_gzip_length")?;
- // content_gzip, if available
- if let Some(content_gzip) = self.content_gzip.as_ref() {
- write.write_all(content_gzip).context("content_gzip")?;
- }
-
- // All done
- Ok(())
- }
-}
-
-pub struct Pack {
- file_descriptors: LinkedList,
-}
-impl Pack {
- pub fn new() -> Self {
- Pack {
- file_descriptors: LinkedList::new(),
- }
- }
- pub fn file_add(
- &mut self,
- fs_path: PathBuf,
- pack_path: String,
- ) -> Result<(), Error> {
- log::info!(
- "Packing file {} -> {}",
- fs_path.as_path().to_string_lossy(),
- pack_path
- );
-
- // content
- let content = Bytes::from(fs::read(&fs_path).context("content")?);
-
- // content_gzip
- let mut content_gzip = gzip::Encoder::new(Vec::new()).context("content_gzip")?;
- content_gzip.write_all(&content).context("content_gzip")?;
- let content_gzip = Bytes::from(
- content_gzip
- .finish()
- .into_result()
- .context("content_gzip")?,
- );
- let content_gzip = if content_gzip.len() < content.len() {
- Some(content_gzip)
- } else {
- None
- };
-
- // content_type
- let mut content_type = mime_guess::from_path(&fs_path)
- .first_or_octet_stream()
- .as_ref()
- .to_owned();
- if content_type.starts_with("text/") {
- content_type.push_str("; charset=UTF-8");
- }
-
- // etag
- let mut etag = Sha3_256::new();
- etag.update(&content);
- let etag = etag.finalize();
- let etag = format!("\"{:x}\"", &etag); // ETag as "quoted" hex sha3
-
- // Info
- log::info!(
- "Packed {}: content_type={}, etag={}, content.len={}, content_gzip.len={:?}",
- pack_path,
- content_type,
- etag,
- content.len(),
- content_gzip.as_ref().map(|content_gzip| content_gzip.len())
- );
-
- // FileDescriptor
- self.file_descriptors.push_back(FileDescriptor {
- pack_path,
- content_type,
- etag,
- content,
- content_gzip,
- });
-
- Ok(())
- }
- pub fn directory_add(
- &mut self,
- fs_path: &Path,
- pack_path_prefix: &Path,
- ) -> Result<(), Error> {
- let walk_dir = WalkDir::new(fs_path).follow_links(true);
- for entry in walk_dir {
- let entry = entry.context("entry")?;
-
- // Strip directories
- if entry.file_type().is_dir() {
- continue;
- }
-
- // Extract path from record
- let entry_path = entry.into_path();
-
- // Strip fs_path from entry path (make it relative to fs_path)
- // Add pack_path_prefix
- let relative_path =
- pack_path_prefix.join(entry_path.strip_prefix(fs_path).context("relative_path")?);
-
- // Convert directory notation to linux-like
- let pack_path_components = relative_path
- .components()
- .filter(|component| !matches!(component, Component::RootDir))
- .map(|component| {
- component
- .as_os_str()
- .to_str()
- .ok_or_else(|| anyhow!("Cannot convert path component to string"))
- })
- .collect::, _>>()
- .context("pack_path_components")?;
- let pack_path = itertools::join([""].iter().chain(pack_path_components.iter()), "/");
-
- // Add file to pack
- self.file_add(entry_path, pack_path).context("file_add")?;
- }
- Ok(())
- }
- pub fn store(
- &mut self,
- path: &Path,
- ) -> Result<(), Error> {
- let mut file = File::create(path).context("file")?;
- for file_descriptor in self.file_descriptors.iter_mut() {
- file_descriptor
- .serialize_into(&mut file)
- .context("file_descriptor")?;
- }
- Ok(())
- }
-}
diff --git a/rustfmt.toml b/rustfmt.toml
index 8e1f965..f9a835b 100644
--- a/rustfmt.toml
+++ b/rustfmt.toml
@@ -1,3 +1,5 @@
edition = "2021"
-fn_args_layout = "Vertical"
+fn_params_layout = "Vertical"
imports_granularity = "Crate"
+wrap_comments = true
+format_code_in_doc_comments = true
diff --git a/tests/Cargo.toml b/tests/Cargo.toml
new file mode 100644
index 0000000..7b8c5b3
--- /dev/null
+++ b/tests/Cargo.toml
@@ -0,0 +1,29 @@
+[package]
+name = "web-static-pack-tests"
+version = "0.5.0-beta.1"
+authors = ["Paweł Kubrak "]
+edition = "2021"
+license = "MIT"
+description = "Integration tests & data for web-static-pack crate."
+homepage = "https://github.com/peku33/web-static-pack"
+repository = "https://github.com/peku33/web-static-pack"
+publish = false
+
+[dependencies]
+web-static-pack-common = { version = "0.5.0-beta.1", path = "../common" }
+web-static-pack = { version = "0.5.0-beta.1", path = "../loader" }
+web-static-pack-packer = { version = "0.5.0-beta.1", path = "../packer" }
+
+anyhow = "1.0.86"
+futures = "0.3.30"
+http = "1.1.0"
+hyper = { version = "1.4.0", features = ["full"] }
+hyper-util = { version = "0.1.6", features = ["full"] }
+include_bytes_aligned = "0.1.3"
+log = "0.4.22"
+memmap2 = "0.9.4"
+ouroboros = "0.18.4"
+reqwest = { version = "0.12.5", features = ["gzip", "brotli"] }
+simple_logger = "5.0.0"
+test-case = "3.3.1"
+tokio = { version = "1.38.0", features = ["full"] }
diff --git a/tests/data/vcard-personal-portfolio.pack b/tests/data/vcard-personal-portfolio.pack
new file mode 100644
index 0000000..cfc229b
Binary files /dev/null and b/tests/data/vcard-personal-portfolio.pack differ
diff --git a/tests/examples/vcard_personal_portfolio_builder.rs b/tests/examples/vcard_personal_portfolio_builder.rs
new file mode 100644
index 0000000..f36642e
--- /dev/null
+++ b/tests/examples/vcard_personal_portfolio_builder.rs
@@ -0,0 +1,26 @@
+//! Builds data/vcard-personal-portfolio.pack from data/vcard-personal-portfolio
+
+#![feature(async_closure)]
+
+use anyhow::Error;
+use simple_logger::SimpleLogger;
+use std::path::PathBuf;
+use web_static_pack_tests::build_vcard_personal_portfolio_cached;
+
+fn main() -> Result<(), Error> {
+ SimpleLogger::new().init().unwrap();
+
+ let directory = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("data");
+ assert!(directory.is_dir());
+
+ log::trace!("building pack");
+ let pack = build_vcard_personal_portfolio_cached();
+
+ log::trace!("saving pack");
+ web_static_pack_packer::pack::store_file(
+ pack,
+ &directory.join("vcard-personal-portfolio.pack"),
+ )?;
+
+ Ok(())
+}
diff --git a/tests/examples/vcard_personal_portfolio_server.rs b/tests/examples/vcard_personal_portfolio_server.rs
new file mode 100644
index 0000000..57cc55a
--- /dev/null
+++ b/tests/examples/vcard_personal_portfolio_server.rs
@@ -0,0 +1,44 @@
+//! Spins up a server listening on [BIND] serving contents of
+//! `data/vcard-personal-portfolio.pack`. Pack should be created upfront with
+//! builder example.
+
+#![feature(async_closure)]
+
+use anyhow::Error;
+use futures::{channel::oneshot, try_join};
+use include_bytes_aligned::include_bytes_aligned;
+use simple_logger::SimpleLogger;
+use std::net::{IpAddr, Ipv4Addr, SocketAddr};
+use tokio::signal::ctrl_c;
+use web_static_pack_tests::serve_pack;
+
+static PACK_ARCHIVED_SERIALIZED: &[u8] =
+ include_bytes_aligned!(16, "../data/vcard-personal-portfolio.pack");
+
+const BIND: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3000);
+
+#[tokio::main(flavor = "current_thread")]
+async fn main() -> Result<(), Error> {
+ SimpleLogger::new().init().unwrap();
+
+ // load `pack` from prebuild version
+ log::trace!("loading pack");
+ let pack_archived = unsafe { web_static_pack::loader::load(PACK_ARCHIVED_SERIALIZED).unwrap() };
+
+ // ctrl_c handler
+ let (shutdown_sender, shutdown_receiver) = oneshot::channel::<()>();
+ let ctrl_c_runner = async move {
+ ctrl_c().await?;
+ shutdown_sender.send(()).unwrap();
+ Ok(())
+ };
+
+ // server
+ log::trace!("running server, go to http://{BIND}/index.html");
+ let server_runner = serve_pack(pack_archived, Some(BIND), None, shutdown_receiver);
+
+ // combine and run
+ try_join!(ctrl_c_runner, server_runner)?;
+
+ Ok(())
+}
diff --git a/tests/src/lib.rs b/tests/src/lib.rs
new file mode 100644
index 0000000..5178b9a
--- /dev/null
+++ b/tests/src/lib.rs
@@ -0,0 +1,220 @@
+#![doc(hidden)]
+
+use anyhow::{anyhow, ensure, Context, Error};
+use futures::{
+ channel::oneshot,
+ future::{select, Either},
+ pin_mut,
+};
+use hyper::{body::Incoming, header, server::conn::http1, service::service_fn, Request};
+use hyper_util::{rt::TokioIo, server::graceful::GracefulShutdown};
+use memmap2::Mmap;
+use ouroboros::self_referencing;
+use reqwest::Client;
+use std::{
+ convert::Infallible,
+ fs::File,
+ mem::transmute,
+ net::{Ipv4Addr, SocketAddr, SocketAddrV4},
+ path::PathBuf,
+ sync::LazyLock,
+};
+use tokio::net::TcpListener;
+
+// builds [web_static_pack_common::pack::Pack] from
+// data/vcard-personal-portfolio
+fn build_vcard_personal_portfolio() -> Result {
+ let mut pack = web_static_pack_packer::pack::Builder::new();
+ pack.file_pack_paths_add(web_static_pack_packer::directory::search(
+ &PathBuf::from(env!("CARGO_MANIFEST_DIR"))
+ .join("data")
+ .join("vcard-personal-portfolio"),
+ &web_static_pack_packer::directory::SearchOptions::default(),
+ &web_static_pack_packer::file::BuildFromPathOptions::default(),
+ )?)?;
+ let pack = pack.finalize();
+
+ Ok(pack)
+}
+pub fn build_vcard_personal_portfolio_cached() -> &'static web_static_pack_common::pack::Pack {
+ static CACHE: LazyLock =
+ LazyLock::new(|| build_vcard_personal_portfolio().unwrap());
+
+ &CACHE
+}
+
+// loads (mmaps) data/vcard-personal-portfolio.pack
+#[self_referencing]
+struct VCardPersonalPortfolioMMap {
+ mmap: Mmap,
+
+ #[borrows(mmap)]
+ pack_archived: &'this web_static_pack_common::pack::PackArchived,
+}
+fn load_vcard_personal_portfolio() -> Result {
+ let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
+ .join("data")
+ .join("vcard-personal-portfolio.pack");
+
+ let file =
+ File::open(path).context("you probably need to run builder example from this crate")?;
+ let mmap = unsafe { Mmap::map(&file)? };
+ // let pack_archived = unsafe { web_static_pack::loader::load(&*mmap) }?;
+
+ let inner = VCardPersonalPortfolioMMap::try_new(mmap, |mmap| unsafe {
+ web_static_pack::loader::load(mmap)
+ })?;
+
+ Ok(inner)
+}
+pub fn load_vcard_personal_portfolio_cached() -> &'static web_static_pack_common::pack::PackArchived
+{
+ static CACHE: LazyLock =
+ LazyLock::new(|| load_vcard_personal_portfolio().unwrap());
+ CACHE.borrow_pack_archived()
+}
+
+// runs a http server serving pack, listening on bind or local emphemeric port
+// if not set, notifying bind_ready_sender when server is ready and where is
+// listening and shuts down when shutdown_receiver yields
+pub async fn serve_pack(
+ pack: &P,
+ bind: Option,
+ bind_ready_sender: Option>,
+ shutdown_receiver: oneshot::Receiver<()>,
+) -> Result<(), Error>
+where
+ P: web_static_pack::pack::Pack + Sync + 'static,
+{
+ log::trace!("staring server");
+
+ // pin shutdown future
+ pin_mut!(shutdown_receiver);
+
+ // make responder from `pack`
+ let responder = web_static_pack::responder::Responder::new(pack);
+
+ // hyper requires service to be static
+ // we use graceful, no connections will outlive server function
+ let responder = unsafe {
+ transmute::<
+ &web_static_pack::responder::Responder<'_, P>,
+ &web_static_pack::responder::Responder<'static, P>,
+ >(&responder)
+ };
+
+ // make hyper service
+ let service_fn = service_fn(|request: Request| async {
+ let (parts, _body) = request.into_parts();
+
+ log::info!("serving {}", parts.uri);
+ let response = responder.respond_flatten(parts);
+
+ Ok::<_, Infallible>(response)
+ });
+
+ // graceful shutdown watcher
+ let graceful = GracefulShutdown::new();
+
+ // use ephemeric port
+ let bind = bind.unwrap_or(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0)));
+
+ // server listener
+ let listener = TcpListener::bind(bind).await?;
+
+ // get final listening port
+ let bind = listener.local_addr()?;
+ log::trace!("listening on {bind}");
+
+ // notify that server is ready
+ if let Some(bind_ready_sender) = bind_ready_sender {
+ bind_ready_sender.send(bind).unwrap();
+ }
+
+ // main loop
+ log::trace!("entering main server loop");
+ loop {
+ let listener_accept = listener.accept();
+ pin_mut!(listener_accept);
+
+ match select(listener_accept, &mut shutdown_receiver).await {
+ Either::Left((result, _)) => {
+ let (stream, _remote_address) = result?;
+ log::trace!("new connection");
+
+ let io = TokioIo::new(stream);
+
+ let connection = http1::Builder::new().serve_connection(io, service_fn);
+ let graceful_connection = graceful.watch(connection);
+
+ tokio::spawn(async move {
+ graceful_connection.await.unwrap();
+ });
+ }
+ Either::Right((result, _)) => {
+ result.unwrap();
+ log::trace!("got exit signal");
+ break;
+ }
+ }
+ }
+
+ log::trace!("waiting for active connections to shutdown");
+ graceful.shutdown().await;
+
+ log::trace!("exiting server");
+ Ok(())
+}
+
+// gets each of paths from pack_paths from pack, performs GET request on
+// http://address/pack_path
+pub async fn download_verify_pack(
+ pack: &P,
+ pack_paths: &[&str],
+ address: SocketAddr,
+) -> Result<(), Error>
+where
+ P: web_static_pack::pack::Pack + Sync + 'static,
+{
+ let client = Client::new();
+
+ for pack_path in pack_paths {
+ log::trace!("downloading {pack_path}");
+
+ let file = pack
+ .get_file_by_path(pack_path)
+ .ok_or_else(|| anyhow!("request file missing in pack"))?;
+
+ let response = client
+ .get(format!("http://{address}{pack_path}"))
+ .send()
+ .await?
+ .error_for_status()?;
+
+ let content_type = response
+ .headers()
+ .get(header::CONTENT_TYPE)
+ .ok_or_else(|| anyhow!("content type missing in response"))?
+ .clone();
+ let etag = response
+ .headers()
+ .get(header::ETAG)
+ .ok_or_else(|| anyhow!("etag missing in response"))?
+ .clone();
+ let cache_control = response
+ .headers()
+ .get(header::CACHE_CONTROL)
+ .ok_or_else(|| anyhow!("cache control missing in response"))?
+ .clone();
+ let content = response.bytes().await?;
+
+ ensure!(content == web_static_pack::file::File::content(file));
+ ensure!(content_type == web_static_pack::file::File::content_type(file));
+ ensure!(etag == web_static_pack::file::File::etag(file));
+ ensure!(cache_control == web_static_pack::file::File::cache_control(file).cache_control());
+ }
+
+ drop(client);
+
+ Ok(())
+}
diff --git a/tests/tests/build_and_load.rs b/tests/tests/build_and_load.rs
new file mode 100644
index 0000000..2ad4145
--- /dev/null
+++ b/tests/tests/build_and_load.rs
@@ -0,0 +1,71 @@
+use std::collections::HashSet;
+use web_static_pack_tests::{
+ build_vcard_personal_portfolio_cached, load_vcard_personal_portfolio_cached,
+};
+
+#[test]
+fn builder_builds_pack_with_same_contents() {
+ // prebuilt `pack` loaded from data
+ let pack_archived = load_vcard_personal_portfolio_cached();
+
+ // `pack` built from source files
+ let pack = build_vcard_personal_portfolio_cached();
+
+ // check if they contain equal keys
+ assert_eq!(
+ pack.files_by_path
+ .keys()
+ .map(|pack_path| &**pack_path)
+ .collect::>(),
+ pack_archived
+ .files_by_path
+ .keys()
+ .map(|pack_path_archived| &**pack_path_archived)
+ .collect::>()
+ );
+
+ // zip values and check if they are equal
+ pack.files_by_path
+ .iter()
+ .map(|(pack_path, file)| {
+ (
+ pack_path,
+ file,
+ pack_archived.files_by_path.get(&**pack_path).unwrap(),
+ )
+ })
+ .for_each(|(_pack_path, file, file_archived)| {
+ assert_eq!(&*file.content, &*file_archived.content);
+ assert_eq!(
+ file.content_gzip.as_deref(),
+ file_archived.content_gzip.as_deref()
+ );
+ assert_eq!(
+ file.content_brotli.as_deref(),
+ file_archived.content_brotli.as_deref()
+ );
+
+ assert_eq!(file.content_type, file_archived.content_type);
+ assert_eq!(file.etag, file_archived.etag);
+ // assert_eq!(file.cache_control, file_archived.cache_control);
+ // TODO: fix this comparision
+ });
+}
+
+#[test]
+fn loader_loads_correctly_prebuilt_pack() {
+ let pack_archived = load_vcard_personal_portfolio_cached();
+
+ // index.html should have content-type: text/html; charset=utf-8
+ assert_eq!(
+ pack_archived
+ .files_by_path
+ .get("/index.html")
+ .unwrap()
+ .content_type,
+ "text/html; charset=utf-8"
+ );
+
+ // index.php should not exists
+ assert!(pack_archived.files_by_path.get("/index.php").is_none());
+}
diff --git a/tests/tests/load_and_serve.rs b/tests/tests/load_and_serve.rs
new file mode 100644
index 0000000..e68837a
--- /dev/null
+++ b/tests/tests/load_and_serve.rs
@@ -0,0 +1,45 @@
+#![feature(async_closure)]
+
+use futures::{channel::oneshot, try_join};
+use std::net::SocketAddr;
+use web_static_pack_tests::{
+ download_verify_pack, load_vcard_personal_portfolio_cached, serve_pack,
+};
+
+#[tokio::test(flavor = "current_thread")]
+async fn client_downloads_verifies_served_by_server() {
+ // `pack` to run both server and client on
+ let pack_archived = load_vcard_personal_portfolio_cached();
+
+ // all paths from `pack` used for test
+ let pack_paths = pack_archived
+ .files_by_path
+ .keys()
+ .map(|pack_path_archived| &**pack_path_archived)
+ .collect::>();
+
+ // makes client wait for server to become ready and turns it off when completed
+ let (bind_ready_sender, bind_ready_receiver) = oneshot::channel::();
+ let (shutdown_sender, shutdown_receiver) = oneshot::channel::<()>();
+
+ // server
+ let server = serve_pack(
+ pack_archived,
+ None,
+ Some(bind_ready_sender),
+ shutdown_receiver,
+ );
+
+ // client
+ let download_verifier = async move {
+ let bind = bind_ready_receiver.await?;
+
+ download_verify_pack(pack_archived, &pack_paths, bind).await?;
+
+ shutdown_sender.send(()).unwrap();
+ Ok(())
+ };
+
+ // wait for all
+ try_join!(server, download_verifier).unwrap();
+}
diff --git a/tests/tests/serve_custom.rs b/tests/tests/serve_custom.rs
new file mode 100644
index 0000000..3ba6799
--- /dev/null
+++ b/tests/tests/serve_custom.rs
@@ -0,0 +1,241 @@
+#![feature(async_closure)]
+
+use anyhow::{anyhow, Error};
+use futures::{channel::oneshot, try_join, Future};
+use http::{header, HeaderMap, HeaderName, HeaderValue, StatusCode};
+use reqwest::{get, Client, ClientBuilder, Url};
+use std::net::SocketAddr;
+use test_case::test_case;
+use web_static_pack_tests::serve_pack;
+
+struct FileMock;
+impl web_static_pack::file::File for FileMock {
+ fn content(&self) -> &[u8] {
+ b"content-identity-is-the-longest-and-least-preferred-option"
+ }
+ fn content_gzip(&self) -> Option<&[u8]> {
+ // "content-gzip"
+ Some(b"\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03\x4b\xce\xcf\x2b\x49\xcd\x2b\xd1\x4d\xaf\xca\x2c\x00\x00\x98\x02\x99\x74\x0c\x00\x00\x00")
+ }
+ fn content_brotli(&self) -> Option<&[u8]> {
+ // "content-brotli"
+ Some(b"\x8b\x06\x80\x63\x6f\x6e\x74\x65\x6e\x74\x2d\x62\x72\x6f\x74\x6c\x69\x03")
+ }
+
+ fn content_type(&self) -> HeaderValue {
+ HeaderValue::from_static("text/plain; charset=utf-8")
+ }
+ fn etag(&self) -> HeaderValue {
+ HeaderValue::from_static("\"etagvalue\"")
+ }
+ fn cache_control(&self) -> web_static_pack::cache_control::CacheControl {
+ web_static_pack::cache_control::CacheControl::MaxCache
+ }
+}
+struct PackMock;
+impl web_static_pack::pack::Pack for PackMock {
+ type File = FileMock;
+
+ fn get_file_by_path(
+ &self,
+ path: &str,
+ ) -> Option<&Self::File> {
+ match path {
+ "/present" => Some(&FileMock),
+ _ => None,
+ }
+ }
+}
+
+async fn run_with_server>, E: FnOnce(Url) -> F>(
+ executor: E, // async fn executor(base_url: Url) -> Result<(), Error> { ... }
+) -> Result<(), Error> {
+ let (bind_ready_sender, bind_ready_receiver) = oneshot::channel::();
+ let (shutdown_sender, shutdown_receiver) = oneshot::channel::<()>();
+
+ let server = serve_pack(&PackMock, None, Some(bind_ready_sender), shutdown_receiver);
+
+ let verifier = async move {
+ let bind = bind_ready_receiver.await?;
+ let base_url = Url::parse(&format!("http://{bind}/"))?;
+
+ executor(base_url).await?;
+
+ shutdown_sender.send(()).unwrap();
+ Ok(())
+ };
+
+ try_join!(server, verifier)?;
+
+ Ok(())
+}
+
+fn header_as_string(
+ headers: &HeaderMap,
+ name: HeaderName,
+) -> &str {
+ headers
+ .get(&name)
+ .ok_or_else(|| anyhow!("missing header {name}"))
+ .unwrap()
+ .to_str()
+ .unwrap()
+}
+
+#[tokio::test(flavor = "current_thread")]
+async fn responds_to_typical_request() {
+ run_with_server(async move |base_url: Url| {
+ let response = get(base_url.join("/present")?).await?.error_for_status()?;
+ let headers = response.headers();
+
+ assert_eq!(
+ header_as_string(headers, header::CONTENT_TYPE),
+ "text/plain; charset=utf-8"
+ );
+ assert_eq!(
+ header_as_string(headers, header::ETAG), // line break
+ "\"etagvalue\""
+ );
+ assert_eq!(
+ header_as_string(headers, header::CACHE_CONTROL), // line break
+ "max-age=31536000, immutable"
+ );
+
+ // reqwest strips content-length and content-encoding when using encoding
+ let body = response.bytes().await?;
+ assert_eq!(&*body, b"content-brotli");
+
+ Ok(())
+ })
+ .await
+ .unwrap();
+}
+
+#[test_case(true, true, b"content-brotli"; "all enabled, brotli is the shortest")]
+#[test_case(false, true, b"content-gzip"; "no brotli, but gzip")]
+#[test_case(false, false, b"content-identity-is-the-longest-and-least-preferred-option"; "nothing, should receive identity")]
+#[tokio::test(flavor = "current_thread")]
+async fn responds_with_other_encodings(
+ brotli: bool,
+ gzip: bool,
+ expected: &[u8],
+) {
+ run_with_server(async move |base_url: Url| {
+ let response = ClientBuilder::new()
+ .brotli(brotli)
+ .gzip(gzip)
+ .build()?
+ .get(base_url.join("/present")?)
+ .send()
+ .await?
+ .error_for_status()?;
+
+ // reqwest strips content-length and content-encoding when using encoding
+ let body = response.bytes().await?;
+ assert_eq!(&*body, expected);
+
+ Ok(())
+ })
+ .await
+ .unwrap();
+}
+
+#[tokio::test(flavor = "current_thread")]
+async fn resolves_no_body_for_head_request() {
+ run_with_server(async move |base_url: Url| {
+ let response = Client::new()
+ .head(base_url.join("/present")?)
+ .send()
+ .await?
+ .error_for_status()?;
+ let headers = response.headers();
+
+ // all headers should be there
+ assert_eq!(
+ header_as_string(headers, header::CONTENT_TYPE),
+ "text/plain; charset=utf-8"
+ );
+ assert_eq!(
+ header_as_string(headers, header::ETAG), // line break
+ "\"etagvalue\""
+ );
+ assert_eq!(
+ header_as_string(headers, header::CACHE_CONTROL), // line break
+ "max-age=31536000, immutable"
+ );
+
+ // reqwest strips content-length and content-encoding when using encoding
+ let body = response.bytes().await?;
+ assert_eq!(&*body, b"");
+
+ Ok(())
+ })
+ .await
+ .unwrap();
+}
+
+#[tokio::test(flavor = "current_thread")]
+async fn resolves_not_modified_for_matching_etag() {
+ run_with_server(async move |base_url: Url| {
+ let response = Client::new()
+ .get(base_url.join("/present")?)
+ .header(header::IF_NONE_MATCH, "\"etagvalue\"")
+ .send()
+ .await?;
+ let headers = response.headers();
+
+ assert_eq!(response.status(), StatusCode::NOT_MODIFIED);
+
+ // `ETag` should be resent, others should be missing.
+ assert_eq!(
+ header_as_string(headers, header::ETAG), // line break
+ "\"etagvalue\""
+ );
+ assert!(headers.get(header::CONTENT_TYPE).is_none());
+
+ // of course no body
+ let body = response.bytes().await?;
+ assert_eq!(&*body, b"");
+
+ Ok(())
+ })
+ .await
+ .unwrap();
+}
+
+#[tokio::test(flavor = "current_thread")]
+async fn resolves_error_for_invalid_method() {
+ run_with_server(async move |base_url: Url| {
+ let response = Client::new()
+ .post(base_url.join("/present")?)
+ .send()
+ .await?;
+
+ assert_eq!(response.status(), StatusCode::METHOD_NOT_ALLOWED);
+
+ // no body for flattening responder
+ let body = response.bytes().await?;
+ assert_eq!(&*body, b"");
+
+ Ok(())
+ })
+ .await
+ .unwrap();
+}
+
+#[tokio::test(flavor = "current_thread")]
+async fn resolves_error_for_file_not_found() {
+ run_with_server(async move |base_url: Url| {
+ let response = Client::new().get(base_url.join("/missing")?).send().await?;
+
+ assert_eq!(response.status(), StatusCode::NOT_FOUND);
+
+ // no body for flattening responder
+ let body = response.bytes().await?;
+ assert_eq!(&*body, b"");
+
+ Ok(())
+ })
+ .await
+ .unwrap();
+}