use sley_core::{GitError, ObjectFormat, ObjectId, Result};
use std::io::{ErrorKind, Read, Write};
use std::sync::RwLock;
static PACKET_TRACE_IDENTITY: RwLock<Option<String>> = RwLock::new(None);
pub fn set_packet_trace_identity(prog: &str) {
if let Ok(mut guard) = PACKET_TRACE_IDENTITY.write() {
*guard = Some(prog.to_string());
}
}
fn packet_trace_prefix() -> String {
PACKET_TRACE_IDENTITY
.read()
.ok()
.and_then(|guard| guard.clone())
.unwrap_or_else(|| "git".to_string())
}
fn packet_trace_sink() -> Option<Box<dyn Write>> {
let value = std::env::var("GIT_TRACE_PACKET").ok()?;
let lower = value.to_ascii_lowercase();
match lower.as_str() {
"" | "0" | "false" => None,
"1" | "2" | "true" => Some(Box::new(std::io::stderr())),
_ => {
if std::path::Path::new(&value).is_absolute() {
std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&value)
.ok()
.map(|f| Box::new(f) as Box<dyn Write>)
} else {
None
}
}
}
}
fn packet_trace_enabled() -> bool {
match std::env::var("GIT_TRACE_PACKET") {
Ok(value) => {
let lower = value.to_ascii_lowercase();
!matches!(lower.as_str(), "" | "0" | "false")
}
Err(_) => false,
}
}
fn packet_trace(data: &[u8], is_write: bool) {
if !packet_trace_enabled() {
return;
}
let Some(mut sink) = packet_trace_sink() else {
return;
};
let rendered: Vec<u8> = if data.starts_with(b"PACK") || data.starts_with(b"\x01PACK") {
b"PACK ...".to_vec()
} else {
data.to_vec()
};
let mut out = format!(
"packet: {:>12}{} ",
packet_trace_prefix(),
if is_write { '>' } else { '<' }
);
for &byte in &rendered {
if byte == b'\n' {
continue;
}
if (0x20..=0x7e).contains(&byte) {
out.push(byte as char);
} else {
out.push_str(&format!("\\{byte:o}"));
}
}
out.push('\n');
let _ = sink.write_all(out.as_bytes());
let _ = sink.flush();
}
pub fn trace_packet_read_payload(payload: &[u8]) {
packet_trace(payload, false);
}
pub fn trace_packet_write_payload(payload: &[u8]) {
packet_trace(payload, true);
}
fn packet_trace_frame(frame: &PktLineFrame, is_write: bool) {
if !packet_trace_enabled() {
return;
}
match frame {
PktLineFrame::Data(payload) => packet_trace(payload, is_write),
PktLineFrame::Flush => packet_trace(b"0000", is_write),
PktLineFrame::Delimiter => packet_trace(b"0001", is_write),
PktLineFrame::ResponseEnd => packet_trace(b"0002", is_write),
}
}
pub const PKT_LINE_MAX_LEN: usize = 65_520;
pub const PKT_LINE_MAX_PAYLOAD_LEN: usize = PKT_LINE_MAX_LEN - 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProtocolVersion {
V0,
V1,
V2,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PktLine(pub Vec<u8>);
impl PktLine {
pub fn encode(&self) -> Vec<u8> {
encode_pkt_line_payload(&self.0)
}
pub fn try_encode(&self) -> Result<Vec<u8>> {
validate_pkt_line_payload(&self.0)?;
Ok(self.encode())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PktLineFrame {
Data(Vec<u8>),
Flush,
Delimiter,
ResponseEnd,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProtocolErrorLine {
pub message: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GitService {
UploadPack,
ReceivePack,
UploadArchive,
}
impl GitService {
pub fn as_str(self) -> &'static str {
match self {
Self::UploadPack => "git-upload-pack",
Self::ReceivePack => "git-receive-pack",
Self::UploadArchive => "git-upload-archive",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RefSpec {
pub force: bool,
pub negative: bool,
pub src: Option<String>,
pub dst: Option<String>,
pub pattern: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FetchHeadRecord {
pub oid: ObjectId,
pub not_for_merge: bool,
pub description: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FetchRefUpdate {
pub src: String,
pub dst: Option<String>,
pub oid: ObjectId,
pub not_for_merge: bool,
pub force: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PushSourceRef {
pub name: String,
pub oid: ObjectId,
}
impl PktLineFrame {
pub fn data(payload: impl Into<Vec<u8>>) -> Result<Self> {
let payload = payload.into();
validate_pkt_line_payload(&payload)?;
Ok(Self::Data(payload))
}
pub fn encode(&self) -> Vec<u8> {
match self {
Self::Data(payload) => encode_pkt_line_payload(payload),
Self::Flush => b"0000".to_vec(),
Self::Delimiter => b"0001".to_vec(),
Self::ResponseEnd => b"0002".to_vec(),
}
}
pub fn try_encode(&self) -> Result<Vec<u8>> {
match self {
Self::Data(payload) => try_encode_pkt_line_payload(payload),
Self::Flush | Self::Delimiter | Self::ResponseEnd => Ok(self.encode()),
}
}
pub fn parse(input: &[u8]) -> Result<(Self, usize)> {
if input.len() < 4 {
return Err(GitError::InvalidFormat("truncated pkt-line length".into()));
}
let len = parse_pkt_len(&input[..4])?;
match len {
0 => Ok((Self::Flush, 4)),
1 => Ok((Self::Delimiter, 4)),
2 => Ok((Self::ResponseEnd, 4)),
3 => Err(GitError::InvalidFormat(
"reserved pkt-line length 0003".into(),
)),
4..=PKT_LINE_MAX_LEN => {
if input.len() < len {
return Err(GitError::InvalidFormat(format!(
"truncated pkt-line payload: expected {} bytes, got {}",
len - 4,
input.len().saturating_sub(4)
)));
}
Ok((Self::Data(input[4..len].to_vec()), len))
}
_ => Err(GitError::InvalidFormat(format!(
"pkt-line length exceeds {PKT_LINE_MAX_LEN}: {len}"
))),
}
}
}
fn validate_pkt_line_payload(payload: &[u8]) -> Result<()> {
if payload.len() > PKT_LINE_MAX_PAYLOAD_LEN {
return Err(GitError::InvalidFormat(format!(
"pkt-line payload exceeds {PKT_LINE_MAX_PAYLOAD_LEN} bytes"
)));
}
Ok(())
}
pub(crate) fn pkt_line_header(len: usize) -> [u8; 4] {
const HEX: &[u8; 16] = b"0123456789abcdef";
[
HEX[(len >> 12) & 0xf],
HEX[(len >> 8) & 0xf],
HEX[(len >> 4) & 0xf],
HEX[len & 0xf],
]
}
fn encode_pkt_line_payload(payload: &[u8]) -> Vec<u8> {
let len = payload.len() + 4;
let mut out = Vec::with_capacity(len);
out.extend_from_slice(&pkt_line_header(len));
out.extend_from_slice(payload);
out
}
fn try_encode_pkt_line_payload(payload: &[u8]) -> Result<Vec<u8>> {
validate_pkt_line_payload(payload)?;
Ok(encode_pkt_line_payload(payload))
}
pub fn parse_pkt_line_stream(mut input: &[u8]) -> Result<Vec<PktLineFrame>> {
let mut frames = Vec::new();
while !input.is_empty() {
let (frame, consumed) = PktLineFrame::parse(input)?;
frames.push(frame);
input = &input[consumed..];
}
Ok(frames)
}
pub(crate) fn parse_pkt_line_frames_until_flush_from(mut input: &[u8]) -> Result<(Vec<PktLineFrame>, usize)> {
let mut frames = Vec::new();
let mut total = 0usize;
loop {
if input.is_empty() {
return Err(GitError::InvalidFormat(
"pkt-line stream ended before flush".into(),
));
}
let (frame, consumed) = PktLineFrame::parse(input)?;
total += consumed;
let done = matches!(frame, PktLineFrame::Flush);
frames.push(frame);
input = &input[consumed..];
if done {
return Ok((frames, total));
}
}
}
pub fn read_pkt_line_frame(reader: &mut impl Read) -> Result<Option<PktLineFrame>> {
let mut header = [0u8; 4];
let mut read = 0usize;
while read < header.len() {
match reader.read(&mut header[read..]) {
Ok(0) if read == 0 => return Ok(None),
Ok(0) => {
return Err(GitError::InvalidFormat("truncated pkt-line length".into()));
}
Ok(n) => read += n,
Err(err) if err.kind() == ErrorKind::Interrupted => {}
Err(err) => return Err(err.into()),
}
}
let len = parse_pkt_len(&header)?;
let frame = match len {
0 => PktLineFrame::Flush,
1 => PktLineFrame::Delimiter,
2 => PktLineFrame::ResponseEnd,
3 => {
return Err(GitError::InvalidFormat(
"reserved pkt-line length 0003".into(),
));
}
4..=PKT_LINE_MAX_LEN => {
let mut payload = vec![0; len - 4];
reader.read_exact(&mut payload)?;
PktLineFrame::Data(payload)
}
_ => {
return Err(GitError::InvalidFormat(format!(
"pkt-line length exceeds {PKT_LINE_MAX_LEN}: {len}"
)));
}
};
packet_trace_frame(&frame, false);
Ok(Some(frame))
}
pub fn read_pkt_line_frames(reader: &mut impl Read) -> Result<Vec<PktLineFrame>> {
let mut frames = Vec::new();
while let Some(frame) = read_pkt_line_frame(reader)? {
frames.push(frame);
}
Ok(frames)
}
pub fn read_pkt_line_frames_until_flush(reader: &mut impl Read) -> Result<Vec<PktLineFrame>> {
read_pkt_line_frames_until_control(reader, |frame| matches!(frame, PktLineFrame::Flush))
}
pub fn read_pkt_line_frames_until_response_end(
reader: &mut impl Read,
) -> Result<Vec<PktLineFrame>> {
read_pkt_line_frames_until_control(reader, |frame| matches!(frame, PktLineFrame::ResponseEnd))
}
fn read_pkt_line_frames_until_control(
reader: &mut impl Read,
stop: impl Fn(&PktLineFrame) -> bool,
) -> Result<Vec<PktLineFrame>> {
let mut frames = Vec::new();
loop {
let Some(frame) = read_pkt_line_frame(reader)? else {
return Err(GitError::InvalidFormat(
"pkt-line stream ended before control packet".into(),
));
};
let done = stop(&frame);
frames.push(frame);
if done {
return Ok(frames);
}
}
}
pub fn write_pkt_line_frame(writer: &mut impl Write, frame: &PktLineFrame) -> Result<()> {
match frame {
PktLineFrame::Data(payload) => write_pkt_line_payload(writer, payload)?,
PktLineFrame::Flush => {
packet_trace(b"0000", true);
writer.write_all(b"0000")?;
}
PktLineFrame::Delimiter => {
packet_trace(b"0001", true);
writer.write_all(b"0001")?;
}
PktLineFrame::ResponseEnd => {
packet_trace(b"0002", true);
writer.write_all(b"0002")?;
}
}
Ok(())
}
pub fn write_pkt_line_payload(writer: &mut impl Write, payload: &[u8]) -> Result<()> {
validate_pkt_line_payload(payload)?;
packet_trace(payload, true);
let len = payload.len() + 4;
writer.write_all(&pkt_line_header(len))?;
writer.write_all(payload)?;
Ok(())
}
pub fn write_pkt_line_frames(writer: &mut impl Write, frames: &[PktLineFrame]) -> Result<()> {
for frame in frames {
write_pkt_line_frame(writer, frame)?;
}
Ok(())
}
pub fn parse_error_line(payload: &[u8]) -> Result<ProtocolErrorLine> {
let text = parse_protocol_v2_line_text("protocol error line", payload)?;
let Some(message) = text.strip_prefix("ERR ") else {
return Err(GitError::InvalidFormat(
"protocol error line must start with ERR".into(),
));
};
validate_protocol_error_message(message)?;
Ok(ProtocolErrorLine {
message: message.to_string(),
})
}
pub fn encode_error_line(error: &ProtocolErrorLine) -> Result<Vec<u8>> {
validate_protocol_error_message(&error.message)?;
Ok(line_from_str(&format!("ERR {}", error.message)))
}
pub fn parse_error_frame(frame: &PktLineFrame) -> Result<Option<ProtocolErrorLine>> {
match frame {
PktLineFrame::Data(payload) if trim_trailing_lf(payload).starts_with(b"ERR ") => {
parse_error_line(payload).map(Some)
}
PktLineFrame::Data(_)
| PktLineFrame::Flush
| PktLineFrame::Delimiter
| PktLineFrame::ResponseEnd => Ok(None),
}
}
pub fn read_error_line(reader: &mut impl Read) -> Result<ProtocolErrorLine> {
let Some(frame) = read_pkt_line_frame(reader)? else {
return Err(GitError::InvalidFormat(
"pkt-line stream ended before protocol error line".into(),
));
};
match frame {
PktLineFrame::Data(payload) => parse_error_line(&payload),
_ => Err(GitError::InvalidFormat(
"protocol error line must be a data packet".into(),
)),
}
}
pub fn write_error_line(writer: &mut impl Write, error: &ProtocolErrorLine) -> Result<()> {
write_pkt_line_frame(writer, &PktLineFrame::data(encode_error_line(error)?)?)
}
pub fn parse_git_service(value: &str) -> Result<GitService> {
match value {
"git-upload-pack" => Ok(GitService::UploadPack),
"git-receive-pack" => Ok(GitService::ReceivePack),
"git-upload-archive" => Ok(GitService::UploadArchive),
other => Err(GitError::InvalidFormat(format!(
"unsupported git service {other}"
))),
}
}
pub fn parse_refspec(value: &str) -> Result<RefSpec> {
validate_refspec_value(value)?;
let (force, value) = value
.strip_prefix('+')
.map_or((false, value), |value| (true, value));
let (negative, value) = value
.strip_prefix('^')
.map_or((false, value), |value| (true, value));
if force && negative {
return Err(GitError::InvalidFormat(
"negative refspec must not be forced".into(),
));
}
let (src, dst) = if negative {
if value.contains(':') {
return Err(GitError::InvalidFormat(
"negative refspec must not have a destination".into(),
));
}
(Some(value), None)
} else if let Some((src, dst)) = value.split_once(':') {
(non_empty(src), non_empty(dst))
} else {
(Some(value), None)
};
if src.is_none() && dst.is_none() && value != ":" {
return Err(GitError::InvalidFormat(
"refspec must include a source or destination".into(),
));
}
if negative && src.is_none() {
return Err(GitError::InvalidFormat(
"negative refspec is missing a source".into(),
));
}
if let Some(src) = src {
validate_refspec_endpoint("refspec source", src)?;
}
if let Some(dst) = dst {
validate_refspec_endpoint("refspec destination", dst)?;
}
let src_pattern_count = src.map(count_refspec_wildcards).unwrap_or(0);
let dst_pattern_count = dst.map(count_refspec_wildcards).unwrap_or(0);
if src_pattern_count > 1 || dst_pattern_count > 1 {
return Err(GitError::InvalidFormat(
"refspec endpoint has too many wildcards".into(),
));
}
if dst.is_some() && (src_pattern_count == 1) != (dst_pattern_count == 1) {
return Err(GitError::InvalidFormat(
"refspec wildcard must appear in both source and destination".into(),
));
}
Ok(RefSpec {
force,
negative,
src: src.map(str::to_string),
dst: dst.map(str::to_string),
pattern: src_pattern_count == 1 || dst_pattern_count == 1,
})
}
pub fn encode_refspec(refspec: &RefSpec) -> Result<String> {
validate_refspec_shape(refspec)?;
let mut out = String::new();
if refspec.force {
out.push('+');
}
if refspec.negative {
out.push('^');
}
if let Some(src) = &refspec.src {
out.push_str(src);
}
if !refspec.negative && refspec.src.is_none() && refspec.dst.is_none() {
out.push(':');
} else if !refspec.negative && refspec.dst.is_some() {
out.push(':');
if let Some(dst) = &refspec.dst {
out.push_str(dst);
}
}
Ok(out)
}
pub fn refspec_matches_source(refspec: &RefSpec, source: &str) -> Result<bool> {
Ok(refspec_map_source(refspec, source)?.is_some())
}
pub fn refspec_map_source(refspec: &RefSpec, source: &str) -> Result<Option<String>> {
validate_refspec_shape(refspec)?;
validate_refspec_endpoint("refspec match source", source)?;
let Some(src) = refspec.src.as_deref() else {
return Ok(None);
};
if refspec.pattern {
let Some((src_prefix, src_suffix)) = src.split_once('*') else {
return Ok(None);
};
let Some(middle) = source
.strip_prefix(src_prefix)
.and_then(|value| value.strip_suffix(src_suffix))
else {
return Ok(None);
};
if let Some(dst) = refspec.dst.as_deref() {
let (dst_prefix, dst_suffix) = dst.split_once('*').ok_or_else(|| {
GitError::InvalidFormat("pattern refspec destination is missing wildcard".into())
})?;
return Ok(Some(format!("{dst_prefix}{middle}{dst_suffix}")));
}
return Ok(Some(source.to_string()));
}
if src == source {
return Ok(Some(
refspec.dst.clone().unwrap_or_else(|| source.to_string()),
));
}
Ok(None)
}
pub fn smart_http_info_refs_path(repository_path: &str, service: GitService) -> Result<String> {
validate_smart_http_service(service)?;
let repository_path = normalize_http_repository_path(repository_path)?;
Ok(format!(
"{repository_path}/info/refs?service={}",
service.as_str()
))
}
pub fn smart_http_rpc_path(repository_path: &str, service: GitService) -> Result<String> {
validate_smart_http_service(service)?;
let repository_path = normalize_http_repository_path(repository_path)?;
Ok(format!("{repository_path}/{}", service.as_str()))
}
pub fn dumb_http_info_refs_path(repository_path: &str) -> Result<String> {
let repository_path = normalize_http_repository_path(repository_path)?;
Ok(format!("{repository_path}/info/refs"))
}
pub fn dumb_http_alternates_path(repository_path: &str) -> Result<String> {
let repository_path = normalize_http_repository_path(repository_path)?;
Ok(format!("{repository_path}/objects/info/http-alternates"))
}
pub fn dumb_http_packs_path(repository_path: &str) -> Result<String> {
let repository_path = normalize_http_repository_path(repository_path)?;
Ok(format!("{repository_path}/objects/info/packs"))
}
pub fn dumb_http_loose_object_path(repository_path: &str, oid: &ObjectId) -> Result<String> {
let repository_path = normalize_http_repository_path(repository_path)?;
let oid = oid.to_string();
let (directory, file) = oid.split_at(2);
Ok(format!("{repository_path}/objects/{directory}/{file}"))
}
pub fn dumb_http_pack_file_path(repository_path: &str, hash: &ObjectId) -> Result<String> {
dumb_http_pack_resource_path(repository_path, hash, "pack")
}
pub fn dumb_http_pack_index_path(repository_path: &str, hash: &ObjectId) -> Result<String> {
dumb_http_pack_resource_path(repository_path, hash, "idx")
}
pub fn smart_http_advertisement_content_type(service: GitService) -> Result<String> {
validate_smart_http_service(service)?;
Ok(format!("application/x-{}-advertisement", service.as_str()))
}
pub fn smart_http_rpc_request_content_type(service: GitService) -> Result<String> {
validate_smart_http_service(service)?;
Ok(format!("application/x-{}-request", service.as_str()))
}
pub fn smart_http_rpc_result_content_type(service: GitService) -> Result<String> {
validate_smart_http_service(service)?;
Ok(format!("application/x-{}-result", service.as_str()))
}
pub fn parse_smart_http_advertisement_content_type(value: &str) -> Result<GitService> {
parse_smart_http_content_type(value, "-advertisement")
}
pub fn parse_smart_http_rpc_request_content_type(value: &str) -> Result<GitService> {
parse_smart_http_content_type(value, "-request")
}
pub fn parse_smart_http_rpc_result_content_type(value: &str) -> Result<GitService> {
parse_smart_http_content_type(value, "-result")
}
pub(crate) fn parse_pkt_len(bytes: &[u8]) -> Result<usize> {
let mut len = 0usize;
for byte in bytes {
len = (len << 4) | hex_nibble(*byte)? as usize;
}
Ok(len)
}
fn hex_nibble(byte: u8) -> Result<u8> {
match byte {
b'0'..=b'9' => Ok(byte - b'0'),
b'a'..=b'f' => Ok(byte - b'a' + 10),
b'A'..=b'F' => Ok(byte - b'A' + 10),
_ => Err(GitError::InvalidFormat(format!(
"invalid pkt-line length byte {byte:#04x}"
))),
}
}
fn validate_protocol_error_message(message: &str) -> Result<()> {
if message.is_empty() {
return Err(GitError::InvalidFormat(
"protocol error message is empty".into(),
));
}
if message
.bytes()
.any(|byte| matches!(byte, b'\n' | b'\r' | 0))
{
return Err(GitError::InvalidFormat(
"protocol error message contains a delimiter byte".into(),
));
}
Ok(())
}
pub(crate) fn validate_capability_field(label: &str, value: &str) -> Result<()> {
if value.is_empty() {
return Err(GitError::InvalidFormat(format!("{label} is empty")));
}
if value
.bytes()
.any(|byte| matches!(byte, b' ' | b'\n' | b'\r' | b'\t' | 0))
{
return Err(GitError::InvalidFormat(format!(
"{label} contains a delimiter byte"
)));
}
Ok(())
}
pub(crate) fn validate_capability_name(value: &str) -> Result<()> {
validate_capability_field("capability name", value)?;
if value.bytes().any(|byte| byte == b'=') {
return Err(GitError::InvalidFormat(
"capability name contains a delimiter byte".into(),
));
}
Ok(())
}
pub(crate) fn non_empty(value: &str) -> Option<&str> {
(!value.is_empty()).then_some(value)
}
fn validate_refspec_value(value: &str) -> Result<()> {
if value.is_empty() {
return Err(GitError::InvalidFormat("refspec is empty".into()));
}
if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
return Err(GitError::InvalidFormat(
"refspec contains a delimiter byte".into(),
));
}
Ok(())
}
pub(crate) fn validate_refspec_endpoint(label: &str, value: &str) -> Result<()> {
if value.is_empty() {
return Err(GitError::InvalidFormat(format!("{label} is empty")));
}
if value
.bytes()
.any(|byte| matches!(byte, b':' | b' ' | b'\t' | b'\n' | b'\r' | 0))
{
return Err(GitError::InvalidFormat(format!(
"{label} contains a delimiter byte"
)));
}
Ok(())
}
fn count_refspec_wildcards(value: &str) -> usize {
value.bytes().filter(|byte| *byte == b'*').count()
}
pub(crate) fn validate_refspec_shape(refspec: &RefSpec) -> Result<()> {
if refspec.force && refspec.negative {
return Err(GitError::InvalidFormat(
"negative refspec must not be forced".into(),
));
}
if refspec.negative && refspec.dst.is_some() {
return Err(GitError::InvalidFormat(
"negative refspec must not have a destination".into(),
));
}
if refspec.negative && refspec.src.is_none() {
return Err(GitError::InvalidFormat(
"negative refspec is missing a source".into(),
));
}
if refspec.src.is_none() && refspec.dst.is_none() && refspec.negative {
return Err(GitError::InvalidFormat(
"refspec must include a source or destination".into(),
));
}
if let Some(src) = &refspec.src {
validate_refspec_endpoint("refspec source", src)?;
}
if let Some(dst) = &refspec.dst {
validate_refspec_endpoint("refspec destination", dst)?;
}
let src_pattern_count = refspec
.src
.as_deref()
.map(count_refspec_wildcards)
.unwrap_or(0);
let dst_pattern_count = refspec
.dst
.as_deref()
.map(count_refspec_wildcards)
.unwrap_or(0);
if src_pattern_count > 1 || dst_pattern_count > 1 {
return Err(GitError::InvalidFormat(
"refspec endpoint has too many wildcards".into(),
));
}
if refspec.dst.is_some() && (src_pattern_count == 1) != (dst_pattern_count == 1) {
return Err(GitError::InvalidFormat(
"refspec wildcard must appear in both source and destination".into(),
));
}
if refspec.pattern != (src_pattern_count == 1 || dst_pattern_count == 1) {
return Err(GitError::InvalidFormat(
"refspec pattern flag does not match endpoints".into(),
));
}
Ok(())
}
pub(crate) fn validate_fetch_head_line(value: &[u8]) -> Result<()> {
if value.is_empty() {
return Err(GitError::InvalidFormat("FETCH_HEAD record is empty".into()));
}
if !value.ends_with(b"\n") {
return Err(GitError::InvalidFormat(
"FETCH_HEAD record missing LF".into(),
));
}
if value.iter().any(|byte| matches!(*byte, b'\r' | 0)) {
return Err(GitError::InvalidFormat(
"FETCH_HEAD record contains a delimiter byte".into(),
));
}
Ok(())
}
pub(crate) fn validate_fetch_head_description_field(value: &str) -> Result<()> {
if value.is_empty() {
return Err(GitError::InvalidFormat(
"FETCH_HEAD description is empty".into(),
));
}
if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
return Err(GitError::InvalidFormat(
"FETCH_HEAD description contains a delimiter byte".into(),
));
}
Ok(())
}
fn validate_smart_http_service(service: GitService) -> Result<()> {
match service {
GitService::UploadPack | GitService::ReceivePack => Ok(()),
GitService::UploadArchive => Err(GitError::InvalidFormat(
"smart HTTP only supports upload-pack and receive-pack services".into(),
)),
}
}
fn normalize_http_repository_path(path: &str) -> Result<String> {
if path.is_empty() {
return Err(GitError::InvalidFormat(
"smart HTTP repository path is empty".into(),
));
}
if !path.starts_with('/') {
return Err(GitError::InvalidFormat(
"smart HTTP repository path must start with /".into(),
));
}
if path
.bytes()
.any(|byte| matches!(byte, b'\n' | b'\r' | 0 | b'?' | b'#'))
{
return Err(GitError::InvalidFormat(
"smart HTTP repository path contains a delimiter byte".into(),
));
}
let normalized = path.trim_end_matches('/');
Ok(if normalized.is_empty() {
"/".into()
} else {
normalized.to_string()
})
}
fn dumb_http_pack_resource_path(
repository_path: &str,
hash: &ObjectId,
suffix: &str,
) -> Result<String> {
let repository_path = normalize_http_repository_path(repository_path)?;
Ok(format!(
"{repository_path}/objects/pack/pack-{hash}.{suffix}"
))
}
fn parse_smart_http_content_type(value: &str, suffix: &str) -> Result<GitService> {
let value = value.trim();
if value.is_empty() {
return Err(GitError::InvalidFormat(
"smart HTTP content type is empty".into(),
));
}
if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
return Err(GitError::InvalidFormat(
"smart HTTP content type contains a delimiter byte".into(),
));
}
let value = value.to_ascii_lowercase();
let service = value
.strip_prefix("application/x-")
.and_then(|value| value.strip_suffix(suffix))
.ok_or_else(|| GitError::InvalidFormat("invalid smart HTTP content type".into()))?;
let service = parse_git_service(service)?;
validate_smart_http_service(service)?;
Ok(service)
}
pub(crate) fn validate_protocol_v2_token(label: &str, value: &str) -> Result<()> {
if value.is_empty() {
return Err(GitError::InvalidFormat(format!("{label} is empty")));
}
if value
.bytes()
.any(|byte| matches!(byte, b' ' | b'\n' | b'\r' | 0))
{
return Err(GitError::InvalidFormat(format!(
"{label} contains a delimiter byte"
)));
}
Ok(())
}
pub(crate) fn validate_protocol_v2_line(label: &str, value: &[u8]) -> Result<()> {
if value.is_empty() {
return Err(GitError::InvalidFormat(format!("{label} is empty")));
}
if value.iter().any(|byte| matches!(*byte, b'\r' | 0)) {
return Err(GitError::InvalidFormat(format!(
"{label} contains a delimiter byte"
)));
}
Ok(())
}
pub(crate) fn parse_protocol_v2_line_text<'a>(label: &str, value: &'a [u8]) -> Result<&'a str> {
validate_protocol_v2_line(label, value)?;
let value = trim_trailing_lf(value);
if value.is_empty() {
return Err(GitError::InvalidFormat(format!("{label} is empty")));
}
if value.iter().any(|byte| matches!(*byte, b'\n' | b'\r' | 0)) {
return Err(GitError::InvalidFormat(format!(
"{label} contains a delimiter byte"
)));
}
std::str::from_utf8(value).map_err(|err| GitError::InvalidFormat(err.to_string()))
}
pub(crate) fn parse_oid_argument(
format: ObjectFormat,
label: &str,
value: &str,
prefix: &str,
) -> Result<ObjectId> {
let oid = value
.strip_prefix(prefix)
.ok_or_else(|| GitError::InvalidFormat(format!("invalid {label}")))?;
validate_protocol_v2_token(label, oid)?;
ObjectId::from_hex(format, oid)
}
pub(crate) fn line(mut payload: Vec<u8>) -> Vec<u8> {
payload.push(b'\n');
payload
}
pub(crate) fn line_from_str(payload: &str) -> Vec<u8> {
line(payload.as_bytes().to_vec())
}
pub(crate) fn trim_trailing_lf(input: &[u8]) -> &[u8] {
input.strip_suffix(b"\n").unwrap_or(input)
}