use std::path::Path; use tokio::process::Command; pub const MAX_REPO_PATH_LEN: usize = 512; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum GitService { UploadPack, ReceivePack, UploadArchive, } impl GitService { pub const fn as_subcommand(self) -> &'static str { match self { Self::UploadPack => "upload-pack", Self::ReceivePack => "receive-pack", Self::UploadArchive => "upload-archive", } } pub const fn is_write(self) -> bool { matches!(self, Self::ReceivePack) } } impl std::str::FromStr for GitService { type Err = (); fn from_str(s: &str) -> Result { match s { "upload-pack" => Ok(Self::UploadPack), "receive-pack" => Ok(Self::ReceivePack), "upload-archive" => Ok(Self::UploadArchive), _ => Err(()), } } } pub fn parse_git_command(cmd: &str) -> Option<(GitService, &str)> { let (bin, path) = cmd.split_once(' ')?; let service = match bin { "git-upload-pack" => GitService::UploadPack, "git-receive-pack" => GitService::ReceivePack, "git-upload-archive" => GitService::UploadArchive, _ => return None, }; Some((service, path)) } pub fn parse_repo_path(path: &str) -> Option<(&str, &str)> { let path = path.trim().trim_matches('\'').trim_matches('/'); if path.is_empty() || path.len() > MAX_REPO_PATH_LEN { return None; } for b in path.bytes() { let ok = b.is_ascii_lowercase() || b.is_ascii_digit() || matches!(b, b'.' | b'-' | b'_' | b'/'); if !ok { return None; } } let path = path.trim_end_matches(".git").trim_end_matches('/'); if path.is_empty() { return None; } let (owner, repo) = path.split_once('/')?; if owner.is_empty() || repo.is_empty() { return None; } if owner == "." || owner == ".." || repo.starts_with("..") || repo.contains("/../") { return None; } Some((owner, repo)) } pub fn sanitize_for_log(input: &str) -> String { let mut out = String::with_capacity(input.len().min(256)); for &b in input.as_bytes().iter().take(256) { match b { 0x00..=0x1f | 0x7f => out.push('?'), 0x80..=0x9f => out.push('?'), _ => out.push(b as char), } } out } pub fn build_git_advertise_command(service: GitService, bare: &Path) -> Command { let mut cmd = Command::new("git"); cmd.current_dir(bare) .arg(service.as_subcommand()) .arg("--advertise-refs") // receive-pack rejects a missing even when GIT_DIR is set, // so the path must be passed explicitly. .arg(bare) .env("GIT_DIR", bare) .env("GIT_TERMINAL_PROMPT", "0"); apply_hardened_env(&mut cmd); cmd } pub fn apply_hardened_env(cmd: &mut Command) { cmd.env("GIT_CONFIG_NOSYSTEM", "1") .env("GIT_NO_REPLACE_OBJECTS", "1"); #[cfg(unix)] cmd.env("GIT_CONFIG_GLOBAL", "/dev/null") .env("GIT_CONFIG_SYSTEM", "/dev/null"); }