2ae9196
ZhenYi
feat(lib/git): add HTTP handler tree with routes and errors across analysis, archives, blobs, branches, commits, conflicts, content, diffs, hooks, lfs, logs, maintenance, packs, patches, pipelines, refs, remotes, repos, signatures, submodules, tags, trees
2ae9196
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<Self, Self::Err> {
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))
}
fn strip_apostrophes(s: &str) -> &str {
s.trim().trim_matches('\'')
}
pub fn parse_repo_path(path: &str) -> Option<(&str, &str)> {
let path = strip_apostrophes(path).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")
.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");
}