feat: create initial version of proc_macro and lib
This commit is contained in:
parent
7dc755dde4
commit
c459800fca
@ -21,6 +21,7 @@ rust:clippy:
|
||||
stage: test
|
||||
image: rust:1.84-alpine3.21
|
||||
before_script:
|
||||
- apk add musl-dev
|
||||
- rustup component add clippy
|
||||
script:
|
||||
- cargo clippy --all-features -- -D warnings
|
||||
|
610
Cargo.lock
generated
610
Cargo.lock
generated
@ -2,6 +2,616 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.24.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
|
||||
dependencies = [
|
||||
"gimli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8"
|
||||
dependencies = [
|
||||
"axum-core",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"itoa",
|
||||
"matchit",
|
||||
"memchr",
|
||||
"mime",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustversion",
|
||||
"serde",
|
||||
"sync_wrapper",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-core"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"mime",
|
||||
"pin-project-lite",
|
||||
"rustversion",
|
||||
"sync_wrapper",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.74"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"miniz_oxide",
|
||||
"object",
|
||||
"rustc-demangle",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
"libc",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "display_full_error"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc7c47e9a2fd28a8edd1446f10dabe8ac5d26bb77ed2b1077bfcd8308904e8c6"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.31.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body-util"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.171"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
|
||||
|
||||
[[package]]
|
||||
name = "matchit"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.36.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
||||
|
||||
[[package]]
|
||||
name = "pin-utils"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.94"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.219"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.219"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "static-serve"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"bytes",
|
||||
"static-serve-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "static-serve-macro"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"display_full_error",
|
||||
"flate2",
|
||||
"glob",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"sha1",
|
||||
"syn",
|
||||
"thiserror",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sync_wrapper"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.44.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"pin-project-lite",
|
||||
"tokio-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"pin-project-lite",
|
||||
"sync_wrapper",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-layer"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
|
||||
|
||||
[[package]]
|
||||
name = "tower-service"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
version = "0.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
|
||||
dependencies = [
|
||||
"zstd-safe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-safe"
|
||||
version = "7.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
|
||||
dependencies = [
|
||||
"zstd-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-sys"
|
||||
version = "2.0.15+zstd.1.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
22
Cargo.toml
22
Cargo.toml
@ -1,16 +1,26 @@
|
||||
[package]
|
||||
name = "static-serve"
|
||||
[workspace]
|
||||
members = [
|
||||
"static-serve",
|
||||
"static-serve-macro",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.83"
|
||||
description = "A helper for compressing and embedding static assets in an Axum webserver"
|
||||
repository = "https://github.com/M4SS-Code/static-serve"
|
||||
license = "MIT OR Apache-2.0"
|
||||
keywords = ["static", "axum", "embed", "web", "conditional"]
|
||||
categories = ["web-programming", "web-programming::http-server", "filesystem"]
|
||||
|
||||
[dependencies]
|
||||
|
||||
[lints.rust]
|
||||
[workspace.lints.rust]
|
||||
missing_docs = "warn"
|
||||
unsafe_code = "deny"
|
||||
unreachable_pub = "deny"
|
||||
|
||||
[lints.clippy]
|
||||
[workspace.lints.clippy]
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
module_name_repetitions = "allow"
|
||||
await_holding_refcell_ref = "deny"
|
||||
|
@ -6,6 +6,8 @@ ignore = [
|
||||
allow = [
|
||||
"MIT",
|
||||
"Apache-2.0",
|
||||
"Unicode-3.0",
|
||||
"BSD-3-Clause",
|
||||
]
|
||||
|
||||
[licenses.private]
|
||||
|
@ -1 +0,0 @@
|
||||
|
27
static-serve-macro/Cargo.toml
Normal file
27
static-serve-macro/Cargo.toml
Normal file
@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "static-serve-macro"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
keywords.workspace = true
|
||||
description.workspace = true
|
||||
categories.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
display_full_error = "1.1"
|
||||
flate2 = "1.1"
|
||||
glob = "0.3"
|
||||
proc-macro2 = "1.0"
|
||||
quote = "1.0"
|
||||
sha1 = "0.10"
|
||||
syn = { version = "2.0", default-features = false }
|
||||
thiserror = "2.0.12"
|
||||
zstd = "0.13"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
82
static-serve-macro/src/error.rs
Normal file
82
static-serve-macro/src/error.rs
Normal file
@ -0,0 +1,82 @@
|
||||
use std::{
|
||||
ffi::{OsStr, OsString},
|
||||
fmt::{Display, Formatter},
|
||||
io,
|
||||
};
|
||||
|
||||
use glob::{GlobError, PatternError};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub(crate) enum Error {
|
||||
#[error("{}", UnknownFileExtension(.0.as_deref()))]
|
||||
UnknownFileExtension(Option<OsString>),
|
||||
#[error("Cannot canonicalize assets directory")]
|
||||
CannotCanonicalizeDirectory(#[source] io::Error),
|
||||
#[error("Invalid unicode in directory name")]
|
||||
InvalidUnicodeInDirectoryName,
|
||||
#[error("Cannot canonicalize ignore directory")]
|
||||
CannotCanonicalizeIgnoreDir(#[source] io::Error),
|
||||
#[error("Invalid unicode in directory name")]
|
||||
InvalidUnicodeInEntryName,
|
||||
#[error("Error while compressing with gzip")]
|
||||
Gzip(#[from] GzipType),
|
||||
#[error("Error while compressing with zstd")]
|
||||
Zstd(#[from] ZstdType),
|
||||
#[error("Error while reading entry contents")]
|
||||
CannotReadEntryContents(#[source] io::Error),
|
||||
#[error("Error while parsing glob pattern")]
|
||||
Pattern(#[source] PatternError),
|
||||
#[error("Error reading path for glob")]
|
||||
Glob(#[source] GlobError),
|
||||
#[error("Cannot get entry metadata")]
|
||||
CannotGetMetadata(#[source] io::Error),
|
||||
}
|
||||
|
||||
struct UnknownFileExtension<'a>(Option<&'a OsStr>);
|
||||
impl Display for UnknownFileExtension<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self.0 {
|
||||
Some(ext) => write!(
|
||||
f,
|
||||
"Unknown file extension in directory of static assets: {}",
|
||||
ext.to_string_lossy()
|
||||
),
|
||||
None => write!(f, "Missing file extension"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub(crate) enum GzipType {
|
||||
#[error("The compressor could not write")]
|
||||
CompressorWrite(#[source] io::Error),
|
||||
#[error("The encoder could not complete the `finish` procedure")]
|
||||
EncoderFinish(#[source] io::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub(crate) enum ZstdType {
|
||||
#[error("The encoder could not write")]
|
||||
EncoderWrite(#[source] io::Error),
|
||||
#[error("The encoder could not complete the `finish` procedure")]
|
||||
EncoderFinish(#[source] io::Error),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::ffi::OsStr;
|
||||
|
||||
use super::UnknownFileExtension;
|
||||
|
||||
#[test]
|
||||
fn unknown_file_extension() {
|
||||
let missing_extension = UnknownFileExtension(None);
|
||||
assert_eq!(missing_extension.to_string(), "Missing file extension");
|
||||
let unknown_extension = UnknownFileExtension(Some(OsStr::new("pippo")));
|
||||
assert_eq!(
|
||||
unknown_extension.to_string(),
|
||||
"Unknown file extension in directory of static assets: pippo"
|
||||
);
|
||||
}
|
||||
}
|
407
static-serve-macro/src/lib.rs
Normal file
407
static-serve-macro/src/lib.rs
Normal file
@ -0,0 +1,407 @@
|
||||
//! Proc macro crate for compressing and embedding static assets
|
||||
//! in a web server
|
||||
//! Macro invocation: `embed_assets!('path/to/assets', compress = true);`
|
||||
|
||||
use std::{
|
||||
convert::Into,
|
||||
fmt::Display,
|
||||
fs,
|
||||
io::{self, Write},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use display_full_error::DisplayFullError;
|
||||
use flate2::write::GzEncoder;
|
||||
use glob::glob;
|
||||
use proc_macro2::{Span, TokenStream};
|
||||
use quote::{quote, ToTokens};
|
||||
use sha1::{Digest as _, Sha1};
|
||||
use syn::{
|
||||
bracketed,
|
||||
parse::{Parse, ParseStream},
|
||||
parse_macro_input, Ident, LitBool, LitByteStr, LitStr, Token,
|
||||
};
|
||||
|
||||
mod error;
|
||||
use error::{Error, GzipType, ZstdType};
|
||||
|
||||
#[proc_macro]
|
||||
/// Embed and optionally compress static assets for a web server
|
||||
pub fn embed_assets(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
let parsed = parse_macro_input!(input as EmbedAssets);
|
||||
quote! { #parsed }.into()
|
||||
}
|
||||
|
||||
struct EmbedAssets {
|
||||
assets_dir: AssetsDir,
|
||||
validated_ignore_dirs: IgnoreDirs,
|
||||
should_compress: ShouldCompress,
|
||||
}
|
||||
|
||||
impl Parse for EmbedAssets {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let assets_dir: AssetsDir = input.parse()?;
|
||||
|
||||
// Default to no compression
|
||||
let mut maybe_should_compress = None;
|
||||
let mut maybe_ignore_dirs = None;
|
||||
|
||||
while !input.is_empty() {
|
||||
input.parse::<Token![,]>()?;
|
||||
let key: Ident = input.parse()?;
|
||||
input.parse::<Token![=]>()?;
|
||||
|
||||
match key.to_string().as_str() {
|
||||
"compress" => {
|
||||
let value = input.parse()?;
|
||||
maybe_should_compress = Some(value);
|
||||
}
|
||||
"ignore_dirs" => {
|
||||
let value = input.parse()?;
|
||||
maybe_ignore_dirs = Some(value);
|
||||
}
|
||||
_ => {
|
||||
return Err(syn::Error::new(
|
||||
key.span(),
|
||||
"Unknown key in embed_assets! macro. Expected `compress` or `ignore_dirs`",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let should_compress = maybe_should_compress.unwrap_or_else(|| {
|
||||
ShouldCompress(LitBool {
|
||||
value: false,
|
||||
span: Span::call_site(),
|
||||
})
|
||||
});
|
||||
|
||||
let ignore_dirs_with_span = maybe_ignore_dirs.unwrap_or(IgnoreDirsWithSpan(vec![]));
|
||||
let validated_ignore_dirs = validate_ignore_dirs(ignore_dirs_with_span, &assets_dir.0)?;
|
||||
|
||||
Ok(Self {
|
||||
assets_dir,
|
||||
validated_ignore_dirs,
|
||||
should_compress,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for EmbedAssets {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let AssetsDir(assets_dir) = &self.assets_dir;
|
||||
let ignore_dirs = &self.validated_ignore_dirs;
|
||||
let ShouldCompress(should_compress) = &self.should_compress;
|
||||
|
||||
let result = generate_static_routes(assets_dir, ignore_dirs, should_compress);
|
||||
|
||||
match result {
|
||||
Ok(value) => {
|
||||
tokens.extend(quote! {
|
||||
#value
|
||||
});
|
||||
}
|
||||
Err(err_message) => {
|
||||
let error = syn::Error::new(Span::call_site(), err_message);
|
||||
tokens.extend(error.to_compile_error());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AssetsDir(ValidAssetsDirTypes);
|
||||
|
||||
impl Parse for AssetsDir {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let input_span = input.span();
|
||||
let assets_dir: ValidAssetsDirTypes = input.parse()?;
|
||||
let literal = assets_dir.to_string();
|
||||
let path = Path::new(&literal);
|
||||
let metadata = match fs::metadata(path) {
|
||||
Ok(meta) => meta,
|
||||
Err(e) if matches!(e.kind(), std::io::ErrorKind::NotFound) => {
|
||||
return Err(syn::Error::new(
|
||||
input_span,
|
||||
"The specified assets directory does not exist",
|
||||
));
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(syn::Error::new(
|
||||
input_span,
|
||||
format!(
|
||||
"Error reading directory {literal}: {}",
|
||||
DisplayFullError(&e)
|
||||
),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
if !metadata.is_dir() {
|
||||
return Err(syn::Error::new(
|
||||
input_span,
|
||||
"The specified assets directory is not a directory",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(AssetsDir(assets_dir))
|
||||
}
|
||||
}
|
||||
|
||||
enum ValidAssetsDirTypes {
|
||||
LiteralStr(LitStr),
|
||||
Ident(Ident),
|
||||
}
|
||||
|
||||
impl Display for ValidAssetsDirTypes {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::LiteralStr(inner) => write!(f, "{}", inner.value()),
|
||||
Self::Ident(inner) => write!(f, "{inner}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for ValidAssetsDirTypes {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
if let Ok(inner) = input.parse::<LitStr>() {
|
||||
Ok(ValidAssetsDirTypes::LiteralStr(inner))
|
||||
} else {
|
||||
let inner = input.parse::<Ident>().map_err(|_| {
|
||||
syn::Error::new(
|
||||
input.span(),
|
||||
"Assets directory must be a literal string or valid identifier",
|
||||
)
|
||||
})?;
|
||||
Ok(ValidAssetsDirTypes::Ident(inner))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct IgnoreDirs(Vec<PathBuf>);
|
||||
|
||||
struct IgnoreDirsWithSpan(Vec<(PathBuf, Span)>);
|
||||
|
||||
impl Parse for IgnoreDirsWithSpan {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let inner_content;
|
||||
bracketed!(inner_content in input);
|
||||
|
||||
let mut dirs = Vec::new();
|
||||
while !inner_content.is_empty() {
|
||||
let directory_span = inner_content.span();
|
||||
let directory_str = inner_content.parse::<LitStr>()?;
|
||||
if !inner_content.is_empty() {
|
||||
inner_content.parse::<Token![,]>()?;
|
||||
}
|
||||
let path = PathBuf::from(directory_str.value());
|
||||
dirs.push((path, directory_span));
|
||||
}
|
||||
|
||||
Ok(IgnoreDirsWithSpan(dirs))
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_ignore_dirs(
|
||||
ignore_dirs: IgnoreDirsWithSpan,
|
||||
assets_dir: &ValidAssetsDirTypes,
|
||||
) -> syn::Result<IgnoreDirs> {
|
||||
let mut valid_ignore_dirs = Vec::new();
|
||||
for (dir, span) in ignore_dirs.0 {
|
||||
let full_path = PathBuf::from(assets_dir.to_string()).join(&dir);
|
||||
match fs::metadata(&full_path) {
|
||||
Ok(meta) if !meta.is_dir() => {
|
||||
return Err(syn::Error::new(
|
||||
span,
|
||||
"The specified ignored directory is not a directory",
|
||||
));
|
||||
}
|
||||
Ok(_) => valid_ignore_dirs.push(full_path),
|
||||
Err(e) if matches!(e.kind(), std::io::ErrorKind::NotFound) => {
|
||||
return Err(syn::Error::new(
|
||||
span,
|
||||
"The specified ignored directory does not exist",
|
||||
))
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(syn::Error::new(
|
||||
span,
|
||||
format!(
|
||||
"Error reading ignored directory {}: {}",
|
||||
dir.to_string_lossy(),
|
||||
DisplayFullError(&e)
|
||||
),
|
||||
))
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok(IgnoreDirs(valid_ignore_dirs))
|
||||
}
|
||||
|
||||
struct ShouldCompress(LitBool);
|
||||
|
||||
impl Parse for ShouldCompress {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let lit = input.parse()?;
|
||||
Ok(ShouldCompress(lit))
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_static_routes(
|
||||
assets_dir: &ValidAssetsDirTypes,
|
||||
ignore_dirs: &IgnoreDirs,
|
||||
should_compress: &LitBool,
|
||||
) -> Result<TokenStream, error::Error> {
|
||||
let assets_dir_abs = Path::new(&assets_dir.to_string())
|
||||
.canonicalize()
|
||||
.map_err(Error::CannotCanonicalizeDirectory)?;
|
||||
let assets_dir_abs_str = assets_dir_abs
|
||||
.to_str()
|
||||
.ok_or(Error::InvalidUnicodeInDirectoryName)?;
|
||||
let canon_ignore_dirs = ignore_dirs
|
||||
.0
|
||||
.iter()
|
||||
.map(|d| d.canonicalize().map_err(Error::CannotCanonicalizeIgnoreDir))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
let mut routes = Vec::new();
|
||||
for entry in glob(&format!("{assets_dir_abs_str}/**/*")).map_err(Error::Pattern)? {
|
||||
let entry = entry.map_err(Error::Glob)?;
|
||||
let metadata = entry.metadata().map_err(Error::CannotGetMetadata)?;
|
||||
if metadata.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip `entry`s which are located in ignored subdirectories
|
||||
if canon_ignore_dirs
|
||||
.iter()
|
||||
.any(|ignore_dir| entry.starts_with(ignore_dir))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let contents = fs::read(&entry).map_err(Error::CannotReadEntryContents)?;
|
||||
|
||||
// Optionally compress files
|
||||
let (maybe_gzip, maybe_zstd) = if should_compress.value {
|
||||
let gzip = gzip_compress(&contents)?;
|
||||
let zstd = zstd_compress(&contents)?;
|
||||
(gzip, zstd)
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
// Create parameters for `::static_serve::static_route()`
|
||||
let entry_path = entry
|
||||
.to_str()
|
||||
.ok_or(Error::InvalidUnicodeInEntryName)?
|
||||
.strip_prefix(assets_dir_abs_str)
|
||||
.unwrap_or_default();
|
||||
let content_type = file_content_type(&entry)?;
|
||||
let etag_str = etag(&contents);
|
||||
let lit_byte_str_contents = LitByteStr::new(&contents, Span::call_site());
|
||||
let maybe_gzip = option_to_token_stream_option(maybe_gzip.as_ref());
|
||||
let maybe_zstd = option_to_token_stream_option(maybe_zstd.as_ref());
|
||||
|
||||
routes.push(quote! {
|
||||
router = ::static_serve::static_route(
|
||||
router,
|
||||
#entry_path,
|
||||
#content_type,
|
||||
#etag_str,
|
||||
#lit_byte_str_contents,
|
||||
#maybe_gzip,
|
||||
#maybe_zstd,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Ok(quote! {
|
||||
pub fn static_router<S>() -> ::axum::Router<S>
|
||||
where S: ::std::clone::Clone + ::std::marker::Send + ::std::marker::Sync + 'static {
|
||||
let mut router = ::axum::Router::<S>::new();
|
||||
#(#routes)*
|
||||
router
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn gzip_compress(contents: &[u8]) -> Result<Option<LitByteStr>, Error> {
|
||||
let mut compressor = GzEncoder::new(Vec::new(), flate2::Compression::best());
|
||||
compressor
|
||||
.write_all(contents)
|
||||
.map_err(|e| Error::Gzip(GzipType::CompressorWrite(e)))?;
|
||||
let compressed = compressor
|
||||
.finish()
|
||||
.map_err(|e| Error::Gzip(GzipType::EncoderFinish(e)))?;
|
||||
|
||||
Ok(maybe_get_compressed(&compressed, contents))
|
||||
}
|
||||
|
||||
fn zstd_compress(contents: &[u8]) -> Result<Option<LitByteStr>, Error> {
|
||||
let level = *zstd::compression_level_range().end();
|
||||
let mut encoder = zstd::Encoder::new(Vec::new(), level).unwrap();
|
||||
write_to_zstd_encoder(&mut encoder, contents)
|
||||
.map_err(|e| Error::Zstd(ZstdType::EncoderWrite(e)))?;
|
||||
|
||||
let compressed = encoder
|
||||
.finish()
|
||||
.map_err(|e| Error::Zstd(ZstdType::EncoderFinish(e)))?;
|
||||
|
||||
Ok(maybe_get_compressed(&compressed, contents))
|
||||
}
|
||||
|
||||
fn write_to_zstd_encoder(
|
||||
encoder: &mut zstd::Encoder<'static, Vec<u8>>,
|
||||
contents: &[u8],
|
||||
) -> io::Result<()> {
|
||||
encoder.set_pledged_src_size(Some(
|
||||
contents
|
||||
.len()
|
||||
.try_into()
|
||||
.expect("contents size should fit into u64"),
|
||||
))?;
|
||||
encoder.window_log(23)?;
|
||||
encoder.include_checksum(false)?;
|
||||
encoder.include_contentsize(false)?;
|
||||
encoder.long_distance_matching(false)?;
|
||||
encoder.write_all(contents)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn option_to_token_stream_option<T: ToTokens>(opt: Option<&T>) -> TokenStream {
|
||||
if let Some(inner) = opt {
|
||||
quote! { ::std::option::Option::Some(#inner) }
|
||||
} else {
|
||||
quote! { ::std::option::Option::None }
|
||||
}
|
||||
}
|
||||
|
||||
fn is_compression_significant(compressed_len: usize, contents_len: usize) -> bool {
|
||||
let ninety_pct_original = contents_len / 10 * 9;
|
||||
compressed_len < ninety_pct_original
|
||||
}
|
||||
|
||||
fn maybe_get_compressed(compressed: &[u8], contents: &[u8]) -> Option<LitByteStr> {
|
||||
is_compression_significant(compressed.len(), contents.len())
|
||||
.then(|| LitByteStr::new(compressed, Span::call_site()))
|
||||
}
|
||||
|
||||
fn file_content_type(path: &Path) -> Result<&'static str, error::Error> {
|
||||
match path.extension() {
|
||||
Some(ext) if ext.eq_ignore_ascii_case("css") => Ok("text/css"),
|
||||
Some(ext) if ext.eq_ignore_ascii_case("js") => Ok("text/javascript"),
|
||||
Some(ext) if ext.eq_ignore_ascii_case("txt") => Ok("text/plain"),
|
||||
Some(ext) if ext.eq_ignore_ascii_case("woff") => Ok("font/woff"),
|
||||
Some(ext) if ext.eq_ignore_ascii_case("woff2") => Ok("font/woff2"),
|
||||
Some(ext) if ext.eq_ignore_ascii_case("svg") => Ok("image/svg+xml"),
|
||||
ext => Err(error::Error::UnknownFileExtension(ext.map(Into::into))),
|
||||
}
|
||||
}
|
||||
|
||||
fn etag(contents: &[u8]) -> String {
|
||||
let sha256 = Sha1::digest(contents);
|
||||
let hash = u64::from_le_bytes(sha256[..8].try_into().unwrap())
|
||||
^ u64::from_le_bytes(sha256[8..16].try_into().unwrap());
|
||||
format!("\"{hash:016x}\"")
|
||||
}
|
18
static-serve/Cargo.toml
Normal file
18
static-serve/Cargo.toml
Normal file
@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "static-serve"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
keywords.workspace = true
|
||||
description.workspace = true
|
||||
categories.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
static-serve-macro = { path = "../static-serve-macro", version = "0.1.0" }
|
||||
axum = { version = "0.8", default-features = false }
|
||||
bytes = "1.10"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
192
static-serve/src/lib.rs
Normal file
192
static-serve/src/lib.rs
Normal file
@ -0,0 +1,192 @@
|
||||
//! Crate for compressing and embedding static assets
|
||||
//! in a web server
|
||||
use std::convert::Infallible;
|
||||
|
||||
use axum::{
|
||||
extract::FromRequestParts,
|
||||
http::{
|
||||
header::{
|
||||
HeaderValue, ACCEPT_ENCODING, CACHE_CONTROL, CONTENT_ENCODING, CONTENT_TYPE, ETAG,
|
||||
IF_NONE_MATCH, VARY,
|
||||
},
|
||||
request::Parts,
|
||||
StatusCode,
|
||||
},
|
||||
response::IntoResponse,
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
|
||||
pub use static_serve_macro::embed_assets;
|
||||
|
||||
/// The accept/reject status for gzip and zstd encoding
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
struct AcceptEncoding {
|
||||
/// Is gzip accepted?
|
||||
pub gzip: bool,
|
||||
/// Is zstd accepted?
|
||||
pub zstd: bool,
|
||||
}
|
||||
|
||||
impl<S> FromRequestParts<S> for AcceptEncoding
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = Infallible;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||
let accept_encoding = parts.headers.get(ACCEPT_ENCODING);
|
||||
let accept_encoding = accept_encoding
|
||||
.and_then(|accept_encoding| accept_encoding.to_str().ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(Self {
|
||||
gzip: accept_encoding.contains("gzip"),
|
||||
zstd: accept_encoding.contains("zstd"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the `IfNoneMatch` header is present
|
||||
#[derive(Debug)]
|
||||
struct IfNoneMatch(Option<HeaderValue>);
|
||||
|
||||
impl IfNoneMatch {
|
||||
/// required function for checking if `IfNoneMatch` is present
|
||||
fn matches(&self, etag: &str) -> bool {
|
||||
self.0
|
||||
.as_ref()
|
||||
.is_some_and(|if_none_match| if_none_match.as_bytes() == etag.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> FromRequestParts<S> for IfNoneMatch
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = Infallible;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||
let if_none_match = parts.headers.get(IF_NONE_MATCH).cloned();
|
||||
Ok(Self(if_none_match))
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
/// The router for adding routes for static assets
|
||||
pub fn static_route<S>(
|
||||
router: Router<S>,
|
||||
web_path: &'static str,
|
||||
content_type: &'static str,
|
||||
etag: &'static str,
|
||||
body: &'static [u8],
|
||||
body_gz: Option<&'static [u8]>,
|
||||
body_zst: Option<&'static [u8]>,
|
||||
) -> Router<S>
|
||||
where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
router.route(
|
||||
web_path,
|
||||
get(
|
||||
move |accept_encoding: AcceptEncoding, if_none_match: IfNoneMatch| async move {
|
||||
let headers_base = [
|
||||
(CONTENT_TYPE, HeaderValue::from_static(content_type)),
|
||||
(ETAG, HeaderValue::from_static(etag)),
|
||||
(
|
||||
CACHE_CONTROL,
|
||||
HeaderValue::from_static("public, max-age=31536000, immutable"),
|
||||
),
|
||||
(VARY, HeaderValue::from_static("Accept-Encoding")),
|
||||
];
|
||||
|
||||
match (
|
||||
if_none_match.matches(etag),
|
||||
accept_encoding.gzip,
|
||||
accept_encoding.zstd,
|
||||
body_gz,
|
||||
body_zst,
|
||||
) {
|
||||
(true, _, _, _, _) => (headers_base, StatusCode::NOT_MODIFIED).into_response(),
|
||||
(false, _, true, _, Some(body_zst)) => (
|
||||
headers_base,
|
||||
[(CONTENT_ENCODING, HeaderValue::from_static("zstd"))],
|
||||
Bytes::from_static(body_zst),
|
||||
)
|
||||
.into_response(),
|
||||
(false, true, _, Some(body_gz), _) => (
|
||||
headers_base,
|
||||
[(CONTENT_ENCODING, HeaderValue::from_static("gzip"))],
|
||||
Bytes::from_static(body_gz),
|
||||
)
|
||||
.into_response(),
|
||||
_ => (headers_base, Bytes::from_static(body)).into_response(),
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
#[test]
|
||||
fn router_created_with_test_routes_lit_str() {
|
||||
embed_assets!("test_assets/small", compress = false);
|
||||
let router: Router<()> = static_router();
|
||||
assert!(router.has_routes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn router_created_not_compressed_because_not_worthwhile() {
|
||||
embed_assets!("test_assets/small", compress = true);
|
||||
let router: Router<()> = static_router();
|
||||
assert!(router.has_routes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn router_created_compressed() {
|
||||
embed_assets!("test_assets/big", compress = true);
|
||||
let router: Router<()> = static_router();
|
||||
assert!(router.has_routes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn router_created_with_test_routes_ident() {
|
||||
embed_assets!(test_assets, compress = true);
|
||||
let router: Router<()> = static_router();
|
||||
assert!(router.has_routes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn router_created_ignore_dirs_one() {
|
||||
embed_assets!(test_assets, ignore_dirs = ["test_assets/big"]);
|
||||
let router: Router<()> = static_router();
|
||||
assert!(router.has_routes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn router_created_ignore_dirs_two() {
|
||||
embed_assets!(
|
||||
test_assets,
|
||||
ignore_dirs = ["test_assets/big", "test_assets/small"]
|
||||
);
|
||||
let router: Router<()> = static_router();
|
||||
// all directories ignored, so router has no routes
|
||||
assert!(!router.has_routes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn router_created_ignore_dirs_with_defaults() {
|
||||
// TODO: actually create one of the default ignore directories
|
||||
// in `test_assets` to make sure this works
|
||||
embed_assets!(
|
||||
test_assets,
|
||||
ignore_dirs = ["test_assets/big"],
|
||||
use_default_ignore_dirs = true
|
||||
);
|
||||
let router: Router<()> = static_router();
|
||||
assert!(router.has_routes());
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user