use async_trait::async_trait;
use crate::limits::Limits;
use crate::seam::RefLine;
use crate::stateless::StatelessResponse;
use crate::substrate::SubstrateError;
#[async_trait]
pub trait PktLineWriter {
async fn write_text_line(&mut self, line: &[u8]) -> Result<(), SubstrateError>;
async fn write_flush(&mut self) -> Result<(), SubstrateError>;
async fn write_data_band(&mut self, data: &[u8]) -> Result<(), SubstrateError>;
}
#[async_trait]
impl<T> PktLineWriter for crate::duplex::Session<T>
where
T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send,
{
async fn write_text_line(&mut self, line: &[u8]) -> Result<(), SubstrateError> {
crate::duplex::Session::write_text_line(self, line).await
}
async fn write_flush(&mut self) -> Result<(), SubstrateError> {
crate::duplex::Session::write_flush(self).await
}
async fn write_data_band(&mut self, data: &[u8]) -> Result<(), SubstrateError> {
crate::duplex::Session::write_data_band(self, data).await
}
}
#[async_trait]
impl<W> PktLineWriter for StatelessResponse<W>
where
W: futures::io::AsyncWrite + Unpin + Send,
{
async fn write_text_line(&mut self, line: &[u8]) -> Result<(), SubstrateError> {
StatelessResponse::write_text_line(self, line).await
}
async fn write_flush(&mut self) -> Result<(), SubstrateError> {
StatelessResponse::write_flush(self).await
}
async fn write_data_band(&mut self, data: &[u8]) -> Result<(), SubstrateError> {
StatelessResponse::write_data_band(self, data).await
}
}
pub(crate) const CAPABILITY_LINES: &[&str] = &[
"ls-refs=unborn",
"fetch=wait-for-done",
"object-format=sha1",
];
pub(crate) const VERSION_LINE: &str = "version 2";
pub async fn advertise_v2(writer: &mut impl PktLineWriter) -> Result<(), SubstrateError> {
writer.write_text_line(VERSION_LINE.as_bytes()).await?;
for capability in CAPABILITY_LINES {
writer.write_text_line(capability.as_bytes()).await?;
}
#[cfg(feature = "sha256")]
writer.write_text_line(b"object-format=sha256").await?;
writer.write_flush().await
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct LsRefsArgs {
pub peel: bool,
pub symrefs: bool,
pub ref_prefixes: Vec<String>,
pub unborn: bool,
}
pub fn parse_ls_refs_args(args: &[String]) -> Result<LsRefsArgs, SubstrateError> {
let mut parsed = LsRefsArgs::default();
for arg in args {
match arg.as_str() {
"peel" => parsed.peel = true,
"symrefs" => parsed.symrefs = true,
"unborn" => parsed.unborn = true,
"ref-prefix" => {
return Err(SubstrateError::Protocol(
"ls-refs: ref-prefix given without a value".to_owned(),
));
}
other => match other.strip_prefix("ref-prefix ") {
Some("") => {
return Err(SubstrateError::Protocol(
"ls-refs: ref-prefix given without a value".to_owned(),
));
}
Some(prefix) => parsed.ref_prefixes.push(prefix.to_owned()),
None => {
return Err(SubstrateError::Protocol(format!(
"ls-refs: unexpected argument {other:?}"
)));
}
},
}
}
Ok(parsed)
}
impl LsRefsArgs {
fn accepts(&self, name: &str) -> bool {
self.ref_prefixes.is_empty() || self.ref_prefixes.iter().any(|p| name.starts_with(p))
}
}
pub(crate) const UNBORN_TOKEN: &str = "unborn";
fn ref_line_text(line: &RefLine, hex: &str, args: &LsRefsArgs) -> String {
let mut text = format!("{hex} {}", line.name);
if args.symrefs {
if let Some(target) = &line.symref_target {
text.push_str(&format!(" symref-target:{target}"));
}
}
if args.peel {
if let Some(peeled) = line.peeled {
text.push_str(&format!(" peeled:{peeled}"));
}
}
text
}
pub async fn serve_ls_refs(
writer: &mut impl PktLineWriter,
refs: Vec<RefLine>,
args: &LsRefsArgs,
limits: &Limits,
) -> Result<(), SubstrateError> {
if refs.len() > limits.max_advertisement_refs {
return Err(SubstrateError::Protocol(format!(
"ls-refs: advertisement ref cap of {} exceeded by {} refs — fail closed, no partial list (ADR-009, review 001 N-1)",
limits.max_advertisement_refs,
refs.len()
)));
}
for line in &refs {
if !args.accepts(&line.name) {
continue;
}
let bytes = match line.oid {
Some(oid) => ref_line_text(line, &oid.to_string(), args),
None => {
if !args.unborn {
continue;
}
ref_line_text(line, UNBORN_TOKEN, args)
}
};
writer.write_text_line(bytes.as_bytes()).await?;
}
writer.write_flush().await
}
#[cfg(test)]
mod tests {
use super::*;
use crate::substrate::SubstrateError;
use gix_hash::ObjectId;
fn oid(b: u8) -> ObjectId {
ObjectId::from([b; 20])
}
fn ref_line(name: &str, oid: Option<ObjectId>) -> RefLine {
RefLine {
name: name.to_owned(),
oid,
peeled: None,
symref_target: None,
}
}
fn protocol_error(result: Result<LsRefsArgs, SubstrateError>) -> String {
match result {
Err(SubstrateError::Protocol(message)) => message,
Ok(args) => panic!("expected a protocol error, got {args:?}"),
Err(other) => panic!("expected a protocol error, got {other:?}"),
}
}
#[test]
fn advertisement_capabilities_are_exactly_the_honest_list() {
assert_eq!(
CAPABILITY_LINES,
&[
"ls-refs=unborn",
"fetch=wait-for-done",
"object-format=sha1",
]
);
let declined = [
"shallow",
"filter",
"packfile-uris",
"object-info",
"server-option",
];
for capability in declined {
assert!(
!CAPABILITY_LINES
.iter()
.any(|line| line.contains(capability)),
"{capability} is declined by omission (ADR-003)"
);
}
}
#[test]
fn parse_reads_flags_and_repeated_prefixes() {
let args = parse_ls_refs_args(&[
"symrefs".to_owned(),
"ref-prefix refs/heads/".to_owned(),
"peel".to_owned(),
"ref-prefix refs/tags/".to_owned(),
])
.unwrap();
assert!(args.peel);
assert!(args.symrefs);
assert!(!args.unborn);
assert_eq!(args.ref_prefixes, vec!["refs/heads/", "refs/tags/"]);
}
#[test]
fn parse_reads_the_unborn_argument() {
let args = parse_ls_refs_args(&["unborn".to_owned(), "symrefs".to_owned()]).unwrap();
assert!(args.unborn);
assert!(args.symrefs);
}
#[test]
fn parse_rejects_unknown_arguments_fail_closed() {
let message = protocol_error(parse_ls_refs_args(&["shallow".to_owned()]));
assert!(message.contains("shallow"), "{message}");
}
#[test]
fn parse_rejects_a_ref_prefix_without_its_value() {
let message = protocol_error(parse_ls_refs_args(&["ref-prefix".to_owned()]));
assert!(message.contains("ref-prefix"), "{message}");
}
#[test]
fn empty_args_parse_to_no_filtering_and_no_attributes() {
let args = parse_ls_refs_args(&[]).unwrap();
assert_eq!(args, LsRefsArgs::default());
}
#[test]
fn the_unborn_token_is_the_upstream_literal() {
assert_eq!(UNBORN_TOKEN, "unborn");
}
#[tokio::test]
async fn serve_streams_filtered_attribute_lines_and_flushes() {
let refs = vec![
ref_line("HEAD", Some(oid(1))),
ref_line("refs/heads/main", Some(oid(2))),
ref_line("refs/tags/v1", Some(oid(3))),
];
let mut sink: Vec<String> = Vec::new();
struct Sink<'a>(&'a mut Vec<String>);
#[async_trait]
impl PktLineWriter for Sink<'_> {
async fn write_text_line(&mut self, line: &[u8]) -> Result<(), SubstrateError> {
self.0.push(String::from_utf8_lossy(line).into_owned());
Ok(())
}
async fn write_flush(&mut self) -> Result<(), SubstrateError> {
self.0.push(String::new());
Ok(())
}
async fn write_data_band(&mut self, data: &[u8]) -> Result<(), SubstrateError> {
self.0
.push(format!("band:{}", String::from_utf8_lossy(data)));
Ok(())
}
}
let args = parse_ls_refs_args(&[
"peel".to_owned(),
"symrefs".to_owned(),
"ref-prefix refs/".to_owned(),
])
.unwrap();
serve_ls_refs(&mut Sink(&mut sink), refs, &args, &Limits::default())
.await
.unwrap();
assert_eq!(
sink,
vec![
format!("{} refs/heads/main", oid(2)),
format!("{} refs/tags/v1", oid(3)),
String::new(),
]
);
}
}