From cb82987c33ce322e1eaaf7c79a0f543107e71284 Mon Sep 17 00:00:00 2001 From: Brummel Date: Sun, 28 Jun 2026 16:43:20 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20walking=20skeleton=20=E2=80=94=20live-r?= =?UTF-8?q?ender=20aura=20+=20data-server=20docs=20from=20Gitea?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first runnable DocSite server (plan docs/plans/0001-walking-skeleton.md): `docsite serve` renders the published docs of aura and data-server live from their Gitea main revisions, dark-themed, navigated from the DocSite-owned whitelist registry; `docsite check` validates every nav reference against the live sources with an exit code. Built across nine TDD tasks — config (registry whitelist + traversal guard), gitea read-only client (Fetcher trait, injected for offline tests), render (comrak GFM + syntect), theme (embedded dark CSS + shell), revision-keyed cache, reference validator, axum server, main wiring. All five domain invariants land: read-only Gitea access via the Fetcher trait; single source of truth via the (repo,path,sha) cache; reference integrity detected (check + a fail-loud render-time error block) never enforced; whitelist publishing with path-traversal refusal; the closed directive vocabulary's no-op expansion seam reserved in render. Verification (run by the orchestrator): cargo build green; cargo test green — 40 tests (32 lib unit + 7 e2e via an injected MockFetcher + 1 gated live smoke that reached the real Gitea this run); cargo clippy --all-targets -D warnings clean. Held quality findings (plan-prescribed / plan-deferred, not defects; tracked as follow-up issues): nav_html interpolates the registry slug/page into the href unescaped (operator-trusted registry — latent, not a live vector); GiteaClient's 404->NotFound status mapping (the invariant-3 broken-reference signal) is covered only by the gated live smoke's happy path, not a unit test. Resolved API deviations from the plan text (newer crate versions): reqwest 0.13 renamed feature rustls-tls -> rustls; comrak 0.52 deprecated `Plugins` (used `comrak::options::Plugins`); axum 0.8 capture syntax `/{slug}/{*page}`. refs #1 --- Cargo.lock | 2430 ++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 12 +- assets/theme.css | 21 + src/cache.rs | 57 + src/check.rs | 252 +++++ src/config/mod.rs | 209 ++++ src/config/registry.rs | 37 + src/gitea/client.rs | 98 ++ src/gitea/mod.rs | 75 ++ src/lib.rs | 13 + src/main.rs | 14 +- src/render/mod.rs | 48 + src/server/mod.rs | 115 ++ src/theme/mod.rs | 87 ++ tests/e2e.rs | 193 ++++ tests/live_smoke.rs | 16 + 16 files changed, 3673 insertions(+), 4 deletions(-) create mode 100644 assets/theme.css create mode 100644 src/cache.rs create mode 100644 src/check.rs create mode 100644 src/config/mod.rs create mode 100644 src/config/registry.rs create mode 100644 src/gitea/client.rs create mode 100644 src/gitea/mod.rs create mode 100644 src/lib.rs create mode 100644 src/render/mod.rs create mode 100644 src/server/mod.rs create mode 100644 src/theme/mod.rs create mode 100644 tests/e2e.rs create mode 100644 tests/live_smoke.rs diff --git a/Cargo.lock b/Cargo.lock index 569bdd1..a0fed1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,2436 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "aws-lc-rs" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "bon" +version = "3.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a602c73c7b0148ec6d12af6fd5cc7a46e2eacc8878271a999abac56eed12f561" +dependencies = [ + "bon-macros", + "rustversion", +] + +[[package]] +name = "bon-macros" +version = "3.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dee98b0db6a962de883bf5d20362dee4d7ca0d12fe39a7c6c73c844e1cd7c1f" +dependencies = [ + "darling", + "ident_case", + "prettyplease", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" + +[[package]] +name = "caseless" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6fd507454086c8edfd769ca6ada439193cdb209c7681712ef6275cccbfe5d8" +dependencies = [ + "unicode-normalization", +] + +[[package]] +name = "cc" +version = "1.2.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", + "terminal_size", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "comrak" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac0b255932a9cd52fbfd664b67957f9f2e095ae4711cb0e41b4e291edef94c2" +dependencies = [ + "bon", + "caseless", + "clap", + "emojis", + "entities", + "finl_unicode", + "fmt2io", + "jetscii", + "phf", + "phf_codegen", + "rustc-hash", + "shell-words", + "smallvec", + "syntect", + "typed-arena", + "xdg", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "docsite" version = "0.1.0" +dependencies = [ + "axum", + "comrak", + "reqwest", + "serde", + "syntect", + "tokio", + "toml", + "tower", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "emojis" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a4d5d50b0b58df5173d8ff1192b4d1422ceae5d981b30d4b6f8ed1d673a2bc4" +dependencies = [ + "phf", +] + +[[package]] +name = "entities" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fancy-regex" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "finl_unicode" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fmt2io" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b6129284da9f7e5296cc22183a63f24300e945e297705dcc0672f7df01d62c8" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jetscii" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47f142fe24a9c9944451e8349de0a56af5f3e7226dc46f3ed4d4ecc0b85af75e" + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "onig" +version = "6.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc3cbf698f9438986c11a880c90a6d04b9de27575afd28bbf45b154b6c709e2" +dependencies = [ + "bitflags", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e68317604e77e53b85896388e1a803c1d21b74c899ec9e5e1112db90735edd7" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plist" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" +dependencies = [ + "base64", + "indexmap", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.39.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[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.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "shell-words" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +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" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syntect" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925" +dependencies = [ + "bincode", + "fancy-regex", + "flate2", + "fnv", + "once_cell", + "onig", + "plist", + "regex-syntax", + "serde", + "serde_derive", + "serde_json", + "thiserror", + "walkdir", + "yaml-rust", +] + +[[package]] +name = "terminal_size" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" +dependencies = [ + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[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_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[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_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "xdg" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fb433233f2df9344722454bc7e96465c9d03bff9d77c248f9e7523fe79585b5" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 725bf4c..26af5a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,5 +5,13 @@ edition = "2021" description = "Single-source-of-truth LAN documentation server" [dependencies] -# Dependencies are added as the walking-skeleton implementation lands -# (HTTP stack, comrak, syntect, toml/serde) via the docs/specs/0001 cycle. +axum = "0.8.9" +comrak = "0.52.0" +reqwest = { version = "0.13.4", default-features = false, features = ["blocking", "rustls"] } +serde = { version = "1.0.228", features = ["derive"] } +syntect = "5.3.0" +tokio = { version = "1.52.3", features = ["rt-multi-thread", "macros", "net"] } +toml = "1.1.2" + +[dev-dependencies] +tower = { version = "0.5.3", features = ["util"] } diff --git a/assets/theme.css b/assets/theme.css new file mode 100644 index 0000000..8208aa0 --- /dev/null +++ b/assets/theme.css @@ -0,0 +1,21 @@ +:root { + --bg: #16161a; --fg: #cdd6f4; --dim: #6c7086; --link: #89b4fa; + --panel: #1e1e2e; --border: #313244; --accent: #f5e0dc; +} +html, body { margin: 0; background: var(--bg); color: var(--fg); + font-family: system-ui, sans-serif; line-height: 1.6; } +header.crumb { padding: 10px 16px; border-bottom: 1px solid var(--border); + font-size: 13px; color: var(--dim); } +header.crumb a { color: var(--link); text-decoration: none; } +nav { padding: 10px 16px; border-bottom: 1px solid var(--border); font-size: 14px; } +nav .sec { color: var(--dim); margin: 8px 0 2px; text-transform: uppercase; font-size: 12px; } +nav a { display: block; color: var(--link); text-decoration: none; padding: 1px 0; } +main.doc { max-width: 860px; margin: 0 auto; padding: 24px 16px 64px; } +main.doc code, main.doc pre { font-family: ui-monospace, monospace; } +main.doc pre { background: var(--panel); border: 1px solid var(--border); + border-radius: 6px; padding: 12px; overflow-x: auto; } +main.doc table { border-collapse: collapse; margin: 12px 0; } +main.doc th, main.doc td { border: 1px solid var(--border); padding: 6px 10px; } +main.doc a { color: var(--link); } +.docsite-error { background: #3a1e26; border: 1px solid #f38ba8; color: #f38ba8; + border-radius: 6px; padding: 12px; margin: 12px 0; font-family: ui-monospace, monospace; } diff --git a/src/cache.rs b/src/cache.rs new file mode 100644 index 0000000..3f9a4a5 --- /dev/null +++ b/src/cache.rs @@ -0,0 +1,57 @@ +//! Revision-keyed render cache: (repo, path, sha) -> rendered HTML. A new +//! sha is a new key, so a changed source re-renders; an unchanged sha serves +//! byte-identical output (invariant 2). Thread-safe for the axum handlers. + +use std::collections::HashMap; +use std::sync::Mutex; + +#[derive(Default)] +pub struct Cache { + map: Mutex>, +} + +impl Cache { + pub fn new() -> Self { + Self::default() + } + + /// Return the cached render, or compute it with `f`, store, and return it. + pub fn get_or_render String>( + &self, repo: &str, path: &str, sha: &str, f: F, + ) -> String { + let key = (repo.to_string(), path.to_string(), sha.to_string()); + if let Some(hit) = self.map.lock().unwrap().get(&key) { + return hit.clone(); + } + let html = f(); + self.map.lock().unwrap().insert(key, html.clone()); + html + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::cell::Cell; + + #[test] + fn same_sha_renders_once_and_is_byte_identical() { + let cache = Cache::new(); + let calls = Cell::new(0); + let render = || { calls.set(calls.get() + 1); "

