80f9e01
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))
}
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 <git-dir> 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");
}