use bstr::{BString, ByteSlice};
use gix_hash::{Kind, ObjectId};
use crate::capabilities::{self, Capabilities};
use crate::error::{Error, Result};
pub const NO_REFS_PSEUDO_REF: &str = "capabilities^{}";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RemoteRef {
pub name: BString,
pub oid: ObjectId,
}
impl RemoteRef {
pub fn new(name: impl Into<BString>, oid: ObjectId) -> Self {
RemoteRef {
name: name.into(),
oid,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Advertisement {
pub refs: Vec<RemoteRef>,
pub capabilities: Capabilities,
pub hash_kind: Kind,
}
impl Advertisement {
pub fn oid_of(&self, name: impl AsRef<[u8]>) -> ObjectId {
let name = name.as_ref();
self.refs
.iter()
.find(|r| r.name == name)
.map(|r| r.oid)
.unwrap_or_else(|| ObjectId::null(self.hash_kind))
}
pub fn has_ref(&self, name: impl AsRef<[u8]>) -> bool {
let name = name.as_ref();
self.refs.iter().any(|r| r.name == name)
}
}
pub fn parse(payloads: &[Vec<u8>]) -> Result<Advertisement> {
let mut refs: Vec<RemoteRef> = Vec::new();
let mut caps = Capabilities::default();
let mut seen_first = false;
let mut oid_width: Option<usize> = None;
for payload in payloads {
let line = chomp(payload);
if line == b"version 1" {
continue;
}
if let Some(msg) = line.strip_prefix(b"ERR ".as_slice()) {
return Err(Error::Remote(msg.as_bstr().to_string()));
}
let (ref_part, caps_part) = capabilities::split(line);
if let (false, Some(raw)) = (seen_first, caps_part) {
caps = Capabilities::parse_bytes(raw)?;
}
seen_first = true;
if ref_part.starts_with(b"shallow ") {
continue;
}
let Some(space) = ref_part.find_byte(b' ') else {
return Err(Error::protocol(format!(
"ref advertisement line has no space: {:?}",
ref_part.as_bstr()
)));
};
let (oid_hex, name) = (&ref_part[..space], &ref_part[space + 1..]);
oid_width.get_or_insert(oid_hex.len());
if name == NO_REFS_PSEUDO_REF.as_bytes() {
continue;
}
let oid = ObjectId::from_hex(oid_hex).map_err(|e| {
Error::protocol(format!(
"bad object id {:?} for ref {:?}: {e}",
oid_hex.as_bstr(),
name.as_bstr()
))
})?;
refs.push(RemoteRef {
name: name.into(),
oid,
});
}
let hash_kind = match caps.value("object-format") {
Some(name) => object_format_kind(name)?,
None => match oid_width {
Some(64) => Kind::Sha256,
_ => Kind::Sha1,
},
};
for r in &refs {
if r.oid.kind() != hash_kind {
return Err(Error::protocol(format!(
"remote advertised {:?} as a {:?} oid but the repository is {hash_kind:?}",
r.name.as_bstr(),
r.oid.kind()
)));
}
}
Ok(Advertisement {
refs,
capabilities: caps,
hash_kind,
})
}
pub fn lines(refs: &[RemoteRef], caps: &[String], hash_kind: Kind) -> Vec<Vec<u8>> {
let mut out = Vec::with_capacity(refs.len().max(1));
for (i, r) in refs.iter().enumerate() {
let mut line = format!("{} ", r.oid.to_hex()).into_bytes();
line.extend_from_slice(r.name.as_slice());
if i == 0 {
capabilities::attach(&mut line, caps);
}
out.push(line);
}
if out.is_empty() {
let mut line = format!(
"{} {NO_REFS_PSEUDO_REF}",
ObjectId::null(hash_kind).to_hex()
)
.into_bytes();
capabilities::attach(&mut line, caps);
out.push(line);
}
out
}
pub fn object_format_name(kind: Kind) -> Result<&'static str> {
match kind {
Kind::Sha1 => Ok("sha1"),
Kind::Sha256 => Ok("sha256"),
other => Err(Error::protocol(format!(
"this build has no `object-format` name for {other:?}"
))),
}
}
pub fn object_format_kind(name: &str) -> Result<Kind> {
match name {
"sha1" => Ok(Kind::Sha1),
"sha256" => Ok(Kind::Sha256),
other => Err(Error::protocol(format!(
"peer advertised an unknown object-format {other:?}"
))),
}
}
pub(crate) fn chomp(line: &[u8]) -> &[u8] {
line.strip_suffix(b"\n").unwrap_or(line)
}