use std::path::{Path, PathBuf}; use crate::error::{GitError, GitResult}; use crate::pb::RepositoryHeader; pub struct GitBare { pub bare_dir: PathBuf, } impl GitBare { pub fn gix_repo(&self) -> GitResult { gix::open(&self.bare_dir) .map_err(|e| GitError::Internal(format!("failed to open gix repository: {e}"))) } pub fn from_repository_header(header: &RepositoryHeader) -> GitResult { let storage_path = header.storage_path.trim(); let relative_path = header.relative_path.trim(); let storage_name = header.storage_name.trim(); let _ = storage_name; // reserved for future sharding logic // Build base path: storage_path if given, else relative_path alone let base = if !storage_path.is_empty() { let p = Path::new(storage_path); if !p.is_absolute() { return Err(GitError::InvalidArgument( "storage_path must be an absolute path".into(), )); } PathBuf::from(p) } else if !relative_path.is_empty() { // relative_path alone is rejected unless absolute return Err(GitError::InvalidArgument( "relative_path requires storage_path to be set".into(), )); } else { return Err(GitError::InvalidArgument("empty repository path".into())); }; // Join relative_path if provided let bare_dir = if !relative_path.is_empty() && !storage_path.is_empty() { let candidate = base.join(relative_path); // Canonicalize to resolve any `..` / symlinks, then check still under base let canonical = candidate .canonicalize() .unwrap_or_else(|_| candidate.clone()); // Path traversal check: canonical resolved dir must start with base let base_canon = base.canonicalize().unwrap_or_else(|_| base.clone()); if !canonical.starts_with(&base_canon) { return Err(GitError::InvalidArgument(format!( "path traversal detected: {relative_path} escapes storage root" ))); } canonical } else if !storage_path.is_empty() { base.canonicalize().unwrap_or(base) } else { return Err(GitError::InvalidArgument("empty repository path".into())); }; // Validate bare_dir exists, is a directory, and is readable if !bare_dir.exists() { return Err(GitError::RepoNotFound); } if !bare_dir.is_dir() { return Err(GitError::InvalidArgument(format!( "not a directory: {}", bare_dir.display() ))); } // Accept either bare repos (HEAD file) or non-bare (HEAD + .git) let head_path = bare_dir.join("HEAD"); if !head_path.exists() { // Maybe it's a non-bare repo let git_dir = bare_dir.join(".git"); if git_dir.is_dir() && git_dir.join("HEAD").exists() { return Ok(Self { bare_dir: git_dir }); } return Err(GitError::NotBareRepository); } Ok(Self { bare_dir }) } /// Detect the repository's object format (SHA-1 or SHA-256). pub fn object_format(&self) -> crate::pb::ObjectFormat { let repo = self.gix_repo().ok(); let kind = repo .map(|r| r.object_hash()) .unwrap_or(gix::hash::Kind::Sha1); match kind { gix::hash::Kind::Sha1 => crate::pb::ObjectFormat::Sha1, gix::hash::Kind::Sha256 => crate::pb::ObjectFormat::Sha256, _ => crate::pb::ObjectFormat::Unspecified, } } /// Convert a hex object id to a protobuf Oid. /// /// `Oid.value` is the binary hash bytes, while `Oid.hex` keeps the printable /// lowercase representation for clients that prefer string IDs. pub fn oid_to_pb(&self, hex: impl Into) -> crate::pb::Oid { let hex = hex.into().to_lowercase(); let format = self.object_format(); crate::pb::Oid { value: crate::oid::hex_to_bytes(&hex).unwrap_or_default(), hex, format: format as i32, } } }