x

".to_string() }; + let a = cache.get_or_render("r", "p", "sha1", render); + let b = cache.get_or_render("r", "p", "sha1", render); + assert_eq!(a, b, "same sha must be byte-identical"); + assert_eq!(calls.get(), 1, "same sha must render exactly once"); + } + + #[test] + fn changed_sha_re_renders() { + let cache = Cache::new(); + let calls = Cell::new(0); + let render = || { calls.set(calls.get() + 1); "x".to_string() }; + cache.get_or_render("r", "p", "sha1", render); + cache.get_or_render("r", "p", "sha2", render); + assert_eq!(calls.get(), 2, "a new sha must re-render"); + } +} diff --git a/src/check.rs b/src/check.rs new file mode 100644 index 0000000..6709ec2 --- /dev/null +++ b/src/check.rs @@ -0,0 +1,252 @@ +//! Reference-integrity validator (invariant 3): for every nav page in the +//! registry, confirm the source exists at the project's branch. Drives +//! `docsite check` (exit-coded) and the boot-time validation pass. Detects +//! and reports; never writes to a source. + +use crate::config::Registry; +use crate::gitea::{Fetcher, GiteaError}; +use std::process::ExitCode; + +/// Width of the `slug/path` column in the `docsite check` report. The whole +/// `slug/path` entry is left-aligned (right-padded) to this width so the detail +/// column lines up even across slugs of differing length. +const PATH_COL: usize = 28; + +pub struct RefResult { + pub slug: String, + pub path: String, + pub ok: bool, + pub detail: String, +} + +pub struct CheckReport { + pub results: Vec, +} + +impl CheckReport { + pub fn broken(&self) -> usize { + self.results.iter().filter(|r| !r.ok).count() + } +} + +/// Validate every nav reference in `reg` against `fetcher`. +pub fn check_all(reg: &Registry, fetcher: &dyn Fetcher) -> CheckReport { + let mut results = Vec::new(); + for proj in ®.project { + let sha = fetcher.branch_sha(&proj.repo, &proj.branch); + for page in proj.section.iter().flat_map(|s| &s.pages) { + let (ok, detail) = match &sha { + Err(GiteaError::NotFound { .. }) => { + (false, format!("branch error: {} {} not found", proj.repo, proj.branch)) + } + Err(GiteaError::Transport(t)) => { + (false, format!("branch error: {} {} unreachable ({t})", proj.repo, proj.branch)) + } + Ok(sha) => { + // the source path is needed only once the branch resolves + let full = proj.source_path(page); + match fetcher.fetch_raw(&proj.repo, &proj.branch, &full) { + Ok(_) => (true, format!("ok ({} @ {})", proj.branch, &sha[..sha.len().min(7)])), + Err(GiteaError::NotFound { .. }) => { + (false, format!("BROKEN — not found at {} {}:{}", proj.repo, proj.branch, full)) + } + Err(GiteaError::Transport(t)) => (false, format!("transport error: {t}")), + } + } + }; + results.push(RefResult { slug: proj.slug.clone(), path: page.clone(), ok, detail }); + } + } + CheckReport { results } +} + +/// `docsite check`: load the registry, validate against live Gitea, print a +/// report, exit 0 iff nothing is broken. +pub fn run_cli(registry_path: &str) -> ExitCode { + ExitCode::from(run_cli_code(registry_path)) +} + +/// The exit-coded body of `docsite check` (invariant 3, exit-coded half), +/// returning the process exit code as a `u8` so the 2/1/0 mapping is testable +/// (`ExitCode` is opaque and not comparable): 2 = registry could not load, +/// 1 = at least one broken reference, 0 = every reference ok. The config-error +/// arm returns before a fetcher is built, keeping that path offline. +fn run_cli_code(registry_path: &str) -> u8 { + let reg = match crate::config::load(registry_path) { + Ok(r) => r, + Err(e) => { eprintln!("docsite: {e}"); return 2; } + }; + let fetcher = crate::gitea::GiteaClient::new(®.gitea); + check_and_report(®, &fetcher) +} + +/// Validate every reference in `reg` against `fetcher`, print the report, and +/// return the broken/ok exit code (1 if any reference is broken, else 0). The +/// injected `fetcher` is the seam that lets the exit mapping be driven offline. +fn check_and_report(reg: &Registry, fetcher: &dyn Fetcher) -> u8 { + let report = check_all(reg, fetcher); + for r in &report.results { + let entry = format!("{}/{}", r.slug, r.path); + println!("{entry: Registry { + toml::from_str(&format!( + r#" +bind="x" +gitea="y" +[[project]] +slug="aura" +repo="Brummel/aura" +branch="{branch}" +docs="{docs}" +[[project.section]] +title="Design" +pages=["design/INDEX.md","glossary.md"] +"#, + )).unwrap() + } + + fn reg() -> Registry { + reg_for("main", "docs") + } + + /// A `Fetcher` whose branch lookup and raw fetch each return a fixed + /// `Result`. Reaches the transport / branch-error arms `MockFetcher` + /// (which only yields Ok/NotFound) cannot exercise. + struct FailFetcher { + branch: Result, + fetch: Result, + } + + impl Fetcher for FailFetcher { + fn branch_sha(&self, _repo: &str, _branch: &str) -> Result { + self.branch.clone() + } + fn fetch_raw(&self, _repo: &str, _branch: &str, _path: &str) -> Result { + self.fetch.clone() + } + } + + #[test] + fn all_ok_reports_zero_broken() { + let f = MockFetcher::new("a517899") + .with("Brummel/aura", "docs/design/INDEX.md", "x") + .with("Brummel/aura", "docs/glossary.md", "y"); + assert_eq!(check_all(®(), &f).broken(), 0); + } + + #[test] + fn missing_source_is_broken() { + // only one of the two pages exists -> one broken + let f = MockFetcher::new("a517899").with("Brummel/aura", "docs/design/INDEX.md", "x"); + let report = check_all(®(), &f); + assert_eq!(report.broken(), 1, "the missing glossary must be broken"); + assert!(report.results.iter().any(|r| !r.ok && r.detail.contains("BROKEN"))); + } + + /// Invariant 3: an unreachable Gitea is reported as broken but kept + /// distinct from a missing/renamed source — a transport failure must NOT + /// masquerade as a "BROKEN — not found" reference. + #[test] + fn transport_failure_is_broken_but_not_reported_as_missing() { + let f = FailFetcher { + branch: Ok("a517899".into()), + fetch: Err(GiteaError::Transport("connection refused".into())), + }; + let report = check_all(®(), &f); + assert_eq!(report.broken(), 2, "both pages broken when Gitea is unreachable"); + assert!( + report.results.iter().all(|r| !r.ok && r.detail.contains("transport")), + "transport failures must be labelled as transport errors" + ); + assert!( + report.results.iter().all(|r| !r.detail.contains("BROKEN")), + "an unreachable Gitea must not be reported as a missing/renamed source" + ); + } + + /// A branch lookup failure marks every page of that project broken and is + /// surfaced as a branch error, not a per-page not-found. + #[test] + fn branch_lookup_failure_marks_all_pages_broken() { + let f = FailFetcher { + branch: Err(GiteaError::Transport("connection refused".into())), + fetch: Ok("ignored".into()), + }; + let report = check_all(®(), &f); + assert_eq!(report.broken(), 2, "all pages broken when the branch can't be resolved"); + assert!( + report.results.iter().all(|r| !r.ok && r.detail.contains("branch error")), + "a failed branch lookup must be labelled as a branch error" + ); + } + + /// `docs = "."` (repo-root docs) builds the bare page path with no docs + /// prefix — the source must be looked up at exactly the page path. + #[test] + fn repo_root_docs_uses_bare_page_path() { + let f = MockFetcher::new("a517899") + .with("Brummel/aura", "design/INDEX.md", "x") + .with("Brummel/aura", "glossary.md", "y"); + assert_eq!(check_all(®_for("main", "."), &f).broken(), 0); + } + + /// Invariant 3 (exit-coded half): `docsite check` returns exit 0 when every + /// reference resolves — the success contract an operator / CI gate relies on. + #[test] + fn check_exit_zero_when_all_references_ok() { + let f = MockFetcher::new("a517899") + .with("Brummel/aura", "docs/design/INDEX.md", "x") + .with("Brummel/aura", "docs/glossary.md", "y"); + assert_eq!(check_and_report(®(), &f), 0); + } + + /// Invariant 3 (exit-coded half): a single broken reference makes + /// `docsite check` return exit 1 — the broken-reference gate, kept distinct + /// from the all-ok 0. + #[test] + fn check_exit_one_when_a_reference_is_broken() { + let f = MockFetcher::new("a517899").with("Brummel/aura", "docs/design/INDEX.md", "x"); + assert_eq!(check_and_report(®(), &f), 1); + } + + /// Invariant 3 (exit-coded half): an unloadable registry makes `docsite + /// check` return exit 2 (config error), distinct from a broken-reference 1 + /// — so an operator can tell a bad config from a broken source. The arm + /// returns before any fetcher is built, so this stays offline. + #[test] + fn check_exit_two_when_config_cannot_load() { + let absent = std::env::temp_dir() + .join(format!("docsite-check-absent-{}.toml", std::process::id())); + let _ = std::fs::remove_file(&absent); + assert_eq!(run_cli_code(absent.to_str().expect("temp path is utf8")), 2); + } + + /// The ok-detail names the project's actual branch, not a hardcoded + /// "main", so a project tracking another branch is reported correctly. + #[test] + fn ok_detail_names_the_projects_actual_branch() { + let f = MockFetcher::new("a517899") + .with("Brummel/aura", "docs/design/INDEX.md", "x") + .with("Brummel/aura", "docs/glossary.md", "y"); + let report = check_all(®_for("dev", "docs"), &f); + assert!( + report.results.iter().all(|r| r.ok && r.detail.contains("dev")), + "ok detail must name the project's actual branch" + ); + assert!( + report.results.iter().all(|r| !r.detail.contains("main")), + "ok detail must not hardcode 'main'" + ); + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..5daed87 --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,209 @@ +pub mod registry; +pub use registry::{PageRef, Project, Registry, Section}; + +use std::path::Path; + +/// Parse a `registry.toml` from disk. +pub fn load(path: impl AsRef) -> Result { + let path = path.as_ref(); + let text = std::fs::read_to_string(path) + .map_err(|e| format!("registry {}: {e}", path.display()))?; + toml::from_str(&text).map_err(|e| format!("registry {} parse: {e}", path.display())) +} + +impl Registry { + pub fn project(&self, slug: &str) -> Option<&Project> { + self.project.iter().find(|p| p.slug == slug) + } + + /// Resolve a served url path (`/`) to a `PageRef`, + /// IFF that page is listed in some section of the project (whitelist). + /// `url_page` is the path after the slug, without a trailing `.md` + /// (e.g. `design/INDEX`). A `..` component is refused (traversal). + pub fn resolve(&self, slug: &str, url_page: &str) -> Option { + if url_page.split('/').any(|c| c == ".." || c.is_empty()) { + return None; + } + let proj = self.project(slug)?; + let want = format!("{url_page}.md"); + let listed = proj + .section + .iter() + .flat_map(|s| &s.pages) + .any(|p| p == &want); + listed.then(|| PageRef { slug: slug.to_string(), rel: want }) + } +} + +impl Project { + /// Join this project's `docs` root with a `page` path (relative to that + /// root) into the source path fetched from Gitea. `docs = "."` (repo-root + /// docs) yields the bare page path; otherwise the docs root, with a trailing + /// slash trimmed, is prefixed. This is the single source for the join so + /// `docsite check` validates exactly the path the server fetches + /// (invariant 3). + pub fn source_path(&self, page: &str) -> String { + if self.docs == "." { + page.to_string() + } else { + format!("{}/{page}", self.docs.trim_end_matches('/')) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn sample() -> Registry { + toml::from_str( + r#" +bind = "0.0.0.0:8080" +gitea = "http://gitea.test" +[[project]] +slug = "aura" +repo = "Brummel/aura" +branch = "main" +docs = "docs" +[[project.section]] +title = "Design" +pages = ["design/INDEX.md"] +"#, + ) + .expect("sample registry parses") + } + + #[test] + fn resolves_a_listed_page() { + let r = sample(); + assert_eq!( + r.resolve("aura", "design/INDEX"), + Some(PageRef { slug: "aura".into(), rel: "design/INDEX.md".into() }) + ); + } + + #[test] + fn refuses_an_unlisted_page() { + // glossary is not in any section -> not reachable (whitelist) + assert_eq!(sample().resolve("aura", "glossary"), None); + } + + #[test] + fn refuses_traversal() { + assert_eq!(sample().resolve("aura", "../secret"), None); + } + + #[test] + fn refuses_unknown_slug() { + assert_eq!(sample().resolve("nope", "design/INDEX"), None); + } + + /// `source_path` is the single source for the docs-root↔source-path join + /// that decides exactly what gets fetched (invariant 3: `docsite check` and + /// the server must agree on the path). `docs = "docs"` prefixes the root; + /// `docs = "."` yields the bare page path; a trailing slash on the root is + /// trimmed so no double slash leaks into the fetched path. + #[test] + fn source_path_joins_docs_root_to_page() { + fn proj(docs: &str) -> Project { + Project { + slug: "aura".into(), + repo: "Brummel/aura".into(), + branch: "main".into(), + docs: docs.into(), + section: vec![], + } + } + assert_eq!(proj("docs").source_path("design/INDEX.md"), "docs/design/INDEX.md"); + assert_eq!(proj(".").source_path("design/INDEX.md"), "design/INDEX.md"); + assert_eq!(proj("docs/").source_path("design/INDEX.md"), "docs/design/INDEX.md"); + } + + #[test] + fn tolerates_unknown_keys() { + // a future key must not break parsing (forward-compatible) + let r: Result = + toml::from_str("bind=\"x\"\ngitea=\"y\"\nfuture_key=\"z\"\n"); + assert!(r.is_ok(), "unknown top-level key must be tolerated"); + } + + /// A unique temp file path for a single test, removed when dropped, so the + /// `load` disk tests don't collide or leave residue. + struct TempFile(std::path::PathBuf); + + impl TempFile { + fn with(tag: &str, contents: &str) -> Self { + let path = std::env::temp_dir().join(format!( + "docsite-registry-{}-{}.toml", + std::process::id(), + tag + )); + std::fs::write(&path, contents).expect("write temp registry"); + TempFile(path) + } + } + + impl Drop for TempFile { + fn drop(&mut self) { + let _ = std::fs::remove_file(&self.0); + } + } + + /// `load` reads the registry off disk and parses it, so a well-formed file + /// on disk round-trips to a usable `Registry` (not just an in-memory string). + #[test] + fn load_reads_a_file_from_disk() { + let tmp = TempFile::with( + "ok", + r#" +bind = "0.0.0.0:8080" +gitea = "http://gitea.test" +[[project]] +slug = "aura" +repo = "Brummel/aura" +branch = "main" +docs = "docs" +[[project.section]] +title = "Design" +pages = ["design/INDEX.md"] +"#, + ); + let r = load(&tmp.0).expect("well-formed registry loads from disk"); + assert_eq!(r.bind, "0.0.0.0:8080"); + assert_eq!( + r.resolve("aura", "design/INDEX"), + Some(PageRef { slug: "aura".into(), rel: "design/INDEX.md".into() }) + ); + } + + /// `load` surfaces the read failure (not a parse failure) when the file is + /// absent, and names the offending path so the operator knows which file. + #[test] + fn load_reports_a_missing_file_with_its_path() { + let path = std::env::temp_dir().join(format!( + "docsite-registry-{}-absent.toml", + std::process::id() + )); + let _ = std::fs::remove_file(&path); + let err = load(&path).expect_err("a missing file must be an error"); + assert!(err.starts_with("registry "), "read error, got: {err}"); + assert!(!err.contains("parse"), "must be the read arm, got: {err}"); + assert!( + err.contains(&path.display().to_string()), + "error must name the path, got: {err}" + ); + } + + /// `load` surfaces a parse failure (distinct from the read failure) when the + /// file exists but is malformed, and names the offending path. + #[test] + fn load_reports_a_parse_error_with_its_path() { + let tmp = TempFile::with("bad", "bind = \"x\"\nthis is not toml\n"); + let err = load(&tmp.0).expect_err("malformed toml must be an error"); + assert!(err.contains("parse"), "parse error, got: {err}"); + assert!( + err.contains(&tmp.0.display().to_string()), + "error must name the path, got: {err}" + ); + } +} diff --git a/src/config/registry.rs b/src/config/registry.rs new file mode 100644 index 0000000..b299a53 --- /dev/null +++ b/src/config/registry.rs @@ -0,0 +1,37 @@ +//! The DocSite-owned `registry.toml` model: the publish whitelist + nav. +//! NO `deny_unknown_fields` — unknown keys are tolerated so the schema can +//! grow (forward-compatible). + +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct Registry { + pub bind: String, + pub gitea: String, + #[serde(default)] + pub project: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct Project { + pub slug: String, + pub repo: String, + pub branch: String, + pub docs: String, + #[serde(default)] + pub section: Vec
, +} + +#[derive(Debug, Deserialize)] +pub struct Section { + pub title: String, + pub pages: Vec, +} + +/// A nav page resolved to its source location: the project plus the page +/// path relative to the project's `docs` root. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PageRef { + pub slug: String, + pub rel: String, +} diff --git a/src/gitea/client.rs b/src/gitea/client.rs new file mode 100644 index 0000000..00cc4c3 --- /dev/null +++ b/src/gitea/client.rs @@ -0,0 +1,98 @@ +//! Read-only Gitea source client (blocking reqwest). Resolves a branch SHA +//! via the API and fetches raw file bytes via the raw endpoint. Token from +//! `$DOCSITE_GITEA_TOKEN` for private repos. + +use super::{Fetcher, GiteaError}; + +/// Extract the commit SHA from a Gitea branch API body without a JSON dep on +/// the model. The API returns `{"name":...,"commit":{"id":"",...}}`; we +/// pull the first `"id":"..."` value. A missing or unterminated key is a +/// malformed response (`Transport`), never a silent empty SHA. +fn extract_commit_id(body: &str) -> Result { + let key = "\"id\":\""; + let start = body.find(key).ok_or_else(|| GiteaError::Transport("no commit id".into()))? + key.len(); + let end = body[start..].find('"').ok_or_else(|| GiteaError::Transport("unterminated id".into()))?; + Ok(body[start..start + end].to_string()) +} + +pub struct GiteaClient { + base: String, + token: Option, + http: reqwest::blocking::Client, +} + +impl GiteaClient { + pub fn new(base: &str) -> Self { + Self { + base: base.trim_end_matches('/').to_string(), + token: std::env::var("DOCSITE_GITEA_TOKEN").ok(), + http: reqwest::blocking::Client::new(), + } + } + + fn get(&self, url: &str) -> Result { + let mut req = self.http.get(url); + if let Some(t) = &self.token { + req = req.header("Authorization", format!("token {t}")); + } + req.send().map_err(|e| GiteaError::Transport(e.to_string())) + } +} + +impl Fetcher for GiteaClient { + fn branch_sha(&self, repo: &str, branch: &str) -> Result { + // GET {base}/api/v1/repos/{repo}/branches/{branch} -> { "commit": { "id": "" } } + let url = format!("{}/api/v1/repos/{repo}/branches/{branch}", self.base); + let resp = self.get(&url)?; + if resp.status() == reqwest::StatusCode::NOT_FOUND { + return Err(GiteaError::NotFound { repo: repo.into(), path: format!("branch:{branch}") }); + } + if !resp.status().is_success() { + return Err(GiteaError::Transport(format!("status {}", resp.status()))); + } + let body = resp.text().map_err(|e| GiteaError::Transport(e.to_string()))?; + extract_commit_id(&body) + } + + fn fetch_raw(&self, repo: &str, branch: &str, path: &str) -> Result { + // GET {base}/{repo}/raw/branch/{branch}/{path} + let url = format!("{}/{repo}/raw/branch/{branch}/{path}", self.base); + let resp = self.get(&url)?; + if resp.status() == reqwest::StatusCode::NOT_FOUND { + return Err(GiteaError::NotFound { repo: repo.into(), path: path.into() }); + } + if !resp.status().is_success() { + return Err(GiteaError::Transport(format!("status {}", resp.status()))); + } + resp.text().map_err(|e| GiteaError::Transport(e.to_string())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// extract_commit_id pulls the branch HEAD SHA out of a well-formed Gitea + /// branch API body — the parse that branch_sha relies on, isolated from HTTP. + #[test] + fn extract_commit_id_pulls_sha_from_branch_body() { + let body = r#"{"name":"main","commit":{"id":"a517899","message":"x"}}"#; + assert_eq!(extract_commit_id(body).unwrap(), "a517899"); + } + + /// A body with no `"id":"` key is malformed transport, not a NotFound — so + /// branch_sha cannot return a bogus empty SHA when the API shape changes. + #[test] + fn extract_commit_id_missing_key_is_transport() { + let err = extract_commit_id(r#"{"name":"main","commit":{}}"#).unwrap_err(); + assert!(matches!(err, GiteaError::Transport(_)), "missing id must be Transport, got {err:?}"); + } + + /// An unterminated id value (no closing quote) is malformed transport, never + /// a slice that runs off the end of the body. + #[test] + fn extract_commit_id_unterminated_is_transport() { + let err = extract_commit_id("{\"id\":\"a517899").unwrap_err(); + assert!(matches!(err, GiteaError::Transport(_)), "unterminated id must be Transport, got {err:?}"); + } +} diff --git a/src/gitea/mod.rs b/src/gitea/mod.rs new file mode 100644 index 0000000..3ad3582 --- /dev/null +++ b/src/gitea/mod.rs @@ -0,0 +1,75 @@ +pub mod client; +pub use client::GiteaClient; + +use std::collections::HashMap; + +/// A read-only source fetcher. Injected so tests run offline. SYNC by design +/// — the production impl uses blocking reqwest; axum handlers call it inside +/// `spawn_blocking`. +pub trait Fetcher: Send + Sync { + /// The current commit SHA of `branch` in `repo` (the cache key source). + fn branch_sha(&self, repo: &str, branch: &str) -> Result; + /// The raw bytes of `path` at `branch` in `repo`. + fn fetch_raw(&self, repo: &str, branch: &str, path: &str) -> Result; +} + +/// `NotFound` is the broken-reference signal — distinct from a transport +/// failure, so a renamed source and an unreachable Gitea render differently. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum GiteaError { + NotFound { repo: String, path: String }, + Transport(String), +} + +/// In-memory fetcher for tests: `(repo, path)` -> body, plus a branch SHA. +pub struct MockFetcher { + pub sha: String, + pub files: HashMap<(String, String), String>, +} + +impl MockFetcher { + pub fn new(sha: &str) -> Self { + Self { sha: sha.to_string(), files: HashMap::new() } + } + pub fn with(mut self, repo: &str, path: &str, body: &str) -> Self { + self.files.insert((repo.to_string(), path.to_string()), body.to_string()); + self + } +} + +impl Fetcher for MockFetcher { + fn branch_sha(&self, _repo: &str, _branch: &str) -> Result { + Ok(self.sha.clone()) + } + fn fetch_raw(&self, repo: &str, _branch: &str, path: &str) -> Result { + self.files + .get(&(repo.to_string(), path.to_string())) + .cloned() + .ok_or_else(|| GiteaError::NotFound { repo: repo.to_string(), path: path.to_string() }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn mock() -> MockFetcher { + MockFetcher::new("a517899").with("Brummel/aura", "docs/glossary.md", "# Glossary\n") + } + + #[test] + fn fetch_hit_returns_body() { + assert_eq!(mock().fetch_raw("Brummel/aura", "main", "docs/glossary.md").unwrap(), "# Glossary\n"); + } + + #[test] + fn fetch_miss_is_notfound() { + let err = mock().fetch_raw("Brummel/aura", "main", "docs/gone.md").unwrap_err(); + assert!(matches!(err, GiteaError::NotFound { .. }), "miss must be NotFound, got {err:?}"); + } + + #[test] + fn branch_sha_returns_pinned() { + assert_eq!(mock().branch_sha("Brummel/aura", "main").unwrap(), "a517899"); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..6d23ab8 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,13 @@ +//! DocSite library crate — the render/serve/check machinery behind the +//! `docsite` binary. Split out as a lib so the integration tests under +//! `tests/` can drive it directly with an injected `Fetcher`. Module +//! declarations are appended by the tasks that create each module, so the +//! lib compiles after every task. + +pub mod cache; +pub mod check; +pub mod config; +pub mod gitea; +pub mod render; +pub mod server; +pub mod theme; diff --git a/src/main.rs b/src/main.rs index 0e758b4..24a59f0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,16 @@ //! data-server docs, themed, navigated from the whitelist registry) lands //! through the first cycle under `docs/specs/`. -fn main() { - eprintln!("docsite: not yet implemented"); +use std::process::ExitCode; + +fn main() -> ExitCode { + let args: Vec = std::env::args().collect(); + match args.get(1).map(String::as_str) { + Some("serve") => docsite::server::run("registry.toml"), + Some("check") => docsite::check::run_cli("registry.toml"), + _ => { + eprintln!("usage: docsite "); + ExitCode::from(2) + } + } } diff --git a/src/render/mod.rs b/src/render/mod.rs new file mode 100644 index 0000000..b4e3ae7 --- /dev/null +++ b/src/render/mod.rs @@ -0,0 +1,48 @@ +//! Markdown -> HTML render: comrak (GitHub-flavored, tables on) with syntect +//! highlighting of code fences (dark theme). `render_doc` is pure. A no-op +//! directive-expansion seam is reserved here (invariant 4) for `code-include` +//! in a later iteration; `code` and `table` are native Markdown. + +use comrak::options::Plugins; +use comrak::plugins::syntect::SyntectAdapter; +use comrak::{markdown_to_html_with_plugins, Options}; + +/// Render GFM markdown to an HTML fragment (no / wrapper). +pub fn render_doc(markdown: &str) -> String { + let mut options = Options::default(); + options.extension.table = true; + options.extension.strikethrough = true; + options.extension.autolink = true; + + let adapter = SyntectAdapter::new(Some("base16-ocean.dark")); + let mut plugins = Plugins::default(); + plugins.render.codefence_syntax_highlighter = Some(&adapter); + + markdown_to_html_with_plugins(markdown, &options, &plugins) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn renders_a_table() { + let html = render_doc("| a | b |\n|---|---|\n| 1 | 2 |\n"); + assert!(html.contains(""), "GFM table not rendered: {html}"); + assert!(html.contains(""), "table cell missing: {html}"); + } + + #[test] + fn highlights_a_code_fence() { + let html = render_doc("```rust\nfn main() {}\n```\n"); + // syntect emits inline-styled spans inside a
+        assert!(html.contains(" for the fence: {html}");
+        assert!(html.contains("style=\"color"), "syntect did not style the fence: {html}");
+    }
+
+    #[test]
+    fn unknown_language_falls_back_without_panic() {
+        let html = render_doc("```nosuchlang\nplain text\n```\n");
+        assert!(html.contains(": {html}");
+    }
+}
diff --git a/src/server/mod.rs b/src/server/mod.rs
new file mode 100644
index 0000000..c3930de
--- /dev/null
+++ b/src/server/mod.rs
@@ -0,0 +1,115 @@
+//! axum router. Routes: `GET /` (project index), `GET /{slug}/{*page}`
+//! (rendered doc), `GET /assets/theme.css`. The fetcher and registry live in
+//! shared state; the page handler enforces the whitelist (config::resolve)
+//! and renders a visible error block on a NotFound (fail-loud, invariant 3).
+
+use crate::cache::Cache;
+use crate::config::Registry;
+use crate::gitea::{Fetcher, GiteaError};
+use axum::{
+    extract::{Path, State},
+    http::StatusCode,
+    response::{Html, IntoResponse, Response},
+    routing::get,
+    Router,
+};
+use std::sync::Arc;
+
+#[derive(Clone)]
+pub struct AppState {
+    pub reg: Arc,
+    pub fetcher: Arc,
+    pub cache: Arc,
+}
+
+/// Build the router for a given state — the test seam (no network, no bind).
+pub fn router(state: AppState) -> Router {
+    Router::new()
+        .route("/", get(index))
+        .route("/assets/theme.css", get(theme_css))
+        .route("/{slug}/{*page}", get(page))
+        .with_state(state)
+}
+
+async fn theme_css() -> impl IntoResponse {
+    ([(axum::http::header::CONTENT_TYPE, "text/css")], crate::theme::THEME_CSS)
+}
+
+async fn index(State(st): State) -> Html {
+    let mut s = String::from("\
+
docsite
    "); + for p in &st.reg.project { + s.push_str(&format!("
  • {0}
  • ", p.slug)); + } + s.push_str("
"); + Html(s) +} + +async fn page(State(st): State, Path((slug, page)): Path<(String, String)>) -> Response { + let url_page = page.trim_end_matches('/'); + let Some(pref) = st.reg.resolve(&slug, url_page) else { + return (StatusCode::NOT_FOUND, "404 — not a published page").into_response(); + }; + let proj = st.reg.project(&pref.slug).expect("resolved slug exists"); + let full = proj.source_path(&pref.rel); + let title = url_page.rsplit('/').next().unwrap_or(url_page).to_string(); + + // fetch SHA + raw inside spawn_blocking (the Fetcher is sync) + let f = st.fetcher.clone(); + let (repo, branch) = (proj.repo.clone(), proj.branch.clone()); + let full2 = full.clone(); + let fetched = tokio::task::spawn_blocking(move || { + let sha = f.branch_sha(&repo, &branch)?; + let body = f.fetch_raw(&repo, &branch, &full2)?; + Ok::<_, GiteaError>((sha, body)) + }) + .await + .expect("join"); + + match fetched { + Ok((sha, md)) => { + let html = st.cache.get_or_render(&proj.repo, &full, &sha, || { + let body = crate::render::render_doc(&md); + crate::theme::page_html(&st.reg, &pref.slug, &title, &body) + }); + Html(html).into_response() + } + Err(GiteaError::NotFound { repo, path }) => { + // fail-loud: a visible error block, 200 (the broken ref is content) + let block = format!( + "
broken reference: {repo} {}:{path} not found
", + proj.branch + ); + let html = crate::theme::page_html(&st.reg, &pref.slug, &title, &block); + Html(html).into_response() + } + Err(GiteaError::Transport(t)) => { + (StatusCode::BAD_GATEWAY, format!("502 — source unreachable: {t}")).into_response() + } + } +} + +/// Bind and serve (the production entry from `main`). +pub fn run(registry_path: &str) -> std::process::ExitCode { + let reg = match crate::config::load(registry_path) { + Ok(r) => r, + Err(e) => { eprintln!("docsite: {e}"); return std::process::ExitCode::from(2); } + }; + let fetcher = Arc::new(crate::gitea::GiteaClient::new(®.gitea)); + let report = check_all_boot(®, fetcher.as_ref()); + eprintln!("docsite: serving {} projects on {}", reg.project.len(), reg.bind); + eprintln!("docsite: {} nav references, {} broken at boot", report.results.len(), report.broken()); + + let bind = reg.bind.clone(); + let state = AppState { reg: Arc::new(reg), fetcher, cache: Arc::new(Cache::new()) }; + let rt = tokio::runtime::Runtime::new().expect("tokio runtime"); + rt.block_on(async move { + let listener = tokio::net::TcpListener::bind(&bind).await.expect("bind"); + axum::serve(listener, router(state)).await.expect("serve"); + }); + std::process::ExitCode::SUCCESS +} + +fn check_all_boot(reg: &Registry, f: &dyn Fetcher) -> crate::check::CheckReport { + crate::check::check_all(reg, f) +} diff --git a/src/theme/mod.rs b/src/theme/mod.rs new file mode 100644 index 0000000..35fd8e3 --- /dev/null +++ b/src/theme/mod.rs @@ -0,0 +1,87 @@ +//! The themed HTML shell: header breadcrumb, nav built from the registry, +//! and the rendered doc body. The stylesheet is embedded (served at +//! /assets/theme.css by the server module) so the binary is self-contained. + +use crate::config::{Project, Registry}; + +pub const THEME_CSS: &str = include_str!("../../assets/theme.css"); + +/// Build the nav `
1
"), "table not rendered"); + assert!(body.contains(" visible error block + let mut s = state(); + let r: Registry = toml::from_str( + "bind=\"x\"\ngitea=\"y\"\n[[project]]\nslug=\"aura\"\nrepo=\"Brummel/aura\"\nbranch=\"main\"\ndocs=\"docs\"\n[[project.section]]\ntitle=\"D\"\npages=[\"gone.md\"]\n", + ).unwrap(); + s.reg = Arc::new(r); + let (status, body) = get(router(s), "/aura/gone").await; + assert_eq!(status, StatusCode::OK, "broken ref is content, not a server fault"); + assert!(body.contains("docsite-error"), "no fail-loud error block: {body}"); + assert!(body.contains("broken reference"), "error block text missing"); +} + +/// A Fetcher that is always unreachable — every call is a transport error. +/// Lets the E2E layer reach the 502 arm that MockFetcher (which only yields +/// Ok / NotFound) cannot exercise. +struct UnreachableFetcher; + +impl Fetcher for UnreachableFetcher { + fn branch_sha(&self, _repo: &str, _branch: &str) -> Result { + Err(GiteaError::Transport("connection refused".into())) + } + fn fetch_raw(&self, _repo: &str, _branch: &str, _path: &str) -> Result { + Err(GiteaError::Transport("connection refused".into())) + } +} + +/// Invariant 3's broken-ref-vs-unreachable distinction, at the HTTP layer: an +/// *unreachable* Gitea is a server-side fault (502 Bad Gateway), NOT the 200 +/// fail-loud content block that a renamed/missing source produces. Collapsing +/// the two — e.g. rendering a 200 error block on a transport failure — would let +/// a transient outage masquerade as a permanent broken reference. The page is +/// whitelisted so `resolve` passes and the transport arm is what's exercised. +/// This is the twin of `broken_reference_shows_failloud_block` (the 200 arm). +#[tokio::test] +async fn transport_failure_is_502_not_failloud_block() { + let st = AppState { + reg: Arc::new(reg()), + fetcher: Arc::new(UnreachableFetcher), + cache: Arc::new(Cache::new()), + }; + let (status, body) = get(router(st), "/aura/design/INDEX").await; + assert_eq!(status, StatusCode::BAD_GATEWAY, "unreachable Gitea must be 502, not 200 content"); + assert!( + !body.contains("docsite-error"), + "a transport failure must not render the fail-loud broken-ref block: {body}" + ); +} + +/// The themed page links its stylesheet at `/assets/theme.css`; this pins that +/// the route actually *serves* the embedded stylesheet — 200, a `text/css` +/// content type, and the real CSS body — so the self-contained binary's styling +/// cannot silently 404. `renders_listed_page_with_theme_and_nav` only asserts the +/// href string is present in the shell, never that the route resolves. +#[tokio::test] +async fn theme_css_route_serves_embedded_stylesheet() { + let resp = router(state()) + .oneshot( + Request::builder() + .uri("/assets/theme.css") + .body(axum::body::Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + let ctype = resp + .headers() + .get(axum::http::header::CONTENT_TYPE) + .and_then(|v| v.to_str().ok()) + .unwrap_or(""); + assert_eq!(ctype, "text/css", "stylesheet must be served as text/css"); + let bytes = to_bytes(resp.into_body(), usize::MAX).await.unwrap(); + let body = String::from_utf8_lossy(&bytes); + assert!(body.contains("--bg: #16161a"), "served body must be the embedded stylesheet"); +} + +/// Whitelist publishing (invariant 5) at the index level: the root index +/// enumerates exactly the registry's projects as navigable links — a project the +/// registry does not list never appears, and each listed project links to its own +/// slug root. Two projects pin that the list is built from the registry (not +/// hardcoded to one), and the `
  • ` count pins *exactly* those, no more. +#[tokio::test] +async fn index_lists_exactly_the_registry_projects_as_links() { + let r: Registry = toml::from_str( + r#" +bind="x" +gitea="y" +[[project]] +slug="aura" +repo="Brummel/aura" +branch="main" +docs="docs" +[[project]] +slug="data-server" +repo="Brummel/data-server" +branch="main" +docs="docs" +"#, + ).unwrap(); + let st = AppState { + reg: Arc::new(r), + fetcher: Arc::new(MockFetcher::new("a517899")), + cache: Arc::new(Cache::new()), + }; + let (status, body) = get(router(st), "/").await; + assert_eq!(status, StatusCode::OK); + assert!(body.contains("aura"), "aura missing from index: {body}"); + assert!( + body.contains("data-server"), + "data-server missing from index: {body}" + ); + assert_eq!( + body.matches("
  • ").count(), + 2, + "index must list exactly the registry's projects, no more: {body}" + ); +} diff --git a/tests/live_smoke.rs b/tests/live_smoke.rs new file mode 100644 index 0000000..c94add9 --- /dev/null +++ b/tests/live_smoke.rs @@ -0,0 +1,16 @@ +//! Gated smoke test against the real Gitea. Skips cleanly when Gitea is +//! unreachable (the project's skip-on-no-dependency convention) so it never +//! fails on an offline machine. + +use docsite::gitea::{Fetcher, GiteaClient}; + +#[test] +fn aura_glossary_is_fetchable_from_live_gitea() { + let client = GiteaClient::new("http://192.168.178.103:3000"); + match client.fetch_raw("Brummel/aura", "main", "docs/glossary.md") { + Ok(body) => assert!(!body.is_empty(), "live glossary should be non-empty"), + Err(e) => { + eprintln!("skip: live Gitea unreachable or auth-gated: {e:?}"); + } + } +}