From 12f2a303e46f460775a0edddaeaba5ea77d77627 Mon Sep 17 00:00:00 2001 From: Seongmin Lee Date: Tue, 22 Sep 2026 02:09:26 +0900 Subject: [PATCH] gitmirror: more km capabilities Signed-off-by: Seongmin Lee --- Cargo.lock | 79 ++- Cargo.toml | 2 + gitmirror/crates/gitmirror-xrpc/Cargo.toml | 2 + gitmirror/crates/gitmirror-xrpc/src/error.rs | 24 + .../crates/gitmirror-xrpc/src/last_commit.rs | 224 ++++++++ gitmirror/crates/gitmirror-xrpc/src/lib.rs | 10 + .../crates/gitmirror-xrpc/src/routes/git.rs | 18 +- .../crates/gitmirror-xrpc/src/routes/mod.rs | 1 + .../crates/gitmirror-xrpc/src/routes/reads.rs | 530 ++++++++++++++++++ gitmirror/crates/gitmirror-xrpc/src/sniff.rs | 267 +++++++++ gitmirror/crates/gitmirror/Cargo.toml | 1 + gitmirror/crates/gitmirror/src/config.rs | 8 + gitmirror/crates/gitmirror/src/main.rs | 12 + nix/Cargo.nix | 488 +++++++++++++++- 14 files changed, 1653 insertions(+), 13 deletions(-) create mode 100644 gitmirror/crates/gitmirror-xrpc/src/last_commit.rs create mode 100644 gitmirror/crates/gitmirror-xrpc/src/routes/reads.rs create mode 100644 gitmirror/crates/gitmirror-xrpc/src/sniff.rs diff --git a/Cargo.lock b/Cargo.lock index a9718db7e..f5816306b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -229,6 +229,12 @@ dependencies = [ "rustversion", ] +[[package]] +name = "arcstr" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03918c3dbd7701a85c6b9887732e2921175f26c350b4563841d0958c21d57e6d" + [[package]] name = "argon2" version = "0.6.0-rc.8" @@ -1411,12 +1417,16 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "combine" -version = "4.6.7" +version = "4.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" dependencies = [ "bytes", + "futures-core", "memchr", + "pin-project-lite", + "tokio", + "tokio-util", ] [[package]] @@ -1876,18 +1886,47 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b" dependencies = [ - "deadpool-runtime", + "deadpool-runtime 0.1.4", "lazy_static", "num_cpus", "tokio", ] +[[package]] +name = "deadpool" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e98a7e119cd347f4201e1159b19831029e203e2d8b790547708e8157b4acf1e" +dependencies = [ + "deadpool-runtime 0.3.1", + "tokio", +] + +[[package]] +name = "deadpool-redis" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243852fff546d8c5aac6ceaac7587f7137915b102c3fbec6256a64ce52452bd2" +dependencies = [ + "deadpool 0.13.1", + "redis", +] + [[package]] name = "deadpool-runtime" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" +[[package]] +name = "deadpool-runtime" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2657f61fb1dd8bf37a8d51093cc7cee4e77125b22f7753f49b289f831bec2bae" +dependencies = [ + "tokio", +] + [[package]] name = "defmt" version = "1.1.0" @@ -2763,6 +2802,7 @@ dependencies = [ "bobbin-runtime", "clap", "confique", + "deadpool-redis", "futures", "gitmirror-xrpc", "jacquard-axum", @@ -2803,6 +2843,7 @@ dependencies = [ "bobbin-knot-proxy", "bobbin-runtime", "chrono", + "deadpool-redis", "futures-lite", "gitmirror-git", "gix", @@ -2820,6 +2861,7 @@ dependencies = [ "reqwest 0.13.1", "rustc-hash", "serde_json", + "sha2 0.11.0", "tangled-axum", "tempfile", "thiserror 2.0.18", @@ -7433,6 +7475,29 @@ dependencies = [ "yasna 0.6.0", ] +[[package]] +name = "redis" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acbc41a996f7652b2ddd9dfd98cc4ff602cfd742ae35382f07f608405ab50ed" +dependencies = [ + "arcstr", + "async-lock", + "bytes", + "cfg-if", + "combine", + "futures-util", + "itoa", + "percent-encoding", + "pin-project-lite", + "ryu", + "socket2", + "tokio", + "tokio-util", + "url", + "xxhash-rust", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -10337,7 +10402,7 @@ checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031" dependencies = [ "assert-json-diff", "base64", - "deadpool", + "deadpool 0.12.3", "futures", "http", "http-body-util", @@ -10420,6 +10485,12 @@ dependencies = [ "markup5ever", ] +[[package]] +name = "xxhash-rust" +version = "0.8.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aee1b19627c7c60102ab80d3a9cbe18de90bfe03bfa6c3715447681f0e8c8af6" + [[package]] name = "yasna" version = "0.5.2" diff --git a/Cargo.toml b/Cargo.toml index 09a24c391..fe9bfb152 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -183,6 +183,8 @@ bs58 = "0.5" zeroize = { version = "1", features = ["derive"] } subtle = "2.6" +deadpool-redis = "0.23" + wiremock = "0.6" tempfile = "3" proptest = "1" diff --git a/gitmirror/crates/gitmirror-xrpc/Cargo.toml b/gitmirror/crates/gitmirror-xrpc/Cargo.toml index 26d17d047..39c07c2e7 100644 --- a/gitmirror/crates/gitmirror-xrpc/Cargo.toml +++ b/gitmirror/crates/gitmirror-xrpc/Cargo.toml @@ -16,6 +16,7 @@ tangled-axum = { workspace = true } anyhow = { workspace = true } axum = { workspace = true } chrono = { workspace = true } +deadpool-redis = { workspace = true } jacquard-axum = { workspace = true } jacquard-common = { workspace = true } jacquard-identity = { workspace = true } @@ -33,6 +34,7 @@ gix-transport = { workspace = true, features = ["async-client"] } quick_cache = { workspace = true } futures-lite = { workspace = true } reqwest = { workspace = true } +sha2 = { workspace = true } [dev-dependencies] tempfile = "3" diff --git a/gitmirror/crates/gitmirror-xrpc/src/error.rs b/gitmirror/crates/gitmirror-xrpc/src/error.rs index 9668069f2..b2c0ec772 100644 --- a/gitmirror/crates/gitmirror-xrpc/src/error.rs +++ b/gitmirror/crates/gitmirror-xrpc/src/error.rs @@ -15,6 +15,10 @@ pub(crate) enum XrpcError { RepoNotFound { detail: String }, RefNotFound { rev: String, detail: String }, RevisionNotFound { rev: String }, + BranchNotFound { name: String }, + EntryNotFound { path: String }, + PathNotFound { path: String }, + NoMergeBase { base: String, head: String }, CompareError(String), FileNotFound { path: String }, BlameTooLarge, @@ -50,6 +54,26 @@ impl IntoResponse for XrpcError { "RevisionNotFound", format!("commit not found: {rev}"), ), + Self::BranchNotFound { name } => ( + StatusCode::NOT_FOUND, + "BranchNotFound", + format!("branch not found: {name}"), + ), + Self::EntryNotFound { path } => ( + StatusCode::NOT_FOUND, + "EntryNotFound", + format!("entry not found: {path}"), + ), + Self::PathNotFound { path } => ( + StatusCode::NOT_FOUND, + "PathNotFound", + format!("path not found: {path}"), + ), + Self::NoMergeBase { base, head } => ( + StatusCode::NOT_FOUND, + "NoMergeBase", + format!("no common ancestor between {base} and {head}"), + ), Self::CompareError(m) => { error!(error = %m, "compare failed"); ( diff --git a/gitmirror/crates/gitmirror-xrpc/src/last_commit.rs b/gitmirror/crates/gitmirror-xrpc/src/last_commit.rs new file mode 100644 index 000000000..c7c19ad45 --- /dev/null +++ b/gitmirror/crates/gitmirror-xrpc/src/last_commit.rs @@ -0,0 +1,224 @@ +use std::collections::{HashMap, HashSet}; +use std::time::{Duration, Instant}; + +use deadpool_redis::Pool; +use deadpool_redis::redis; +use gix::ObjectId; +use gix::bstr::ByteSlice as _; +use gix::revision::walk::Sorting; +use tracing::warn; + +// TODO(boltless): change to something else like "/". currently empty to be v1-compatible +pub(crate) const DIR_KEY: &str = ""; + +const TTL: u64 = 30 * 24 * 60 * 60; + +pub(crate) const BUDGET: Duration = Duration::from_secs(5); + +fn cache_key(head: &ObjectId, tree_path: &str, name: &str) -> String { + let joined = match (tree_path.is_empty(), name.is_empty()) { + (true, true) => String::new(), + (true, false) => name.to_owned(), + (false, true) => tree_path.to_owned(), + (false, false) => format!("{tree_path}/{name}"), + }; + format!("last_commit:{head}:{joined}") +} + +pub(crate) async fn load( + pool: Option<&Pool>, + head: &ObjectId, + tree_path: &str, + names: &[String], +) -> HashMap { + let Some(pool) = pool else { + return HashMap::new(); + }; + if names.is_empty() { + return HashMap::new(); + } + let keys: Vec = names + .iter() + .map(|name| cache_key(head, tree_path, name)) + .collect(); + + let cached = async { + let mut conn = pool.get().await?; + let values = redis::cmd("MGET") + .arg(&keys) + .query_async::>>(&mut conn) + .await?; + anyhow::Ok(values) + } + .await; + + let cached = match cached { + Ok(cached) => cached, + Err(error) => { + warn!(%error, "last-commit cache read failed"); + return HashMap::new(); + } + }; + + names + .iter() + .zip(cached) + .filter_map(|(name, value)| { + let oid = ObjectId::from_hex(value?.as_bytes()).ok()?; + Some((name.clone(), oid)) + }) + .collect() +} + +pub(crate) async fn store( + pool: Option<&Pool>, + head: &ObjectId, + tree_path: &str, + found: &HashMap, +) { + let Some(pool) = pool else { return }; + if found.is_empty() { + return; + } + + let written = async { + let mut conn = pool.get().await?; + let mut pipe = redis::pipe(); + for (name, oid) in found { + pipe.cmd("SET") + .arg(cache_key(head, tree_path, name)) + .arg(oid.to_string()) + .arg("EX") + .arg(TTL) + .ignore(); + } + pipe.query_async::<()>(&mut conn).await?; + anyhow::Ok(()) + } + .await; + + if let Err(error) = written { + warn!(%error, "last-commit cache write failed"); + } +} + +fn commit_tree(repo: &gix::Repository, commit: ObjectId) -> Option { + repo.find_commit(commit).ok()?.tree_id().ok().map(|id| id.detach()) +} + +fn subtree_of( + repo: &gix::Repository, + memo: &mut HashMap>, + root: ObjectId, + tree_path: &str, +) -> Option { + if let Some(cached) = memo.get(&root) { + return *cached; + } + let found = if tree_path.is_empty() { + Some(root) + } else { + repo.find_tree(root) + .ok() + .and_then(|tree| tree.lookup_entry_by_path(tree_path).ok().flatten()) + .filter(|entry| entry.mode().is_tree()) + .map(|entry| entry.object_id()) + }; + memo.insert(root, found); + found +} + +fn ensure_entries( + repo: &gix::Repository, + memo: &mut HashMap>, + subtree: ObjectId, +) { + if memo.contains_key(&subtree) { + return; + } + let mut entries = HashMap::new(); + if let Ok(tree) = repo.find_tree(subtree) { + for entry in tree.iter().flatten() { + entries.insert(entry.filename().to_str_lossy().into_owned(), entry.object_id()); + } + } + memo.insert(subtree, entries); +} + +pub(crate) fn walk( + repo: &gix::Repository, + head: ObjectId, + tree_path: &str, + names: &[String], + deadline: Instant, +) -> HashMap { + let mut out = HashMap::new(); + if names.is_empty() { + return out; + } + let mut pending: HashSet<&str> = names.iter().map(String::as_str).collect(); + + let walk = repo + .rev_walk([head]) + .sorting(Sorting::ByCommitTime(Default::default())) + .all(); + let walk = match walk { + Ok(walk) => walk, + Err(error) => { + warn!(%error, "last-commit walk could not start"); + return out; + } + }; + + let mut subtrees: HashMap> = HashMap::new(); + let mut entries: HashMap> = HashMap::new(); + let empty = HashMap::new(); + + for info in walk { + if pending.is_empty() || Instant::now() >= deadline { + break; + } + let Ok(info) = info else { break }; + let Some(root) = commit_tree(repo, info.id) else { + continue; + }; + let current = subtree_of(repo, &mut subtrees, root, tree_path); + let parent = info + .parent_ids + .first() + .and_then(|parent| commit_tree(repo, *parent)) + .and_then(|root| subtree_of(repo, &mut subtrees, root, tree_path)); + + if current == parent { + continue; + } + + if pending.remove(DIR_KEY) { + out.insert(DIR_KEY.to_owned(), info.id); + } + if pending.is_empty() { + break; + } + + if let Some(subtree) = current { + ensure_entries(repo, &mut entries, subtree); + } + if let Some(subtree) = parent { + ensure_entries(repo, &mut entries, subtree); + } + let current = current.and_then(|s| entries.get(&s)).unwrap_or(&empty); + let parent = parent.and_then(|s| entries.get(&s)).unwrap_or(&empty); + + let touched: Vec<&str> = pending + .iter() + .copied() + .filter(|name| current.get(*name) != parent.get(*name)) + .collect(); + for name in touched { + pending.remove(name); + out.insert(name.to_owned(), info.id); + } + } + + out +} diff --git a/gitmirror/crates/gitmirror-xrpc/src/lib.rs b/gitmirror/crates/gitmirror-xrpc/src/lib.rs index 98723fb3c..687597fef 100644 --- a/gitmirror/crates/gitmirror-xrpc/src/lib.rs +++ b/gitmirror/crates/gitmirror-xrpc/src/lib.rs @@ -2,9 +2,11 @@ mod did_ext; mod diff; mod error; mod git_transport; +mod last_commit; mod merge; pub mod metrics; mod routes; +mod sniff; use std::{path::PathBuf, sync::Arc}; @@ -29,6 +31,7 @@ pub struct AppState { pub(crate) knot_policy: KnotPolicy, pub(crate) clock: Arc, pub(crate) stats: Arc, + pub(crate) redis: Option, } impl AppState { @@ -39,6 +42,7 @@ impl AppState { service_auth: service_auth::ServiceAuthConfig>, knot_policy: KnotPolicy, clock: Arc, + redis: Option, ) -> Self { let layout = Arc::new(Layout::new((*repo_base).clone())); Self { @@ -48,6 +52,7 @@ impl AppState { knot_policy, clock, stats: Arc::default(), + redis, } } } @@ -81,6 +86,11 @@ pub fn router(state: AppState) -> Router { .route("/xrpc/sh.tangled.git.temp2.mergeCheck", get(git::merge_check)) .route("/xrpc/sh.tangled.git.mergeCommit", post(git::merge_commit)) .route("/xrpc/org.tangled.temp.git.deleteBranch", post(git::delete_branch)) + .route("/xrpc/org.tangled.temp.git.getBlob", get(reads::get_blob)) + .route("/xrpc/org.tangled.temp.git.getBranch", get(reads::get_branch)) + .route("/xrpc/org.tangled.temp.git.getEntry", get(reads::get_entry)) + .route("/xrpc/org.tangled.temp.git.getMergeBase", get(reads::get_merge_base)) + .route("/xrpc/org.tangled.temp.git.getTree", get(reads::get_tree)) .layer(axum::middleware::from_fn(metrics::metrics_middleware)) .with_state(state) } diff --git a/gitmirror/crates/gitmirror-xrpc/src/routes/git.rs b/gitmirror/crates/gitmirror-xrpc/src/routes/git.rs index 19e6cb7ed..cfdbd22ea 100644 --- a/gitmirror/crates/gitmirror-xrpc/src/routes/git.rs +++ b/gitmirror/crates/gitmirror-xrpc/src/routes/git.rs @@ -60,7 +60,7 @@ impl MergeStyle { } } -struct GixSignature<'a>(gix::actor::SignatureRef<'a>); +pub(crate) struct GixSignature<'a>(pub(crate) gix::actor::SignatureRef<'a>); impl TryFrom> for sh_tangled::git::Signature { type Error = anyhow::Error; @@ -82,6 +82,22 @@ impl TryFrom> for sh_tangled::git::Signature { } } +impl TryFrom> for sh_tangled::git::temp::Signature { + type Error = anyhow::Error; + + fn try_from(sig: GixSignature) -> Result { + let sh_tangled::git::Signature { + name, email, when, .. + } = sig.try_into()?; + Ok(Self { + name, + email, + when, + extra_data: Default::default(), + }) + } +} + struct GixCommit<'a>(&'a gix::Commit<'a>); impl TryFrom> for list_commits::Commit { diff --git a/gitmirror/crates/gitmirror-xrpc/src/routes/mod.rs b/gitmirror/crates/gitmirror-xrpc/src/routes/mod.rs index c2bf1c3ee..873f9c3b8 100644 --- a/gitmirror/crates/gitmirror-xrpc/src/routes/mod.rs +++ b/gitmirror/crates/gitmirror-xrpc/src/routes/mod.rs @@ -1 +1,2 @@ pub mod git; +pub(crate) mod reads; diff --git a/gitmirror/crates/gitmirror-xrpc/src/routes/reads.rs b/gitmirror/crates/gitmirror-xrpc/src/routes/reads.rs new file mode 100644 index 000000000..2e541ae56 --- /dev/null +++ b/gitmirror/crates/gitmirror-xrpc/src/routes/reads.rs @@ -0,0 +1,530 @@ +use std::collections::HashMap; +use std::time::Instant; + +use axum::extract::State; +use axum::http::{HeaderMap, StatusCode, header}; +use axum::response::{IntoResponse as _, Response}; +use gix::ObjectId; +use gix::bstr::ByteSlice as _; +use gix::objs::tree::EntryKind; +use jacquard_axum::{ExtractXrpc, XrpcResponse}; +use jacquard_common::ToSmolStr; +use jacquard_common::types::did::Did; +use lexicons::org_tangled::temp::git::{get_blob, get_branch, get_entry, get_merge_base, get_tree}; +use lexicons::sh_tangled::git::temp; +use sha2::{Digest as _, Sha256}; +use tracing::warn; + +use crate::routes::git::GixSignature; +use crate::{AppState, error::XrpcError, last_commit, sniff}; + +const IMMUTABLE: &str = "public, max-age=31536000, immutable"; +const MUTABLE: &str = "private, no-store"; + +const BLOB_ETAG_DOMAIN: &str = "knotmirror.blob.v1"; +const LARGE_BLOB: u64 = 1024 * 1024; + +fn cache_control(refs: &[&str]) -> &'static str { + if !refs.is_empty() && refs.iter().all(|r| ObjectId::from_hex(r.as_bytes()).is_ok()) { + IMMUTABLE + } else { + MUTABLE + } +} + +fn cache_header(value: &'static str) -> [(header::HeaderName, &'static str); 1] { + [(header::CACHE_CONTROL, value)] +} + +fn open(state: &AppState, repo: &Did) -> Result { + state.layout.open(repo).map_err(|e| XrpcError::RepoNotFound { + detail: e.to_string(), + }) +} + +fn resolve_commit(repo: &gix::Repository, refspec: &str) -> Result { + if refspec.is_empty() || refspec.contains('\0') || refspec.starts_with('-') { + return Err(XrpcError::InvalidRequest(format!( + "invalid ref spec {refspec:?}" + ))); + } + let not_found = |detail: String| XrpcError::RefNotFound { + rev: refspec.to_owned(), + detail, + }; + let id = match repo.rev_parse_single(refspec.as_bytes()) { + Ok(id) => id, + Err(gix::revision::spec::parse::single::Error::RangedRev { spec }) => { + return Err(XrpcError::InvalidRequest(format!( + "ref must resolve to a single revision, got a range: {spec}" + ))); + } + Err(error) => return Err(not_found(error.to_string())), + }; + id.object() + .and_then(|object| object.peel_tags_to_end()) + .map_err(|e| not_found(e.to_string()))? + .try_into_commit() + .map(|commit| commit.id) + .map_err(|_| not_found("revision does not point at a commit".to_owned())) +} + +fn mode_of(kind: EntryKind) -> String { + format!("{:07o}", kind as u16) +} + +fn blob_size(repo: &gix::Repository, oid: ObjectId) -> Result { + repo.find_header(oid) + .map(|header| header.size()) + .map_err(|e| XrpcError::Internal(e.to_string())) +} + +fn hydrate(repo: &gix::Repository, oid: ObjectId) -> Result { + let internal = |e: &dyn std::fmt::Display| XrpcError::Internal(e.to_string()); + let commit = repo.find_commit(oid).map_err(|e| internal(&e))?; + let decoded = commit.decode().map_err(|e| internal(&e))?; + Ok(temp::Commit { + hash: oid.to_smolstr(), + tree: decoded.tree.to_smolstr(), + author: GixSignature(decoded.author().map_err(|e| internal(&e))?) + .try_into() + .map_err(|e: anyhow::Error| internal(&e))?, + committer: GixSignature(decoded.committer().map_err(|e| internal(&e))?) + .try_into() + .map_err(|e: anyhow::Error| internal(&e))?, + message: decoded.message.to_smolstr(), + extra_data: Default::default(), + }) +} + +fn hydrate_all( + repo: &gix::Repository, + found: &HashMap, +) -> HashMap { + let mut commits: HashMap = HashMap::new(); + found + .iter() + .filter_map(|(name, oid)| { + let commit = match commits.get(oid) { + Some(commit) => commit.clone(), + None => { + let commit = hydrate(repo, *oid) + .inspect_err(|error| warn!(?error, "could not read last commit")) + .ok()?; + commits.insert(*oid, commit.clone()); + commit + } + }; + Some((name.clone(), commit)) + }) + .collect() +} + +async fn last_commits( + state: &AppState, + repo: gix::Repository, + head: ObjectId, + tree_path: String, + names: Vec, +) -> Result, XrpcError> { + let pool = state.redis.clone(); + let cached = last_commit::load(pool.as_ref(), &head, &tree_path, &names).await; + let missing: Vec = names + .into_iter() + .filter(|name| !cached.contains_key(name)) + .collect(); + + let deadline = Instant::now() + last_commit::BUDGET; + let path = tree_path.clone(); + let (walked, hydrated) = tokio::task::spawn_blocking(move || { + let walked = last_commit::walk(&repo, head, &path, &missing, deadline); + let mut found = cached; + found.extend(walked.iter().map(|(name, oid)| (name.clone(), *oid))); + (walked, hydrate_all(&repo, &found)) + }) + .await + .map_err(|e| XrpcError::Internal(e.to_string()))?; + + last_commit::store(pool.as_ref(), &head, &tree_path, &walked).await; + Ok(hydrated) +} + +pub(crate) async fn get_branch( + State(state): State, + ExtractXrpc(args): ExtractXrpc, +) -> Result, XrpcError> { + let repo = open(&state, &args.repo)?; + let name = args.name.to_string(); + + tokio::task::spawn_blocking(move || { + let missing = || XrpcError::BranchNotFound { name: name.clone() }; + let mut reference = repo + .find_reference(format!("refs/heads/{name}").as_str()) + .map_err(|_| missing())?; + let commit = reference.peel_to_commit().map_err(|_| missing())?; + let decoded = commit + .decode() + .map_err(|e| XrpcError::Internal(e.to_string()))?; + Ok(get_branch::GetBranchOutput { + name: name.as_str().into(), + hash: commit.id.to_smolstr(), + message: decoded.message.to_smolstr(), + author: GixSignature( + decoded + .author() + .map_err(|e| XrpcError::Internal(e.to_string()))?, + ) + .try_into() + .map_err(|e: anyhow::Error| XrpcError::Internal(e.to_string()))?, + extra_data: Default::default(), + }) + }) + .await + .map_err(|e| XrpcError::Internal(e.to_string()))? + .map(XrpcResponse) +} + +pub(crate) async fn get_merge_base( + State(state): State, + ExtractXrpc(args): ExtractXrpc, +) -> Result< + ( + [(header::HeaderName, &'static str); 1], + XrpcResponse, + ), + XrpcError, +> { + let repo = open(&state, &args.repo)?; + let cache = cache_control(&[args.base.as_ref(), args.head.as_ref()]); + let (base_spec, head_spec) = (args.base.to_string(), args.head.to_string()); + + let commit = tokio::task::spawn_blocking(move || { + let base = resolve_commit(&repo, &base_spec)?; + let head = resolve_commit(&repo, &head_spec)?; + match repo.merge_base(head, base) { + Ok(id) => Ok(id.detach().to_smolstr()), + Err(gix::repository::merge_base::Error::NotFound { .. }) => Err(XrpcError::NoMergeBase { + base: base_spec, + head: head_spec, + }), + Err(error) => Err(XrpcError::Internal(error.to_string())), + } + }) + .await + .map_err(|e| XrpcError::Internal(e.to_string()))??; + + Ok(( + cache_header(cache), + XrpcResponse(get_merge_base::GetMergeBaseOutput { + commit, + extra_data: Default::default(), + }), + )) +} + +fn split_path(path: &str) -> (String, String) { + match path.rsplit_once('/') { + Some((dir, name)) => (dir.to_owned(), name.to_owned()), + None => (String::new(), path.to_owned()), + } +} + +fn submodule_at( + repo: &gix::Repository, + commit: ObjectId, + path: &str, +) -> Result, XrpcError> { + let Some(entry) = repo + .find_commit(commit) + .ok() + .and_then(|commit| commit.tree().ok()) + .and_then(|tree| tree.lookup_entry_by_path(".gitmodules").ok().flatten()) + else { + return Ok(None); + }; + if !entry.mode().is_blob() { + return Ok(None); + } + let object = entry + .object() + .map_err(|e| XrpcError::Internal(e.to_string()))?; + let modules = gix::submodule::File::from_bytes(&object.data, None, &Default::default()) + .map_err(|e| XrpcError::Internal(e.to_string()))?; + + let names: Vec<_> = modules.names().map(ToOwned::to_owned).collect(); + for name in names { + let name = name.as_bstr(); + let matches = modules + .path(name) + .ok() + .is_some_and(|value| value.as_ref() == path.as_bytes()); + if !matches { + continue; + } + let url = modules + .url(name) + .map_err(|e| XrpcError::Internal(e.to_string()))?; + let branch = match modules.branch(name).ok().flatten() { + Some(gix::submodule::config::Branch::Name(name)) => { + Some(name.to_str_lossy().as_ref().into()) + } + Some(gix::submodule::config::Branch::CurrentInSuperproject) => None, + None => None, + }; + return Ok(Some(temp::Submodule { + name: name.to_str_lossy().as_ref().into(), + url: url.to_bstring().to_str_lossy().as_ref().into(), + branch, + extra_data: Default::default(), + })); + } + Ok(None) +} + +pub(crate) async fn get_entry( + State(state): State, + ExtractXrpc(args): ExtractXrpc, +) -> Result< + ( + [(header::HeaderName, &'static str); 1], + XrpcResponse, + ), + XrpcError, +> { + let repo = open(&state, &args.repo)?; + let refspec = args.r#ref.as_deref().unwrap_or("HEAD").to_owned(); + let cache = cache_control(&[&refspec]); + let path = args.path.to_string(); + if path.is_empty() { + return Err(XrpcError::InvalidRequest( + "missing path parameter".to_owned(), + )); + } + + let (repo, head, mut output) = tokio::task::spawn_blocking(move || { + let head = resolve_commit(&repo, &refspec)?; + let (kind, oid) = { + let entry = repo + .find_commit(head) + .map_err(|e| XrpcError::Internal(e.to_string()))? + .tree() + .map_err(|e| XrpcError::Internal(e.to_string()))? + .lookup_entry_by_path(&path) + .map_err(|e| XrpcError::Internal(e.to_string()))? + .ok_or_else(|| XrpcError::EntryNotFound { path: path.clone() })?; + (entry.mode().kind(), entry.object_id()) + }; + let size = match kind { + EntryKind::Tree | EntryKind::Commit => 0, + _ => blob_size(&repo, oid)? as i64, + }; + let submodule = match kind { + EntryKind::Commit => submodule_at(&repo, head, &path)?, + _ => None, + }; + + Ok::<_, XrpcError>(( + repo, + head, + get_entry::GetEntryOutput { + name: split_path(&path).1.as_str().into(), + mode: mode_of(kind).as_str().into(), + oid: oid.to_smolstr(), + size, + last_commit: None, + submodule, + extra_data: Default::default(), + }, + )) + }) + .await + .map_err(|e| XrpcError::Internal(e.to_string()))??; + + let (dir, name) = split_path(&args.path); + output.last_commit = last_commits(&state, repo, head, dir, vec![name.clone()]) + .await? + .remove(&name); + + Ok((cache_header(cache), XrpcResponse(output))) +} + +pub(crate) async fn get_tree( + State(state): State, + ExtractXrpc(args): ExtractXrpc, +) -> Result< + ( + [(header::HeaderName, &'static str); 1], + XrpcResponse, + ), + XrpcError, +> { + let repo = open(&state, &args.repo)?; + let refspec = args.r#ref.as_deref().unwrap_or("HEAD").to_owned(); + let cache = cache_control(&[&refspec]); + let tree_path = args.path.as_deref().unwrap_or("").trim_matches('/').to_owned(); + + let path = tree_path.clone(); + let (repo, head, mut entries) = tokio::task::spawn_blocking(move || { + let head = resolve_commit(&repo, &refspec)?; + let entries = { + let root = repo + .find_commit(head) + .map_err(|e| XrpcError::Internal(e.to_string()))? + .tree() + .map_err(|e| XrpcError::Internal(e.to_string()))?; + let subtree = if path.is_empty() { + root + } else { + let entry = root + .lookup_entry_by_path(&path) + .map_err(|e| XrpcError::Internal(e.to_string()))? + .filter(|entry| entry.mode().is_tree()) + .ok_or_else(|| XrpcError::PathNotFound { path: path.clone() })?; + repo.find_tree(entry.object_id()) + .map_err(|_| XrpcError::PathNotFound { path: path.clone() })? + }; + + let mut entries: Vec = Vec::new(); + for entry in subtree.iter() { + let entry = entry.map_err(|e| XrpcError::Internal(e.to_string()))?; + let kind = entry.mode().kind(); + let size = match kind { + EntryKind::Tree | EntryKind::Commit => 0, + _ => blob_size(&repo, entry.object_id())? as i64, + }; + entries.push(get_tree::TreeEntry { + name: entry.filename().to_str_lossy().as_ref().into(), + mode: mode_of(kind).as_str().into(), + size, + last_commit: None, + extra_data: Default::default(), + }); + } + entries + }; + Ok::<_, XrpcError>((repo, head, entries)) + }) + .await + .map_err(|e| XrpcError::Internal(e.to_string()))??; + + let mut names: Vec = vec![last_commit::DIR_KEY.to_owned()]; + names.extend(entries.iter().map(|entry| entry.name.to_string())); + let mut found = last_commits(&state, repo, head, tree_path, names).await?; + + for entry in &mut entries { + entry.last_commit = found.remove(entry.name.as_str()); + } + + Ok(( + cache_header(cache), + XrpcResponse(get_tree::GetTreeOutput { + entries, + last_commit: found.remove(last_commit::DIR_KEY), + extra_data: Default::default(), + }), + )) +} + +fn blob_etag(contents: &[u8]) -> String { + let mut digest = Sha256::new(); + digest.update(BLOB_ETAG_DOMAIN.as_bytes()); + digest.update(b"\0"); + digest.update(contents); + let hex: String = digest + .finalize() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect(); + format!("\"{hex}\"") +} + +fn etag_matches(headers: &HeaderMap, etag: &str) -> bool { + headers + .get(header::IF_NONE_MATCH) + .and_then(|value| value.to_str().ok()) + .is_some_and(|value| { + value == "*" + || value + .split(',') + .map(str::trim) + .any(|candidate| candidate.trim_start_matches("W/") == etag) + }) +} + +pub(crate) async fn get_blob( + State(state): State, + headers: HeaderMap, + ExtractXrpc(args): ExtractXrpc, +) -> Result { + let repo = open(&state, &args.repo)?; + let refspec = args.r#ref.as_deref().unwrap_or("HEAD").to_owned(); + let cache = cache_control(&[&refspec]); + let path = args.path.to_string(); + if path.is_empty() { + return Err(XrpcError::InvalidRequest( + "missing path parameter".to_owned(), + )); + } + + let lookup = path.clone(); + let contents = tokio::task::spawn_blocking(move || { + let head = resolve_commit(&repo, &refspec)?; + let entry = repo + .find_commit(head) + .map_err(|e| XrpcError::Internal(e.to_string()))? + .tree() + .map_err(|e| XrpcError::Internal(e.to_string()))? + .lookup_entry_by_path(&lookup) + .map_err(|e| XrpcError::Internal(e.to_string()))? + .filter(|entry| entry.mode().is_blob_or_symlink()) + .ok_or_else(|| XrpcError::FileNotFound { + path: lookup.clone(), + })?; + repo.find_object(entry.object_id()) + .map(|object| object.data.clone()) + .map_err(|e| XrpcError::Internal(e.to_string())) + }) + .await + .map_err(|e| XrpcError::Internal(e.to_string()))??; + + if contents.len() as u64 > LARGE_BLOB { + return Ok(( + StatusCode::OK, + [ + (header::CACHE_CONTROL, cache.to_owned()), + (header::CONTENT_TYPE, "application/octet-stream".to_owned()), + (header::CONTENT_LENGTH, contents.len().to_string()), + (header::X_CONTENT_TYPE_OPTIONS, "nosniff".to_owned()), + ], + contents, + ) + .into_response()); + } + + let etag = blob_etag(&contents); + if etag_matches(&headers, &etag) { + return Ok((StatusCode::NOT_MODIFIED, cache_header(cache)).into_response()); + } + + let detected = sniff::override_by_extension(&path, sniff::detect_content_type(&contents)); + let content_type = if detected.starts_with("image/") || detected.starts_with("video/") { + detected.to_owned() + } else if sniff::is_textual_mime(detected) { + "text/plain; charset=utf-8".to_owned() + } else { + "application/octet-stream".to_owned() + }; + + Ok(( + StatusCode::OK, + [ + (header::CACHE_CONTROL, cache.to_owned()), + (header::CONTENT_TYPE, content_type), + (header::ETAG, etag), + (header::CONTENT_LENGTH, contents.len().to_string()), + (header::X_CONTENT_TYPE_OPTIONS, "nosniff".to_owned()), + ], + contents, + ) + .into_response()) +} diff --git a/gitmirror/crates/gitmirror-xrpc/src/sniff.rs b/gitmirror/crates/gitmirror-xrpc/src/sniff.rs new file mode 100644 index 000000000..f653573cf --- /dev/null +++ b/gitmirror/crates/gitmirror-xrpc/src/sniff.rs @@ -0,0 +1,267 @@ +//! ported from `knot2/crates/knot-xrpc/src/sniff.rs` +// TODO: there can be only one + +const SNIFF_LIMIT: usize = 512; + +fn is_ws(byte: u8) -> bool { + matches!(byte, b'\t' | b'\n' | 0x0c | b'\r' | b' ') +} + +fn is_tt(byte: u8) -> bool { + matches!(byte, b' ' | b'>') +} + +enum Sig { + Exact(&'static [u8], &'static str), + Masked { + mask: &'static [u8], + pat: &'static [u8], + skip_ws: bool, + ct: &'static str, + }, + Html(&'static [u8]), + Mp4, + Text, +} + +impl Sig { + fn detect(&self, data: &[u8], first_non_ws: usize) -> Option<&'static str> { + match self { + Sig::Exact(sig, ct) => data.starts_with(sig).then_some(*ct), + Sig::Masked { + mask, + pat, + skip_ws, + ct, + } => { + let data = if *skip_ws { + &data[first_non_ws..] + } else { + data + }; + (mask.len() == pat.len() + && data.len() >= pat.len() + && pat + .iter() + .zip(mask.iter()) + .enumerate() + .all(|(index, (byte, mask))| data[index] & mask == *byte)) + .then_some(*ct) + } + Sig::Html(tag) => { + let data = &data[first_non_ws..]; + (data.len() > tag.len() + && tag.iter().enumerate().all(|(index, byte)| { + let candidate = data[index]; + let candidate = match byte.is_ascii_uppercase() { + true => candidate & 0xDF, + false => candidate, + }; + *byte == candidate + }) + && is_tt(data[tag.len()])) + .then_some("text/html; charset=utf-8") + } + Sig::Mp4 => mp4(data), + Sig::Text => text(data, first_non_ws), + } + } +} + +fn mp4(data: &[u8]) -> Option<&'static str> { + if data.len() < 12 { + return None; + } + let box_size = u32::from_be_bytes([data[0], data[1], data[2], data[3]]) as usize; + if data.len() < box_size || !box_size.is_multiple_of(4) || &data[4..8] != b"ftyp" { + return None; + } + (8..box_size) + .step_by(4) + .filter(|start| *start != 12) + .any(|start| &data[start..start + 3] == b"mp4") + .then_some("video/mp4") +} + +fn text(data: &[u8], first_non_ws: usize) -> Option<&'static str> { + data[first_non_ws..] + .iter() + .all(|byte| !matches!(byte, 0x00..=0x08 | 0x0b | 0x0e..=0x1a | 0x1c..=0x1f)) + .then_some("text/plain; charset=utf-8") +} + +const SIGNATURES: &[Sig] = &[ + Sig::Html(b" &'static str { + let data = &content[..content.len().min(SNIFF_LIMIT)]; + let first_non_ws = data + .iter() + .position(|byte| !is_ws(*byte)) + .unwrap_or(data.len()); + SIGNATURES + .iter() + .find_map(|sig| sig.detect(data, first_non_ws)) + .unwrap_or("application/octet-stream") +} + +pub(crate) fn override_by_extension(path: &str, detected: &'static str) -> &'static str { + let extension = path.rsplit_once('.').map(|(_, ext)| ext).unwrap_or(""); + match extension.to_ascii_lowercase().as_str() { + "svg" => "image/svg+xml", + "avif" => "image/avif", + "jxl" => "image/jxl", + "heic" | "heif" => "image/heif", + _ => detected, + } +} + +pub(crate) fn is_textual_mime(mime: &str) -> bool { + mime.starts_with("text/") + || matches!( + mime, + "application/json" + | "application/xml" + | "application/yaml" + | "application/x-yaml" + | "application/toml" + | "application/javascript" + | "application/ecmascript" + ) +} + +#[cfg(test)] +mod tests { + use super::detect_content_type; + + #[test] + fn detects_common_content_signatures() { + let cases: &[(&[u8], &str)] = &[ + (b" \n", "text/html; charset=utf-8"), + (b"", "text/html; charset=utf-8"), + (b"", "text/html; charset=utf-8"), + (b"\n\t", "text/xml; charset=utf-8"), + (b"\xfe\xff\x00h", "text/plain; charset=utf-16be"), + (b"\xef\xbb\xbfhello", "text/plain; charset=utf-8"), + (b"\x89PNG\x0d\x0a\x1a\x0a", "image/png"), + (b"GIF89a", "image/gif"), + (b"fn main() {}\n", "text/plain; charset=utf-8"), + (b"\x00\x01\x02\x03", "application/octet-stream"), + (b"", "text/plain; charset=utf-8"), + ]; + cases.iter().for_each(|(input, expected)| { + assert_eq!(detect_content_type(input), *expected); + }); + } +} diff --git a/gitmirror/crates/gitmirror/Cargo.toml b/gitmirror/crates/gitmirror/Cargo.toml index e92a4ad42..7ef4eab1a 100644 --- a/gitmirror/crates/gitmirror/Cargo.toml +++ b/gitmirror/crates/gitmirror/Cargo.toml @@ -14,6 +14,7 @@ bobbin-runtime = { workspace = true } gitmirror-xrpc = { workspace = true } anyhow = { workspace = true } +deadpool-redis = { workspace = true } axum = { workspace = true } clap = { workspace = true } confique = { workspace = true } diff --git a/gitmirror/crates/gitmirror/src/config.rs b/gitmirror/crates/gitmirror/src/config.rs index 51965fabc..d2920ab55 100644 --- a/gitmirror/crates/gitmirror/src/config.rs +++ b/gitmirror/crates/gitmirror/src/config.rs @@ -18,6 +18,8 @@ pub struct MirrorConfig { #[config(nested)] pub identity: IdentityConfig, + #[config(nested)] + pub redis: RedisConfig, #[config(nested)] pub knot: KnotConfig, #[config(nested)] @@ -89,6 +91,12 @@ pub struct IdentityConfig { pub plc_url: String, } +#[derive(Debug, Config)] +pub struct RedisConfig { + #[config(env = "GITMIRROR_REDIS_URL")] + pub url: Option, +} + #[derive(Debug, Config)] pub struct KnotConfig { /// Allow knot endpoints on private or loopback addresses. diff --git a/gitmirror/crates/gitmirror/src/main.rs b/gitmirror/crates/gitmirror/src/main.rs index f33c54ddb..6131c4e53 100644 --- a/gitmirror/crates/gitmirror/src/main.rs +++ b/gitmirror/crates/gitmirror/src/main.rs @@ -102,6 +102,17 @@ async fn serve(cfg: MirrorConfig) -> anyhow::Result<()> { ); let service_auth = ServiceAuthConfig::new(cfg.service_auth.did.clone(), directory).disable_replay_protection(); + let redis = cfg + .redis + .url + .as_deref() + .map(|url| { + deadpool_redis::Config::from_url(url) + .create_pool(Some(deadpool_redis::Runtime::Tokio1)) + .with_context(|| format!("build redis pool for {url}")) + }) + .transpose()?; + let state = AppState::new( Arc::new(cfg.repo.scan_path), http, @@ -111,6 +122,7 @@ async fn serve(cfg: MirrorConfig) -> anyhow::Result<()> { require_https: cfg.knot.require_https, }, Arc::new(SystemClock::new()), + redis, ); let app = router(state); diff --git a/nix/Cargo.nix b/nix/Cargo.nix index 7d408e799..439d53de5 100644 --- a/nix/Cargo.nix +++ b/nix/Cargo.nix @@ -613,6 +613,16 @@ rec { # File a bug if you depend on any for non-debug work! debug = internal.debugCrate { inherit packageId; }; }; + "tranquil-gate" = rec { + packageId = "tranquil-gate"; + build = internal.buildRustCrateWithFeatures { + packageId = "tranquil-gate"; + }; + + # Debug support which might change between releases. + # File a bug if you depend on any for non-debug work! + debug = internal.debugCrate { inherit packageId; }; + }; "trusted-proxies" = rec { packageId = "trusted-proxies"; build = internal.buildRustCrateWithFeatures { @@ -1300,6 +1310,21 @@ rec { "serde" = [ "dep:serde" ]; }; }; + "arcstr" = rec { + crateName = "arcstr"; + version = "1.2.0"; + edition = "2021"; + sha256 = "0vbyslhqr5fh84w5dd2hqck5y5r154p771wqddfah0bpplyqr483"; + authors = [ + "Thom Chiovoloni " + ]; + features = { + "default" = [ "substr" ]; + "serde" = [ "dep:serde" ]; + "substr-usize-indices" = [ "substr" ]; + }; + resolvedDefaultFeatures = [ "default" "substr" ]; + }; "argon2" = rec { crateName = "argon2"; version = "0.6.0-rc.8"; @@ -5222,9 +5247,9 @@ rec { }; "combine" = rec { crateName = "combine"; - version = "4.6.7"; + version = "4.6.8"; edition = "2018"; - sha256 = "1z8rh8wp59gf8k23ar010phgs0wgf5i8cx4fg01gwcnzfn5k0nms"; + sha256 = "0ppwzwdmszpan9ybx1myc6ldg5zih2sazf9idckdxrh9gn9j1hyg"; authors = [ "Markus Westerlind " ]; @@ -5234,17 +5259,49 @@ rec { packageId = "bytes"; optional = true; } + { + name = "futures-core"; + packageId = "futures-core"; + rename = "futures-core-03"; + optional = true; + usesDefaultFeatures = false; + } { name = "memchr"; packageId = "memchr"; usesDefaultFeatures = false; } + { + name = "pin-project-lite"; + packageId = "pin-project-lite"; + optional = true; + } + { + name = "tokio"; + packageId = "tokio"; + rename = "tokio-dep"; + optional = true; + usesDefaultFeatures = false; + } + { + name = "tokio-util"; + packageId = "tokio-util"; + optional = true; + usesDefaultFeatures = false; + features = [ "codec" ]; + } ]; devDependencies = [ { name = "bytes"; packageId = "bytes"; } + { + name = "tokio"; + packageId = "tokio"; + rename = "tokio-dep"; + features = [ "fs" "macros" "rt" "rt-multi-thread" "io-util" ]; + } ]; features = { "bytes" = [ "dep:bytes" ]; @@ -5265,7 +5322,7 @@ rec { "tokio-dep" = [ "dep:tokio-dep" ]; "tokio-util" = [ "dep:tokio-util" ]; }; - resolvedDefaultFeatures = [ "alloc" "bytes" "default" "std" ]; + resolvedDefaultFeatures = [ "alloc" "bytes" "default" "futures-core-03" "pin-project-lite" "std" "tokio" "tokio-dep" "tokio-util" ]; }; "compression-codecs" = rec { crateName = "compression-codecs"; @@ -6652,7 +6709,7 @@ rec { sha256 = "0icp1n694mxxbskqyf51a1pmc341hc7lwxadqapr09gah57dx1n2"; }; - "deadpool" = rec { + "deadpool 0.12.3" = rec { crateName = "deadpool"; version = "0.12.3"; edition = "2021"; @@ -6663,7 +6720,7 @@ rec { dependencies = [ { name = "deadpool-runtime"; - packageId = "deadpool-runtime"; + packageId = "deadpool-runtime 0.1.4"; } { name = "lazy_static"; @@ -6694,7 +6751,112 @@ rec { }; resolvedDefaultFeatures = [ "default" "managed" "unmanaged" ]; }; - "deadpool-runtime" = rec { + "deadpool 0.13.1" = rec { + crateName = "deadpool"; + version = "0.13.1"; + edition = "2024"; + sha256 = "07ng99xibs08fxa91dyqw81y4a8hhccrn5g10517yd6d37hsg61y"; + authors = [ + "Michael P. Jung " + ]; + dependencies = [ + { + name = "deadpool-runtime"; + packageId = "deadpool-runtime 0.3.1"; + } + { + name = "tokio"; + packageId = "tokio"; + features = [ "sync" ]; + } + ]; + devDependencies = [ + { + name = "tokio"; + packageId = "tokio"; + features = [ "macros" "rt" "rt-multi-thread" "time" ]; + } + ]; + features = { + "default" = [ "managed" "unmanaged" ]; + "rt_async-std_1" = [ "deadpool-runtime/async-std_1" ]; + "rt_smol_2" = [ "deadpool-runtime/smol_2" ]; + "rt_tokio_1" = [ "deadpool-runtime/tokio_1" ]; + "serde" = [ "dep:serde" ]; + }; + resolvedDefaultFeatures = [ "managed" "rt_tokio_1" ]; + }; + "deadpool-redis" = rec { + crateName = "deadpool-redis"; + version = "0.23.1"; + edition = "2024"; + sha256 = "1lib8m9cwr3a4p3bwgrc21dr2dvigxccganfqsmcbn26ypzm4f14"; + libName = "deadpool_redis"; + authors = [ + "Michael P. Jung " + "Subeom Choi " + ]; + dependencies = [ + { + name = "deadpool"; + packageId = "deadpool 0.13.1"; + usesDefaultFeatures = false; + features = [ "managed" ]; + } + { + name = "redis"; + packageId = "redis"; + usesDefaultFeatures = false; + features = [ "aio" ]; + } + ]; + devDependencies = [ + { + name = "redis"; + packageId = "redis"; + usesDefaultFeatures = false; + features = [ "tokio-comp" ]; + } + ]; + features = { + "acl" = [ "redis/acl" ]; + "ahash" = [ "redis/ahash" ]; + "bigdecimal" = [ "redis/bigdecimal" ]; + "bloom" = [ "redis/bloom" ]; + "cache-aio" = [ "redis/cache-aio" ]; + "cluster" = [ "redis/cluster-async" ]; + "cluster-async" = [ "cluster" ]; + "connection-manager" = [ "redis/connection-manager" ]; + "default" = [ "rt_tokio_1" ]; + "entra-id" = [ "redis/entra-id" ]; + "geospatial" = [ "redis/geospatial" ]; + "json" = [ "redis/json" ]; + "num-bigint" = [ "redis/num-bigint" ]; + "rt_smol_2" = [ "deadpool/rt_smol_2" "smol-comp" ]; + "rt_tokio_1" = [ "deadpool/rt_tokio_1" "tokio-comp" ]; + "rust_decimal" = [ "redis/rust_decimal" ]; + "script" = [ "redis/script" ]; + "search_unfinished" = [ "redis/search_unfinished" ]; + "sentinel" = [ "redis/sentinel" "dep:tokio" "tokio/sync" ]; + "serde" = [ "deadpool/serde" "dep:serde" ]; + "smol-comp" = [ "redis/smol-comp" ]; + "smol-native-tls-comp" = [ "redis/smol-native-tls-comp" ]; + "smol-rustls-comp" = [ "redis/smol-rustls-comp" ]; + "streams" = [ "redis/streams" ]; + "tls-native-tls" = [ "redis/tls-native-tls" ]; + "tls-rustls" = [ "redis/tls-rustls" ]; + "tls-rustls-insecure" = [ "redis/tls-rustls-insecure" ]; + "tls-rustls-webpki-roots" = [ "redis/tls-rustls-webpki-roots" ]; + "token-based-authentication" = [ "redis/token-based-authentication" ]; + "tokio-comp" = [ "redis/tokio-comp" ]; + "tokio-native-tls-comp" = [ "redis/tokio-native-tls-comp" ]; + "tokio-rustls-comp" = [ "redis/tokio-rustls-comp" ]; + "uuid" = [ "redis/uuid" ]; + "vector-sets" = [ "redis/vector-sets" ]; + }; + resolvedDefaultFeatures = [ "default" "rt_tokio_1" "tokio-comp" ]; + }; + "deadpool-runtime 0.1.4" = rec { crateName = "deadpool-runtime"; version = "0.1.4"; edition = "2021"; @@ -6708,6 +6870,39 @@ rec { "tokio_1" = [ "dep:tokio_1" ]; }; }; + "deadpool-runtime 0.3.1" = rec { + crateName = "deadpool-runtime"; + version = "0.3.1"; + edition = "2024"; + sha256 = "1bibxhdq77r8kgs56xrgn8jp3rz4rv3kq2aiimxg72yxn4gzcmr6"; + libName = "deadpool_runtime"; + authors = [ + "Michael P. Jung " + ]; + dependencies = [ + { + name = "tokio"; + packageId = "tokio"; + rename = "tokio_1"; + optional = true; + features = [ "time" "rt" ]; + } + ]; + devDependencies = [ + { + name = "tokio"; + packageId = "tokio"; + rename = "tokio_1"; + features = [ "rt" "macros" ]; + } + ]; + features = { + "async-std_1" = [ "dep:async-std_1" ]; + "smol_2" = [ "dep:smol_2_async-io" "dep:smol_2_blocking" "dep:smol_2_futures-lite" ]; + "tokio_1" = [ "dep:tokio_1" ]; + }; + resolvedDefaultFeatures = [ "tokio_1" ]; + }; "defmt" = rec { crateName = "defmt"; version = "1.1.0"; @@ -9657,6 +9852,10 @@ rec { usesDefaultFeatures = false; features = [ "toml" ]; } + { + name = "deadpool-redis"; + packageId = "deadpool-redis"; + } { name = "futures"; packageId = "futures"; @@ -9791,6 +9990,10 @@ rec { packageId = "chrono"; features = [ "serde" ]; } + { + name = "deadpool-redis"; + packageId = "deadpool-redis"; + } { name = "futures-lite"; packageId = "futures-lite"; @@ -9869,6 +10072,10 @@ rec { packageId = "serde_json"; features = [ "raw_value" ]; } + { + name = "sha2"; + packageId = "sha2 0.11.0"; + } { name = "tangled-axum"; packageId = "tangled-axum"; @@ -25919,6 +26126,137 @@ rec { }; resolvedDefaultFeatures = [ "aws_lc_rs" "crypto" "pem" ]; }; + "redis" = rec { + crateName = "redis"; + version = "1.7.0"; + edition = "2024"; + sha256 = "1vahmc2q8q3zy2157qrafkyjqq7zqj6dkpyrvnr54xkgk4dc9jra"; + dependencies = [ + { + name = "arcstr"; + packageId = "arcstr"; + } + { + name = "async-lock"; + packageId = "async-lock"; + optional = true; + } + { + name = "bytes"; + packageId = "bytes"; + optional = true; + } + { + name = "cfg-if"; + packageId = "cfg-if"; + optional = true; + } + { + name = "combine"; + packageId = "combine"; + usesDefaultFeatures = false; + features = [ "std" ]; + } + { + name = "futures-util"; + packageId = "futures-util"; + optional = true; + usesDefaultFeatures = false; + features = [ "std" "sink" ]; + } + { + name = "itoa"; + packageId = "itoa"; + } + { + name = "percent-encoding"; + packageId = "percent-encoding"; + } + { + name = "pin-project-lite"; + packageId = "pin-project-lite"; + optional = true; + } + { + name = "ryu"; + packageId = "ryu"; + } + { + name = "socket2"; + packageId = "socket2"; + target = { target, features }: (!(builtins.elem "wasm" target."family")); + features = [ "all" ]; + } + { + name = "tokio"; + packageId = "tokio"; + optional = true; + features = [ "sync" ]; + } + { + name = "tokio"; + packageId = "tokio"; + optional = true; + target = { target, features }: (("wasi" == target."os" or null) && ("p2" == target."env" or null)); + features = [ "sync" ]; + } + { + name = "tokio-util"; + packageId = "tokio-util"; + optional = true; + } + { + name = "url"; + packageId = "url"; + } + { + name = "xxhash-rust"; + packageId = "xxhash-rust"; + features = [ "xxh3" ]; + } + ]; + devDependencies = [ + { + name = "tokio"; + packageId = "tokio"; + features = [ "rt" "macros" "rt-multi-thread" "test-util" "time" "macros" "io-util" ]; + } + ]; + features = { + "ahash" = [ "dep:ahash" ]; + "aio" = [ "bytes" "dep:pin-project-lite" "dep:futures-util" "dep:tokio" "dep:tokio-util" "dep:async-lock" "tokio-util/codec" "combine/tokio" "dep:cfg-if" ]; + "bb8" = [ "dep:bb8" ]; + "bigdecimal" = [ "dep:bigdecimal" ]; + "bytes" = [ "dep:bytes" ]; + "cache-aio" = [ "aio" "dep:lru" ]; + "cluster" = [ "dep:crc16" "dep:rand" ]; + "cluster-async" = [ "aio" "cluster" "dep:log" "dep:futures-channel" ]; + "connection-manager" = [ "dep:arc-swap" "dep:futures-channel" "aio" "dep:backon" ]; + "default" = [ "acl" "streams" "geospatial" "script" "num-bigint" ]; + "entra-id" = [ "dep:azure_identity" "dep:azure_core" "dep:base64" "token-based-authentication" "tokio-comp" ]; + "hashbrown" = [ "dep:hashbrown" ]; + "json" = [ "dep:serde" "serde/derive" "dep:serde_json" ]; + "num-bigint" = [ "dep:num-bigint" ]; + "r2d2" = [ "dep:r2d2" ]; + "rust_decimal" = [ "dep:rust_decimal" ]; + "script" = [ "dep:sha1_smol" ]; + "sentinel" = [ "dep:log" "dep:rand" ]; + "smol-comp" = [ "aio" "dep:smol" "dep:smol-timeout" "dep:async-io" ]; + "smol-native-tls-comp" = [ "smol-comp" "dep:async-native-tls" "tls-native-tls" ]; + "smol-rustls-comp" = [ "smol-comp" "dep:futures-rustls" "tls-rustls" ]; + "tls-native-tls" = [ "dep:native-tls" ]; + "tls-rustls" = [ "dep:rustls" "rustls/std" "dep:rustls-native-certs" ]; + "tls-rustls-insecure" = [ "tls-rustls" ]; + "tls-rustls-webpki-roots" = [ "tls-rustls" "dep:webpki-roots" ]; + "token-based-authentication" = [ "dep:futures-util" "dep:log" "dep:backon" ]; + "tokio-comp" = [ "aio" "tokio/net" "tokio/rt" "tokio/time" ]; + "tokio-native-tls-comp" = [ "tokio-comp" "tls-native-tls" "dep:tokio-native-tls" ]; + "tokio-rustls-comp" = [ "tokio-comp" "tls-rustls" "dep:tokio-rustls" ]; + "uuid" = [ "dep:uuid" ]; + "vector-sets" = [ "dep:serde" "serde/derive" "dep:serde_json" ]; + }; + resolvedDefaultFeatures = [ "aio" "bytes" "tokio-comp" ]; + }; "redox_syscall" = rec { crateName = "redox_syscall"; version = "0.5.18"; @@ -26566,6 +26904,11 @@ rec { packageId = "serde_json"; optional = true; } + { + name = "serde_urlencoded"; + packageId = "serde_urlencoded"; + optional = true; + } { name = "sync_wrapper"; packageId = "sync_wrapper"; @@ -26720,7 +27063,7 @@ rec { "webpki-roots" = [ "dep:webpki-roots" ]; "zstd" = [ "tower-http/decompression-zstd" ]; }; - resolvedDefaultFeatures = [ "__rustls" "__rustls-aws-lc-rs" "__tls" "gzip" "http2" "json" "rustls" "stream" ]; + resolvedDefaultFeatures = [ "__rustls" "__rustls-aws-lc-rs" "__tls" "gzip" "http2" "json" "query" "rustls" "stream" ]; }; "resolv-conf" = rec { crateName = "resolv-conf"; @@ -33231,6 +33574,122 @@ rec { } ]; + }; + "tranquil-gate" = rec { + crateName = "tranquil-gate"; + version = "0.0.1"; + edition = "2024"; + crateBin = [ + { + name = "tranquil-gate"; + path = "src/main.rs"; + requiredFeatures = [ ]; + } + ]; + src = lib.cleanSourceWith { filter = sourceFilter; src = ../crates/tranquil-gate; }; + libName = "tranquil_gate"; + dependencies = [ + { + name = "anyhow"; + packageId = "anyhow"; + } + { + name = "axum"; + packageId = "axum"; + features = [ "macros" ]; + } + { + name = "base64"; + packageId = "base64"; + } + { + name = "bobbin-runtime"; + packageId = "bobbin-runtime"; + } + { + name = "bs58"; + packageId = "bs58"; + } + { + name = "clap"; + packageId = "clap"; + features = [ "derive" "env" ]; + } + { + name = "confique"; + packageId = "confique"; + usesDefaultFeatures = false; + features = [ "toml" ]; + } + { + name = "jacquard-axum"; + packageId = "jacquard-axum"; + } + { + name = "jacquard-common"; + packageId = "jacquard-common"; + } + { + name = "jacquard-identity"; + packageId = "jacquard-identity"; + features = [ "cache" ]; + } + { + name = "k256"; + packageId = "k256"; + features = [ "ecdsa" ]; + } + { + name = "reqwest"; + packageId = "reqwest 0.13.1"; + usesDefaultFeatures = false; + features = [ "rustls" "http2" "json" "gzip" "stream" "query" ]; + } + { + name = "serde"; + packageId = "serde"; + features = [ "derive" ]; + } + { + name = "serde_json"; + packageId = "serde_json"; + features = [ "raw_value" ]; + } + { + name = "tangled-axum"; + packageId = "tangled-axum"; + } + { + name = "tokio"; + packageId = "tokio"; + features = [ "macros" "rt-multi-thread" "time" "signal" "io-util" "net" "sync" ]; + } + { + name = "tracing"; + packageId = "tracing"; + } + { + name = "tracing-subscriber"; + packageId = "tracing-subscriber"; + features = [ "env-filter" "fmt" "json" ]; + } + ]; + devDependencies = [ + { + name = "tempfile"; + packageId = "tempfile"; + } + { + name = "tower"; + packageId = "tower"; + features = [ "util" "limit" "load-shed" ]; + } + { + name = "wiremock"; + packageId = "wiremock"; + } + ]; + }; "triomphe" = rec { crateName = "triomphe"; @@ -37730,7 +38189,7 @@ rec { } { name = "deadpool"; - packageId = "deadpool"; + packageId = "deadpool 0.12.3"; } { name = "futures"; @@ -38004,6 +38463,19 @@ rec { features = { }; }; + "xxhash-rust" = rec { + crateName = "xxhash-rust"; + version = "0.8.18"; + edition = "2018"; + sha256 = "1xlaih71ys27aiqw79mz0gz0pscdw75sklw0mc103in74ybb3qdf"; + libName = "xxhash_rust"; + authors = [ + "Douman " + ]; + features = { + }; + resolvedDefaultFeatures = [ "xxh3" ]; + }; "yasna 0.5.2" = rec { crateName = "yasna"; version = "0.5.2"; -- 2.51.2