michal/tit
Browse tree · Show commit · Download archive
Blob: src/git/upload_pack.rs
use std::path::Path;
use gix::hash::{Kind, ObjectId};
use thiserror::Error;
use super::packetline::{
Packet, PacketLineError, decode, encode_data, encode_flush, encode_sideband,
};
use super::repository::{GitReference, GitRepository, GitRepositoryError};
const AGENT: &str = concat!("tit/", env!("CARGO_PKG_VERSION"));
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum ProtocolVersion {
V0,
V1,
V2,
}
pub(crate) struct UploadPack {
repository: GitRepository,
}
impl UploadPack {
pub(crate) fn open(path: &Path) -> Result<Self, UploadPackError> {
Ok(Self {
repository: GitRepository::open(path)?,
})
}
pub(crate) fn advertisement(
&self,
version: ProtocolVersion,
http: bool,
) -> Result<Vec<u8>, UploadPackError> {
let mut output = Vec::new();
if http && version != ProtocolVersion::V2 {
encode_data(b"# service=git-upload-pack\n", &mut output)?;
encode_flush(&mut output);
}
match version {
ProtocolVersion::V0 => self.advertise_v0_or_v1(&mut output, false)?,
ProtocolVersion::V1 => self.advertise_v0_or_v1(&mut output, true)?,
ProtocolVersion::V2 => self.advertise_v2(&mut output)?,
}
Ok(output)
}
pub(crate) fn respond(
&self,
version: ProtocolVersion,
request: &[u8],
) -> Result<Vec<u8>, UploadPackError> {
let packets = decode(request)?;
match version {
ProtocolVersion::V0 | ProtocolVersion::V1 => self.respond_v1(&packets),
ProtocolVersion::V2 => self.respond_v2(&packets),
}
}
fn advertise_v0_or_v1(
&self,
output: &mut Vec<u8>,
version_packet: bool,
) -> Result<(), UploadPackError> {
if version_packet {
encode_data(b"version 1\n", output)?;
}
let references = self.repository.references()?;
let capabilities = format!(
"agent={AGENT} object-format={}",
hash_name(self.repository.object_format())
);
if references.is_empty() {
let zero = "0".repeat(self.repository.object_format().len_in_hex());
encode_data(
format!("{zero} capabilities^{{}}\0{capabilities}\n").as_bytes(),
output,
)?;
} else {
for (index, reference) in references.iter().enumerate() {
let suffix = if index == 0 {
format!("\0{capabilities}")
} else {
String::new()
};
encode_data(
format!(
"{} {}{suffix}\n",
reference.target,
String::from_utf8_lossy(&reference.name)
)
.as_bytes(),
output,
)?;
if let Some(peeled) = reference.peeled {
encode_data(
format!(
"{peeled} {}^{{}}\n",
String::from_utf8_lossy(&reference.name)
)
.as_bytes(),
output,
)?;
}
}
}
encode_flush(output);
Ok(())
}
fn advertise_v2(&self, output: &mut Vec<u8>) -> Result<(), UploadPackError> {
for capability in [
"version 2\n".to_owned(),
format!("agent={AGENT}\n"),
format!(
"object-format={}\n",
hash_name(self.repository.object_format())
),
"ls-refs=symrefs peel\n".to_owned(),
"fetch=wait-for-done\n".to_owned(),
] {
encode_data(capability.as_bytes(), output)?;
}
encode_flush(output);
Ok(())
}
fn respond_v1(&self, packets: &[Packet]) -> Result<Vec<u8>, UploadPackError> {
let mut wants = Vec::new();
let mut haves = Vec::new();
let mut done = false;
for packet in packets {
let Packet::Data(line) = packet else {
continue;
};
let line = trim_line(line);
if let Some(value) = line.strip_prefix(b"want ") {
let id = value.split(|byte| *byte == b' ').next().unwrap_or_default();
wants.push(self.parse_id(id)?);
} else if let Some(value) = line.strip_prefix(b"have ") {
haves.push(self.parse_id(value)?);
} else if line == b"done" {
done = true;
} else {
return Err(UploadPackError::UnsupportedRequest);
}
}
if !done || wants.is_empty() {
return Err(UploadPackError::IncompleteNegotiation);
}
let pack = self.repository.make_pack(&wants, &haves)?;
let mut output = Vec::with_capacity(pack.len() + 8);
encode_data(b"NAK\n", &mut output)?;
output.extend_from_slice(&pack);
Ok(output)
}
fn respond_v2(&self, packets: &[Packet]) -> Result<Vec<u8>, UploadPackError> {
let delimiter = packets
.iter()
.position(|packet| *packet == Packet::Delimiter)
.ok_or(UploadPackError::UnsupportedRequest)?;
let command = command_name(&packets[..delimiter], self.repository.object_format())?;
let arguments = &packets[delimiter + 1..];
match command {
b"ls-refs" => self.respond_ls_refs(arguments),
b"fetch" => self.respond_fetch(arguments),
_ => Err(UploadPackError::UnsupportedRequest),
}
}
fn respond_ls_refs(&self, packets: &[Packet]) -> Result<Vec<u8>, UploadPackError> {
let mut symrefs = false;
let mut peel = false;
let mut prefixes = Vec::new();
for packet in packets {
match packet {
Packet::Data(line) => {
let line = trim_line(line);
if line == b"symrefs" {
symrefs = true;
} else if line == b"peel" {
peel = true;
} else if let Some(prefix) = line.strip_prefix(b"ref-prefix ") {
prefixes.push(prefix.to_vec());
} else {
return Err(UploadPackError::UnsupportedRequest);
}
}
Packet::Flush => {}
Packet::Delimiter | Packet::ResponseEnd => {
return Err(UploadPackError::UnsupportedRequest);
}
}
}
let mut output = Vec::new();
for reference in self.repository.references()? {
if !prefixes.is_empty()
&& !prefixes
.iter()
.any(|prefix| reference.name.starts_with(prefix))
{
continue;
}
encode_data(&format_ref(&reference, symrefs, peel), &mut output)?;
}
encode_flush(&mut output);
Ok(output)
}
fn respond_fetch(&self, packets: &[Packet]) -> Result<Vec<u8>, UploadPackError> {
let mut wants = Vec::new();
let mut haves = Vec::new();
let mut done = false;
for packet in packets {
match packet {
Packet::Data(line) => {
let line = trim_line(line);
if let Some(value) = line.strip_prefix(b"want ") {
wants.push(self.parse_id(value)?);
} else if let Some(value) = line.strip_prefix(b"have ") {
haves.push(self.parse_id(value)?);
} else if line == b"done" {
done = true;
} else if !matches!(
line,
b"thin-pack" | b"no-progress" | b"include-tag" | b"ofs-delta"
) {
return Err(UploadPackError::UnsupportedRequest);
}
}
Packet::Flush => {}
Packet::Delimiter | Packet::ResponseEnd => {
return Err(UploadPackError::UnsupportedRequest);
}
}
}
if wants.is_empty() {
return Err(UploadPackError::IncompleteNegotiation);
}
if !done {
let mut output = Vec::new();
encode_data(b"acknowledgments\n", &mut output)?;
encode_data(b"NAK\n", &mut output)?;
encode_flush(&mut output);
return Ok(output);
}
let pack = self.repository.make_pack(&wants, &haves)?;
let mut output = Vec::with_capacity(pack.len() + 32);
encode_data(b"packfile\n", &mut output)?;
encode_sideband(&pack, &mut output)?;
encode_flush(&mut output);
Ok(output)
}
fn parse_id(&self, input: &[u8]) -> Result<ObjectId, UploadPackError> {
let id = ObjectId::from_hex(input).map_err(|_| UploadPackError::InvalidObjectId)?;
if id.kind() != self.repository.object_format() {
return Err(UploadPackError::InvalidObjectId);
}
Ok(id)
}
}
fn command_name(packets: &[Packet], object_format: Kind) -> Result<&[u8], UploadPackError> {
let mut command = None;
for packet in packets {
let Packet::Data(line) = packet else {
return Err(UploadPackError::UnsupportedRequest);
};
let line = trim_line(line);
if let Some(value) = line.strip_prefix(b"command=") {
if command.replace(value).is_some() {
return Err(UploadPackError::UnsupportedRequest);
}
} else if let Some(value) = line.strip_prefix(b"object-format=") {
if value != hash_name(object_format).as_bytes() {
return Err(UploadPackError::InvalidObjectId);
}
} else if !line.starts_with(b"agent=") {
return Err(UploadPackError::UnsupportedRequest);
}
}
command.ok_or(UploadPackError::UnsupportedRequest)
}
fn format_ref(reference: &GitReference, symrefs: bool, peel: bool) -> Vec<u8> {
let mut output = format!(
"{} {}",
reference.target,
String::from_utf8_lossy(&reference.name)
)
.into_bytes();
if symrefs && let Some(target) = &reference.symbolic_target {
output.extend_from_slice(b" symref-target:");
output.extend_from_slice(target);
}
if peel && let Some(target) = reference.peeled {
output.extend_from_slice(format!(" peeled:{target}").as_bytes());
}
output.push(b'\n');
output
}
fn trim_line(line: &[u8]) -> &[u8] {
line.strip_suffix(b"\n").unwrap_or(line)
}
pub(crate) fn hash_name(kind: Kind) -> &'static str {
match kind {
Kind::Sha1 => "sha1",
Kind::Sha256 => "sha256",
_ => unreachable!("gix returned an unsupported object hash"),
}
}
#[derive(Debug, Error)]
pub(crate) enum UploadPackError {
#[error(transparent)]
Repository(#[from] GitRepositoryError),
#[error(transparent)]
PacketLine(#[from] PacketLineError),
#[error("upload-pack request is not supported")]
UnsupportedRequest,
#[error("upload-pack object ID is not valid for this repository")]
InvalidObjectId,
#[error("upload-pack negotiation is incomplete")]
IncompleteNegotiation,
}