Skip to main content

git_internal/protocol/
utils.rs

1//! Helper functions shared by the Git smart protocol handlers, including pkt-line parsing, pkt-line
2//! encoding, subsequence scans, and response builders that honor HTTP/SSH quirks.
3
4use bytes::{Buf, BufMut, Bytes, BytesMut};
5
6use super::types::{PKT_LINE_END_MARKER, TransportProtocol};
7
8/// Read a packet line from the given bytes buffer
9///
10/// Returns a tuple of (bytes_consumed, packet_data)
11///
12/// This is the original simple implementation from ceres
13pub fn read_pkt_line(bytes: &mut Bytes) -> (usize, Bytes) {
14    if bytes.is_empty() {
15        return (0, Bytes::new());
16    }
17
18    // Ensure we have at least 4 bytes for the length prefix
19    if bytes.len() < 4 {
20        return (0, Bytes::new());
21    }
22
23    let pkt_length = bytes.slice(0..4);
24    let pkt_length_str = match core::str::from_utf8(&pkt_length) {
25        Ok(s) => s,
26        Err(_) => {
27            tracing::warn!("Invalid UTF-8 in packet length: {:?}", pkt_length);
28            return (0, Bytes::new());
29        }
30    };
31
32    let pkt_length = match usize::from_str_radix(pkt_length_str, 16) {
33        Ok(len) => len,
34        Err(_) => {
35            tracing::warn!("Invalid hex packet length: {:?}", pkt_length_str);
36            return (0, Bytes::new());
37        }
38    };
39
40    if pkt_length == 0 {
41        bytes.advance(4);
42        return (4, Bytes::new()); // Consumed 4 bytes for the "0000" marker
43    }
44
45    if pkt_length < 4 {
46        tracing::warn!("Invalid packet length: {} (must be >= 4)", pkt_length);
47        return (0, Bytes::new());
48    }
49
50    if bytes.len() < pkt_length {
51        tracing::warn!(
52            "Insufficient data: need {} bytes, have {}",
53            pkt_length,
54            bytes.len()
55        );
56        return (0, Bytes::new());
57    }
58
59    // this operation will change the original bytes
60    bytes.advance(4);
61    let data_length = pkt_length - 4;
62    let pkt_line = bytes.copy_to_bytes(data_length);
63    tracing::debug!("pkt line: {:?}", pkt_line);
64
65    (pkt_length, pkt_line)
66}
67
68/// Add a packet line string to the buffer with proper length prefix
69///
70/// This is the original simple implementation from ceres
71pub fn add_pkt_line_string(pkt_line_stream: &mut BytesMut, buf_str: String) {
72    let buf_str_length = buf_str.len() + 4;
73    pkt_line_stream.put(Bytes::from(format!("{buf_str_length:04x}")));
74    pkt_line_stream.put(buf_str.as_bytes());
75}
76
77/// Read until whitespace and return the extracted string
78///
79/// This is the original implementation from ceres
80pub fn read_until_white_space(bytes: &mut Bytes) -> String {
81    let mut buf = Vec::new();
82    while bytes.has_remaining() {
83        let c = bytes.get_u8();
84        if c.is_ascii_whitespace() || c == 0 {
85            break;
86        }
87        buf.push(c);
88    }
89    match String::from_utf8(buf) {
90        Ok(s) => s,
91        Err(e) => {
92            tracing::warn!("Invalid UTF-8 in protocol data: {}", e);
93            String::new() // Return empty string on invalid UTF-8
94        }
95    }
96}
97
98/// Build a smart reply packet line stream
99///
100/// This is the original simple implementation from ceres
101pub fn build_smart_reply(
102    transport_protocol: TransportProtocol,
103    ref_list: &[String],
104    service: String,
105) -> BytesMut {
106    let mut pkt_line_stream = BytesMut::new();
107    if transport_protocol == TransportProtocol::Http {
108        add_pkt_line_string(&mut pkt_line_stream, format!("# service={service}\n"));
109        pkt_line_stream.put(&PKT_LINE_END_MARKER[..]);
110    }
111
112    for ref_line in ref_list {
113        add_pkt_line_string(&mut pkt_line_stream, ref_line.to_string());
114    }
115    pkt_line_stream.put(&PKT_LINE_END_MARKER[..]);
116    pkt_line_stream
117}
118
119/// Search for a subsequence in a byte slice
120pub fn search_subsequence(haystack: &[u8], needle: &[u8]) -> Option<usize> {
121    haystack
122        .windows(needle.len())
123        .position(|window| window == needle)
124}
125
126#[cfg(test)]
127mod tests {
128    use bytes::Bytes;
129
130    use super::*;
131
132    /// Test that read_pkt_line correctly reads a complete packet line
133    #[test]
134    fn read_pkt_line_incomplete_does_not_consume() {
135        let mut buf = Bytes::from_static(b"0009do");
136        let before = buf.len();
137        let (len, data) = read_pkt_line(&mut buf);
138        assert_eq!(len, 0);
139        assert!(data.is_empty());
140        assert_eq!(buf.len(), before);
141    }
142}