use super::{Protocol, Reference, References};
use git_zero_core::{
Error, ObjectFormat, ObjectId, Result,
io::{SliceWriter, Write},
pack,
pktline::{self, Packet},
};
pub struct SmartAdvertisement<'a> {
bytes: &'a [u8],
body_offset: usize,
head: Option<ObjectId>,
capabilities: &'a [u8],
format: Option<ObjectFormat>,
}
impl<'a> SmartAdvertisement<'a> {
pub fn parse(data: &'a [u8]) -> Result<Self> {
let body = advertisement_body(data)?;
let mut first = body;
let capabilities = match pktline::read(&mut first)? {
Some(Packet::Data(line)) => line.splitn(2, |&b| b == 0).nth(1).unwrap_or_default(),
_ => &[],
};
let mut head = None;
let mut refs = References {
protocol: Protocol::Smart,
bytes: body,
failed: false,
format: None,
};
for entry in &mut refs {
let entry = entry?;
if entry.name == "HEAD" && head.replace(entry.id).is_some() {
return Err(Error::InvalidReference);
}
}
Ok(Self {
bytes: data,
body_offset: data.len() - body.len(),
head,
capabilities,
format: refs.format,
})
}
pub fn head(&self) -> Result<ObjectId> {
self.head.ok_or(Error::ObjectNotFound)
}
pub fn resolve_reference(&self, name: &str) -> Result<ObjectId> {
if name == "HEAD" {
return self.head();
}
git_zero_core::repository::validate_ref_name(name)?;
let mut found = None;
for entry in self.references() {
let entry = entry?;
if entry.name == name && found.replace(entry.id).is_some() {
return Err(Error::InvalidReference);
}
}
found.ok_or(Error::ObjectNotFound)
}
pub fn object_format(&self) -> Option<ObjectFormat> {
self.format
}
pub fn supports_shallow(&self) -> bool {
self.supports("shallow")
}
pub fn supports(&self, capability: &str) -> bool {
self.capabilities
.split(|b| b.is_ascii_whitespace())
.any(|flag| flag == capability.as_bytes())
}
pub fn fetch_options(&self, shallow_depth: Option<u32>) -> FetchOptions {
FetchOptions {
shallow_depth: shallow_depth.filter(|_| self.supports_shallow()),
ofs_delta: self.supports("ofs-delta"),
no_progress: self.supports("no-progress"),
}
}
pub fn validate_want(&self, id: ObjectId) -> Result<()> {
if self.format.is_some_and(|format| format != id.format()) {
return Err(Error::InvalidRequest);
}
if self.supports("allow-tip-sha1-in-want") || self.supports("allow-reachable-sha1-in-want")
{
return Ok(());
}
for reference in self.references() {
if reference?.id == id {
return Ok(());
}
}
Err(Error::InvalidRequest)
}
pub fn references(&self) -> References<'a> {
References {
protocol: Protocol::Smart,
bytes: &self.bytes[self.body_offset..],
failed: false,
format: self.format,
}
}
}
pub(super) fn advertisement_body(mut data: &[u8]) -> Result<&[u8]> {
if !matches!(
pktline::read(&mut data)?,
Some(Packet::Data(b"# service=git-upload-pack\n"))
) || !matches!(pktline::read(&mut data)?, Some(Packet::Flush))
{
return Err(Error::InvalidPacket);
}
Ok(data)
}
pub(super) fn next_reference<'a>(
bytes: &mut &'a [u8],
format: &mut Option<ObjectFormat>,
) -> Result<Option<Reference<'a>>> {
loop {
match pktline::read(bytes)? {
Some(Packet::Data(line)) => {
let mut parser = References {
protocol: Protocol::Smart,
bytes: &[],
failed: false,
format: *format,
};
let reference = parser.parse(line)?;
*format = parser.format;
if reference.id.is_zero() && reference.name == "capabilities^{}" {
continue;
}
if reference.name != "HEAD" {
git_zero_core::repository::validate_ref_name(
reference.name.strip_suffix("^{}").unwrap_or(reference.name),
)?;
}
return Ok(Some(reference));
}
Some(Packet::Flush) if bytes.is_empty() => return Ok(None),
_ => return Err(Error::InvalidPacket),
}
}
}
pub fn read_smart_reference<'a>(
data: &'a [u8],
offset: &mut usize,
) -> Result<Option<Reference<'a>>> {
if *offset == data.len() && *offset != 0 {
return Ok(None);
}
let body = advertisement_body(data)?;
let mut first = body;
let mut parser = References {
protocol: Protocol::Smart,
bytes: &[],
failed: false,
format: None,
};
if let Some(Packet::Data(line)) = pktline::read(&mut first)? {
parser.parse(line)?;
}
let mut bytes = if *offset == 0 {
body
} else {
data.get(*offset..).ok_or(Error::InvalidPacket)?
};
let entry = next_reference(&mut bytes, &mut parser.format)?;
*offset = data.len() - bytes.len();
Ok(entry)
}
#[derive(Clone, Copy, Debug, Default)]
pub struct FetchOptions {
pub shallow_depth: Option<u32>,
pub ofs_delta: bool,
pub no_progress: bool,
}
pub fn write_fetch_request(
wants: &[ObjectId],
options: FetchOptions,
output: &mut impl Write,
) -> Result<()> {
write_fetch_request_iter(wants.iter().copied(), options, output)
}
pub fn write_fetch_request_iter(
wants: impl Iterator<Item = ObjectId> + Clone,
options: FetchOptions,
output: &mut impl Write,
) -> Result<()> {
let format = wants.clone().next().ok_or(Error::InvalidRequest)?.format();
if options.shallow_depth == Some(0) || wants.clone().any(|want| want.format() != format) {
return Err(Error::InvalidRequest);
}
for (position, want) in wants.clone().enumerate() {
if wants
.clone()
.take(position)
.any(|previous| previous == want)
{
continue;
}
let mut storage = [0; 128];
let mut line = SliceWriter::new(&mut storage);
line.write_all(b"want ")?;
line.write_all(&want.to_hex())?;
if position == 0 {
if format == ObjectFormat::Sha256 {
line.write_all(b" ")?;
line.write_all(format.capability())?;
}
if options.ofs_delta {
line.write_all(b" ofs-delta")?;
}
if options.no_progress {
line.write_all(b" no-progress")?;
}
if options.shallow_depth.is_some() {
line.write_all(b" shallow")?;
}
}
line.write_all(b"\n")?;
pktline::write(output, line.as_bytes())?;
}
if let Some(depth) = options.shallow_depth {
use core::fmt::Write as _;
let mut storage = [0; 32];
let mut line = SliceWriter::new(&mut storage);
writeln!(&mut line, "deepen {depth}").map_err(|_| Error::BufferTooSmall)?;
pktline::write(output, line.as_bytes())?;
}
output.write_all(b"0000")?;
pktline::write(output, b"done\n")
}
pub fn read_pack_response(bytes: &[u8]) -> Result<&[u8]> {
read_pack_response_with_format(bytes, ObjectFormat::Sha1)
}
pub fn read_pack_response_with_format(mut bytes: &[u8], format: ObjectFormat) -> Result<&[u8]> {
loop {
match pktline::read(&mut bytes)? {
Some(Packet::Data(b"NAK\n")) => break,
Some(Packet::Data(line)) if line.starts_with(b"shallow ") => {
if ObjectId::from_hex(&line.strip_suffix(b"\n").unwrap_or(line)[8..])?.format()
!= format
{
return Err(Error::InvalidRequest);
}
}
Some(Packet::Flush) => {}
_ => return Err(Error::InvalidPacket),
}
}
pack::validate_with_format(bytes, format)?;
Ok(bytes)
}