use std::io::{BufRead, Write};
use gunnar_pktline::Packet;
use crate::error::{Error, Result};
use crate::limits::CHUNK_BYTES;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SectionEnd {
Delim,
Flush,
}
impl SectionEnd {
pub(crate) fn name(self) -> &'static str {
match self {
SectionEnd::Delim => "delim-pkt",
SectionEnd::Flush => "flush-pkt",
}
}
}
pub(crate) fn read_args(
input: &mut dyn BufRead,
max_args: usize,
what: &'static str,
) -> Result<(Vec<String>, SectionEnd)> {
let mut args = Vec::new();
loop {
match gunnar_pktline::expect_packet(input, what)? {
Packet::Flush => return Ok((args, SectionEnd::Flush)),
Packet::Delim => return Ok((args, SectionEnd::Delim)),
Packet::Data(data) => {
if args.len() >= max_args {
return Err(Error::BadRequest(format!(
"more than {max_args} arguments on one command"
)));
}
args.push(text_of(&data, what)?);
}
Packet::ResponseEnd => {
return Err(Error::Framing(format!(
"a response-end-pkt appeared while reading {what}; \
it belongs to git protocol v2 and has no meaning here"
)))
}
}
}
}
pub(crate) const MAX_LINE_BYTES: usize = 256;
pub(crate) fn read_lines(
input: &mut dyn BufRead,
max_lines: usize,
what: &'static str,
) -> Result<Vec<String>> {
let mut lines = Vec::new();
let max_bytes = max_lines.saturating_mul(MAX_LINE_BYTES);
let mut bytes = 0usize;
loop {
match gunnar_pktline::expect_packet(input, what)? {
Packet::Flush => return Ok(lines),
Packet::Data(data) => {
if lines.len() >= max_lines {
return Err(Error::BadRequest(format!(
"more than {max_lines} lines in {what}"
)));
}
bytes = bytes.saturating_add(data.len());
if bytes > max_bytes {
return Err(Error::BadRequest(format!(
"more than {max_bytes} bytes of text in {what}"
)));
}
lines.push(text_of(&data, what)?);
}
other => {
return Err(Error::Framing(format!(
"unexpected {other:?} while reading {what}"
)))
}
}
}
}
fn text_of(data: &[u8], what: &'static str) -> Result<String> {
let trimmed = data.strip_suffix(b"\n").unwrap_or(data);
std::str::from_utf8(trimmed)
.map(str::to_owned)
.map_err(|_| Error::BadRequest(format!("a non-UTF-8 line in {what}")))
}
pub(crate) fn arg<'a>(args: &'a [String], key: &str) -> Option<&'a str> {
args.iter().find_map(|entry| {
let rest = entry.strip_prefix(key)?;
match rest.strip_prefix('=') {
Some(value) => Some(value),
None if rest.is_empty() => Some(""),
None => None,
}
})
}
pub const CAPABILITIES: &[&str] = &["version=1"];
pub const PROTOCOL_VERSION: &str = "1";
pub(crate) fn write_capabilities(out: &mut dyn Write) -> Result<()> {
for capability in CAPABILITIES {
write_text(out, capability)?;
}
write_flush(out)
}
pub(crate) fn write_status(out: &mut dyn Write, code: u16, args: &[String]) -> Result<()> {
write_text(out, &format!("status {code}"))?;
for arg in args {
write_text(out, arg)?;
}
write_flush(out)
}
pub(crate) fn write_status_lines(
out: &mut dyn Write,
code: u16,
args: &[String],
lines: &[String],
) -> Result<()> {
write_text(out, &format!("status {code}"))?;
for arg in args {
write_text(out, arg)?;
}
write_delim(out)?;
for line in lines {
write_text(out, line)?;
}
write_flush(out)
}
pub(crate) fn write_error(out: &mut dyn Write, code: u16, message: &str) -> Result<()> {
write_status_lines(out, code, &[], &[sanitise(message)])
}
fn sanitise(message: &str) -> String {
message
.chars()
.map(|c| if c.is_control() { ' ' } else { c })
.take(512)
.collect()
}
pub(crate) fn write_text(out: &mut dyn Write, line: &str) -> Result<()> {
let mut sink: &mut dyn Write = out;
gunnar_pktline::write_text(&mut sink, line.as_bytes())?;
Ok(())
}
pub(crate) fn write_flush(out: &mut dyn Write) -> Result<()> {
let mut sink: &mut dyn Write = out;
gunnar_pktline::write_flush(&mut sink)?;
Ok(())
}
pub(crate) fn write_delim(out: &mut dyn Write) -> Result<()> {
let mut sink: &mut dyn Write = out;
gunnar_pktline::write_delim(&mut sink)?;
Ok(())
}
pub(crate) struct DataFramer<'a> {
out: &'a mut dyn Write,
buf: Vec<u8>,
framed: u64,
}
impl<'a> DataFramer<'a> {
pub(crate) fn new(out: &'a mut dyn Write) -> Self {
DataFramer {
out,
buf: Vec::with_capacity(CHUNK_BYTES),
framed: 0,
}
}
pub(crate) fn finish(mut self) -> Result<u64> {
self.emit()?;
Ok(self.framed)
}
fn emit(&mut self) -> Result<()> {
if self.buf.is_empty() {
return Ok(());
}
let mut sink: &mut dyn Write = self.out;
gunnar_pktline::write_data(&mut sink, &self.buf)?;
self.framed += self.buf.len() as u64;
self.buf.clear();
Ok(())
}
}
impl Write for DataFramer<'_> {
fn write(&mut self, data: &[u8]) -> std::io::Result<usize> {
if data.is_empty() {
return Ok(0);
}
let room = CHUNK_BYTES - self.buf.len();
let take = room.min(data.len());
self.buf.extend_from_slice(&data[..take]);
if self.buf.len() == CHUNK_BYTES {
self.emit().map_err(as_io)?;
}
Ok(take)
}
fn flush(&mut self) -> std::io::Result<()> {
self.emit().map_err(as_io)?;
self.out.flush()
}
}
fn as_io(err: Error) -> std::io::Error {
match err {
Error::Io(io) => io,
other => std::io::Error::other(other.to_string()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn quit_gets_no_delim_and_everything_else_does() {
let mut out = Vec::new();
write_status(&mut out, 200, &[]).unwrap();
assert_eq!(out, b"000fstatus 200\n0000");
let mut out = Vec::new();
write_status_lines(&mut out, 200, &[], &[]).unwrap();
assert_eq!(out, b"000fstatus 200\n00010000");
}
#[test]
fn an_error_carries_its_message_where_the_client_reads_it() {
let mut out = Vec::new();
write_error(&mut out, 404, "object not found").unwrap();
let text = String::from_utf8_lossy(&out).to_string();
assert!(text.starts_with("000fstatus 404\n0001"), "{text}");
assert!(text.contains("object not found"), "{text}");
}
#[test]
fn a_control_byte_in_a_message_cannot_forge_a_frame() {
let mut out = Vec::new();
write_error(&mut out, 400, "bad\n0000stat\u{1b}[2Jus 200").unwrap();
let text = String::from_utf8_lossy(&out).to_string();
assert_eq!(text.matches("status 4").count(), 1);
assert_eq!(text.matches('\n').count(), 2);
assert!(!text.contains('\u{1b}'));
}
#[test]
fn a_capped_section_is_capped_in_bytes_and_not_only_in_lines() {
let line = vec![b'x'; gunnar_pktline::MAX_PAYLOAD];
let mut wire = Vec::new();
for _ in 0..100 {
gunnar_pktline::write_data(&mut wire, &line).expect("frame");
}
gunnar_pktline::write_flush(&mut wire).expect("flush");
let err = read_lines(&mut wire.as_slice(), 100, "the batch objects")
.map(|lines| lines.iter().map(String::len).sum::<usize>())
.expect_err("a section of maximal lines must be refused");
assert!(
matches!(err, Error::BadRequest(_)),
"a section over its byte ceiling is the client's fault, not a framing fault: {err:?}"
);
assert!(
format!("{err}").contains("bytes of text"),
"the refusal must name the ceiling that fired: {err}"
);
let mut wire = Vec::new();
for seq in 0..100u32 {
let oid = format!("{:064x} {}", seq, 4096);
gunnar_pktline::write_data(&mut wire, oid.as_bytes()).expect("frame");
}
gunnar_pktline::write_flush(&mut wire).expect("flush");
let lines = read_lines(&mut wire.as_slice(), 100, "the batch objects")
.expect("a real batch section must be read");
assert_eq!(lines.len(), 100);
let used: usize = lines.iter().map(|l| l.len()).sum();
assert!(
used * 3 < 100 * MAX_LINE_BYTES,
"the byte ceiling is too close to a real batch section ({used} bytes used of {})",
100 * MAX_LINE_BYTES
);
}
#[test]
fn args_are_parsed_including_the_empty_value_form() {
let args: Vec<String> = ["size=17", "id=", "token=abc", "bare"]
.iter()
.map(|s| s.to_string())
.collect();
assert_eq!(arg(&args, "size"), Some("17"));
assert_eq!(arg(&args, "id"), Some(""));
assert_eq!(arg(&args, "token"), Some("abc"));
assert_eq!(arg(&args, "bare"), Some(""));
assert_eq!(arg(&args, "missing"), None);
assert_eq!(arg(&args, "siz"), None);
assert_eq!(arg(&args, "toke"), None);
}
#[test]
fn the_data_framer_never_buffers_more_than_one_chunk() {
let mut out = Vec::new();
let mut framer = DataFramer::new(&mut out);
let payload = vec![b'x'; CHUNK_BYTES * 3 + 7];
std::io::Write::write_all(&mut framer, &payload).unwrap();
assert!(
framer.buf.capacity() <= CHUNK_BYTES * 2,
"the framer grew to {} bytes for a {}-byte object",
framer.buf.capacity(),
payload.len()
);
let framed = framer.finish().unwrap();
assert_eq!(framed, payload.len() as u64);
let mut decoder = gunnar_pktline::Decoder::new();
decoder.push(&out);
let mut back = Vec::new();
for packet in decoder.drain().unwrap() {
back.extend_from_slice(packet.payload().expect("data packets only"));
}
assert_eq!(back, payload);
}
#[test]
fn reading_a_section_reports_which_terminator_arrived() {
let mut input: &[u8] = b"000bsize=1\n0001";
let mut reader: &mut dyn BufRead = &mut input;
let (args, end) = read_args(&mut reader, 8, "put-object").unwrap();
assert_eq!(args, vec!["size=1".to_string()]);
assert_eq!(end, SectionEnd::Delim);
let mut input: &[u8] = b"000bsize=1\n0000";
let mut reader: &mut dyn BufRead = &mut input;
let (_, end) = read_args(&mut reader, 8, "put-object").unwrap();
assert_eq!(end, SectionEnd::Flush);
}
#[test]
fn the_argument_cap_fires() {
let mut framed = Vec::new();
for i in 0..10 {
gunnar_pktline::write_text(&mut framed, format!("a{i}=1").as_bytes()).unwrap();
}
gunnar_pktline::write_flush(&mut framed).unwrap();
let mut slice: &[u8] = &framed;
let mut reader: &mut dyn BufRead = &mut slice;
let err = read_args(&mut reader, 4, "batch").expect_err("over the cap");
assert_eq!(err.status(), 400);
let mut slice: &[u8] = &framed;
let mut reader: &mut dyn BufRead = &mut slice;
assert_eq!(read_args(&mut reader, 32, "batch").unwrap().0.len(), 10);
}
}