426a121f23b76660d4e726c990a4ce183c1c0384 / frogue / src / protocol / pktline.rs
Raw
//! Git pkt-line protocol codec
//!
//! The pkt-line format is a simple length-prefixed line format used by git
//! for communication. Each packet starts with a 4-digit hex length (including
//! the 4 bytes themselves), followed by the payload. The special length "0000"
//! is a flush packet.
use std::io::{self, Read, Write};
/// A flush packet (length 0000)
pub const FLUSH: &[u8] = b"0000";
/// Encodes a pkt-line with the given payload.
///
/// The length includes the 4-byte length prefix itself.
pub fn encode(payload: &[u8], out: &mut impl Write) -> io::Result<()> {
let len = payload.len() + 4;
if len > 0xFFFF {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"pkt-line payload too large",
));
}
write!(out, "{:04x}", len)?;
out.write_all(payload)?;
Ok(())
}
/// Encodes a flush packet.
pub fn encode_flush(out: &mut impl Write) -> io::Result<()> {
out.write_all(FLUSH)
}
/// Encodes a pkt-line with a newline-terminated payload.
///
/// Convenience function that adds a newline to the payload before encoding.
pub fn encode_line(line: &str, out: &mut impl Write) -> io::Result<()> {
let mut payload = line.as_bytes().to_vec();
if !payload.ends_with(b"\n") {
payload.push(b'\n');
}
encode(&payload, out)
}
/// A pkt-line packet
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Packet {
/// A flush packet
Flush,
/// A data packet with the payload
Data(Vec<u8>),
}
impl Packet {
/// Returns true if this is a flush packet.
pub fn is_flush(&self) -> bool {
matches!(self, Packet::Flush)
}
/// Returns the payload if this is a data packet.
pub fn data(&self) -> Option<&[u8]> {
match self {
Packet::Flush => None,
Packet::Data(data) => Some(data),
}
}
}
/// Decodes a single pkt-line from the input.
///
/// Returns None if the input is exhausted (EOF).
pub fn decode(in_stream: &mut impl Read) -> io::Result<Option<Packet>> {
let mut len_buf = [0u8; 4];
match in_stream.read_exact(&mut len_buf) {
Ok(_) => {}
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(None),
Err(e) => return Err(e),
}
let len_str = std::str::from_utf8(&len_buf).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidData,
"pkt-line length not valid UTF-8",
)
})?;
let len = usize::from_str_radix(len_str, 16)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "pkt-line length not valid hex"))?;
if len == 0 {
return Ok(Some(Packet::Flush));
}
if len < 4 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"pkt-line length too small",
));
}
let payload_len = len - 4;
let mut payload = vec![0u8; payload_len];
in_stream.read_exact(&mut payload)?;
Ok(Some(Packet::Data(payload)))
}
/// Reads a single text line (newline-terminated) from the input.
///
/// Used by the proc-receive hook to read the initial capability line,
/// which git sends as a plain line rather than a pkt-line.
pub fn read_line(in_stream: &mut impl Read) -> io::Result<String> {
let mut line = String::new();
let mut buf = [0u8; 1];
loop {
match in_stream.read_exact(&mut buf) {
Ok(_) => {
if buf[0] == b'\n' {
return Ok(line);
}
line.push(buf[0] as char);
}
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {
return Ok(line);
}
Err(e) => return Err(e),
}
}
}
/// Writes a string followed by a newline.
pub fn write_line(line: &str, out: &mut impl Write) -> io::Result<()> {
out.write_all(line.as_bytes())?;
if !line.ends_with('\n') {
out.write_all(b"\n")?;
}
Ok(())
}
/// Reads all packets from the input until a flush packet or EOF.
pub fn read_packets(in_stream: &mut impl Read) -> io::Result<Vec<Packet>> {
let mut packets = Vec::new();
loop {
match decode(in_stream)? {
None => break,
Some(Packet::Flush) => {
packets.push(Packet::Flush);
break;
}
Some(packet) => packets.push(packet),
}
}
Ok(packets)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encode_empty_payload() {
let mut buf = Vec::new();
encode(b"", &mut buf).unwrap();
assert_eq!(buf, b"0004");
}
#[test]
fn encode_with_payload() {
let mut buf = Vec::new();
encode(b"hello", &mut buf).unwrap();
assert_eq!(buf, b"0009hello");
}
#[test]
fn test_encode_flush() {
let mut buf = Vec::new();
encode_flush(&mut buf).unwrap();
assert_eq!(buf, b"0000");
}
#[test]
fn encode_line_adds_newline() {
let mut buf = Vec::new();
encode_line("hello", &mut buf).unwrap();
assert_eq!(buf, b"000ahello\n");
}
#[test]
fn encode_line_keeps_existing_newline() {
let mut buf = Vec::new();
encode_line("hello\n", &mut buf).unwrap();
assert_eq!(buf, b"000ahello\n");
}
#[test]
fn decode_flush() {
let mut cursor = std::io::Cursor::new(b"0000");
let packet = decode(&mut cursor).unwrap().unwrap();
assert_eq!(packet, Packet::Flush);
}
#[test]
fn decode_with_payload() {
let mut cursor = std::io::Cursor::new(b"0009hello");
let packet = decode(&mut cursor).unwrap().unwrap();
assert_eq!(packet, Packet::Data(b"hello".to_vec()));
}
#[test]
fn decode_eof() {
let mut cursor = std::io::Cursor::new(b"");
assert!(decode(&mut cursor).unwrap().is_none());
}
#[test]
fn round_trip() {
let mut buf = Vec::new();
encode(b"first", &mut buf).unwrap();
encode(b"second", &mut buf).unwrap();
encode_flush(&mut buf).unwrap();
let mut cursor = std::io::Cursor::new(buf);
let packets = read_packets(&mut cursor).unwrap();
assert_eq!(packets.len(), 3);
assert_eq!(packets[0], Packet::Data(b"first".to_vec()));
assert_eq!(packets[1], Packet::Data(b"second".to_vec()));
assert_eq!(packets[2], Packet::Flush);
}
}