use std::collections::{HashSet, VecDeque};
use super::{AdvertisedRef, Result, ServeError};
use crate::config::ConfigSet;
use crate::objects::{ObjectId, ObjectKind};
use crate::repo::Repository;
use crate::rev_list::ObjectFilter;
use crate::upload_filter::{validate_upload_filter_request, UploadFilterError};
pub(crate) struct UploadPackPolicy {
pub allow_filter: bool,
pub allow_tip_sha1_in_want: bool,
pub allow_reachable_sha1_in_want: bool,
pub ref_in_want: bool,
pub allow_sideband_all: bool,
}
impl UploadPackPolicy {
pub fn from_config(config: &ConfigSet) -> Self {
let flag = |key: &str| config.get_bool(key).and_then(|v| v.ok()).unwrap_or(false);
Self {
allow_filter: flag("uploadpack.allowfilter"),
allow_tip_sha1_in_want: flag("uploadpack.allowtipsha1inwant"),
allow_reachable_sha1_in_want: flag("uploadpack.allowreachablesha1inwant"),
ref_in_want: flag("uploadpack.allowrefinwant"),
allow_sideband_all: flag("uploadpack.allowsidebandall"),
}
}
pub fn v0_capability_tokens(&self) -> Vec<&'static str> {
let mut caps = vec!["shallow", "deepen-since", "deepen-not", "deepen-relative"];
if self.allow_filter {
caps.push("filter");
}
if self.allow_tip_sha1_in_want {
caps.push("allow-tip-sha1-in-want");
}
if self.allow_reachable_sha1_in_want {
caps.push("allow-reachable-sha1-in-want");
}
caps
}
pub fn v2_fetch_features(&self) -> Vec<&'static str> {
let mut f = vec!["shallow"];
if self.allow_filter {
f.push("filter");
}
if self.ref_in_want {
f.push("ref-in-want");
}
if self.allow_sideband_all {
f.push("sideband-all");
}
f
}
pub fn parse_filter(&self, config: &ConfigSet, spec: &str) -> Result<ObjectFilter> {
if !self.allow_filter {
return Err(ServeError::Protocol(
"filter not supported (uploadpack.allowFilter is false)".into(),
));
}
validate_upload_filter_request(config, spec).map_err(ServeError::UploadFilter)?;
ObjectFilter::parse(spec)
.map_err(|e| ServeError::UploadFilter(UploadFilterError::InvalidFilterSpec(e)))
}
}
pub(crate) fn resolve_want_ref(
repo: &Repository,
refs: &[AdvertisedRef],
hidden: &[String],
name: &str,
) -> Result<ObjectId> {
if matches!(name, "HEAD" | "refs/HEAD") {
let head = super::head_info(repo);
return head
.oid
.ok_or_else(|| ServeError::Protocol("unknown ref HEAD".into()));
}
let full = if name.starts_with("refs/") {
name.to_owned()
} else {
format!("refs/{name}")
};
if crate::hide_refs::ref_is_hidden(name, &full, hidden) {
return Err(ServeError::Protocol(format!("unknown ref {name}")));
}
let allowed: HashSet<&str> = refs.iter().map(|r| r.name.as_str()).collect();
if !allowed.contains(full.as_str()) && !allowed.contains(name) {
return Err(ServeError::Protocol(format!("unknown ref {name}")));
}
crate::refs::resolve_ref(repo.git_dir(), &full)
.or_else(|_| crate::refs::resolve_ref(repo.git_dir(), name))
.map_err(|_| ServeError::Protocol(format!("unknown ref {name}")))
}
pub(crate) fn validate_wants_filtered(repo: &Repository, wants: &[ObjectId]) -> Result<()> {
for w in wants {
if !repo.odb().contains(w)? {
return Err(ServeError::NotOurRef(*w));
}
}
Ok(())
}
pub(crate) fn validate_wants(
repo: &Repository,
refs: &[AdvertisedRef],
head: Option<ObjectId>,
wants: &[ObjectId],
policy: &UploadPackPolicy,
stateless_rpc: bool,
) -> Result<()> {
if policy.allow_reachable_sha1_in_want {
let reachable = reachable_from_repository_tips(repo)?;
for w in wants {
if !reachable.contains(w) {
return Err(ServeError::NotOurRef(*w));
}
}
return Ok(());
}
let advertised = advertised_object_ids(refs, head);
let extra: Vec<ObjectId> = wants
.iter()
.copied()
.filter(|w| !advertised.contains(w))
.collect();
if extra.is_empty() {
return Ok(());
}
if stateless_rpc {
validate_stateless_extra_wants(repo, &extra)?;
return Ok(());
}
if policy.allow_tip_sha1_in_want {
let tips = repository_tip_commits(repo)?;
for w in &extra {
if !tips.contains(w) {
return Err(ServeError::NotOurRef(*w));
}
}
return Ok(());
}
Err(ServeError::NotOurRef(extra[0]))
}
fn advertised_object_ids(refs: &[AdvertisedRef], head: Option<ObjectId>) -> HashSet<ObjectId> {
let mut allowed: HashSet<ObjectId> = refs
.iter()
.flat_map(|r| std::iter::once(r.oid).chain(r.peeled))
.collect();
if let Some(h) = head {
allowed.insert(h);
}
allowed
}
fn validate_stateless_extra_wants(repo: &Repository, wants: &[ObjectId]) -> Result<()> {
let tips = repository_tip_commits(repo)?;
let ref_objects = repository_ref_object_ids(repo)?;
for w in wants {
if ref_objects.contains(w) {
continue;
}
if !object_reachable_from_tip_commits(repo, *w, &tips)? {
return Err(ServeError::NotOurRef(*w));
}
}
Ok(())
}
fn repository_ref_object_ids(repo: &Repository) -> Result<HashSet<ObjectId>> {
let mut ids = HashSet::new();
for (_, oid) in crate::refs::list_refs(repo.git_dir(), "refs/")? {
ids.insert(oid);
}
if let Ok(head) = crate::refs::resolve_ref(repo.git_dir(), "HEAD") {
ids.insert(head);
}
Ok(ids)
}
fn object_reachable_from_tip_commits(
repo: &Repository,
want: ObjectId,
tips: &HashSet<ObjectId>,
) -> Result<bool> {
if tips.contains(&want) {
return Ok(true);
}
if !repo.odb().contains(&want)? {
return Ok(false);
}
let Ok(obj) = repo.odb().read(&want) else {
return Ok(false);
};
match obj.kind {
ObjectKind::Commit => commit_is_ancestor_of_some_tip(repo, want, tips),
ObjectKind::Tag => {
let Ok(tag) = crate::objects::parse_tag(&obj.data) else {
return Ok(false);
};
reachable_object_from_tips(repo, tag.object, tips)
}
ObjectKind::Tree | ObjectKind::Blob => object_reachable_in_tip_closure(repo, want, tips),
}
}
fn reachable_object_from_tips(
repo: &Repository,
object: ObjectId,
tips: &HashSet<ObjectId>,
) -> Result<bool> {
if tips.contains(&object) {
return Ok(true);
}
if !repo.odb().contains(&object)? {
return Ok(false);
}
let Ok(obj) = repo.odb().read(&object) else {
return Ok(false);
};
match obj.kind {
ObjectKind::Commit => commit_is_ancestor_of_some_tip(repo, object, tips),
ObjectKind::Tree | ObjectKind::Blob => object_reachable_in_tip_closure(repo, object, tips),
ObjectKind::Tag => {
let Ok(tag) = crate::objects::parse_tag(&obj.data) else {
return Ok(false);
};
reachable_object_from_tips(repo, tag.object, tips)
}
}
}
fn commit_is_ancestor_of_some_tip(
repo: &Repository,
want: ObjectId,
tips: &HashSet<ObjectId>,
) -> Result<bool> {
for tip in tips {
if want == *tip || crate::merge_base::is_ancestor(repo, want, *tip)? {
return Ok(true);
}
}
Ok(false)
}
fn object_reachable_in_tip_closure(
repo: &Repository,
target: ObjectId,
tips: &HashSet<ObjectId>,
) -> Result<bool> {
let mut seen = HashSet::new();
let mut queue: VecDeque<ObjectId> = tips.iter().copied().collect();
while let Some(oid) = queue.pop_front() {
if oid == target {
return Ok(true);
}
if !seen.insert(oid) {
continue;
}
let Ok(obj) = repo.odb().read(&oid) else {
continue;
};
match obj.kind {
ObjectKind::Commit => {
if let Ok(c) = crate::objects::parse_commit(&obj.data) {
queue.extend(c.parents);
queue.push_back(c.tree);
}
}
ObjectKind::Tree => {
if let Ok(entries) = crate::objects::parse_tree(&obj.data) {
for entry in entries {
queue.push_back(entry.oid);
}
}
}
ObjectKind::Tag => {
if let Ok(tag) = crate::objects::parse_tag(&obj.data) {
queue.push_back(tag.object);
}
}
ObjectKind::Blob => {}
}
}
Ok(false)
}
fn repository_tip_commits(repo: &Repository) -> Result<HashSet<ObjectId>> {
let mut tips = HashSet::new();
for (_, oid) in crate::refs::list_refs(repo.git_dir(), "refs/")? {
if let Ok(p) = repo.odb().peel_tags(oid).map(|(p, _)| p) {
tips.insert(p);
}
}
if let Ok(head) = crate::refs::resolve_ref(repo.git_dir(), "HEAD") {
if let Ok(p) = repo.odb().peel_tags(head).map(|(p, _)| p) {
tips.insert(p);
}
}
Ok(tips)
}
fn reachable_from_repository_tips(repo: &Repository) -> Result<HashSet<ObjectId>> {
let tips = repository_tip_commits(repo)?;
let mut seen = HashSet::new();
let mut q: VecDeque<ObjectId> = tips.iter().copied().collect();
while let Some(oid) = q.pop_front() {
if !seen.insert(oid) {
continue;
}
let Ok(obj) = repo.odb().read(&oid) else {
continue;
};
match obj.kind {
ObjectKind::Commit => {
if let Ok(c) = crate::objects::parse_commit(&obj.data) {
q.extend(c.parents);
q.push_back(c.tree);
}
}
ObjectKind::Tree => {
if let Ok(entries) = crate::objects::parse_tree(&obj.data) {
for entry in entries {
q.push_back(entry.oid);
}
}
}
ObjectKind::Tag => {
if let Ok(tag) = crate::objects::parse_tag(&obj.data) {
q.push_back(tag.object);
}
}
ObjectKind::Blob => {}
}
}
Ok(seen)
}