upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/git/protocol.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/git/protocol.rs')
-rw-r--r--src/git/protocol.rs254
1 files changed, 254 insertions, 0 deletions
diff --git a/src/git/protocol.rs b/src/git/protocol.rs
new file mode 100644
index 0000000..84da131
--- /dev/null
+++ b/src/git/protocol.rs
@@ -0,0 +1,254 @@
1//! Git Smart HTTP Protocol Implementation
2//!
3//! This module implements the Git pkt-line format and protocol utilities.
4//!
5//! # Pkt-line Format
6//!
7//! A pkt-line is a variable length binary string with a 4-byte length prefix:
8//! - First 4 bytes: hex digits representing total length (including these 4 bytes)
9//! - Remaining bytes: payload data
10//! - Special case "0000": flush packet (end of section)
11//!
12//! # References
13//! - https://git-scm.com/docs/protocol-common#_pkt_line_format
14
15use std::fmt;
16
17/// Represents a Git pkt-line packet
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum PktLine {
20 /// Data packet with payload
21 Data(Vec<u8>),
22 /// Flush packet (0000)
23 Flush,
24}
25
26impl PktLine {
27 /// Create a data packet from bytes
28 pub fn data(data: impl Into<Vec<u8>>) -> Self {
29 Self::Data(data.into())
30 }
31
32 /// Create a flush packet
33 pub fn flush() -> Self {
34 Self::Flush
35 }
36
37 /// Encode this packet to wire format
38 pub fn encode(&self) -> Vec<u8> {
39 match self {
40 PktLine::Flush => b"0000".to_vec(),
41 PktLine::Data(data) => {
42 let len = data.len() + 4;
43 let mut result = Vec::with_capacity(len);
44 result.extend_from_slice(format!("{:04x}", len).as_bytes());
45 result.extend_from_slice(data);
46 result
47 }
48 }
49 }
50
51 /// Parse a single pkt-line from bytes
52 /// Returns (packet, remaining_bytes)
53 pub fn parse(input: &[u8]) -> Result<(Self, &[u8]), ProtocolError> {
54 if input.len() < 4 {
55 return Err(ProtocolError::InsufficientData);
56 }
57
58 let len_str = std::str::from_utf8(&input[0..4])
59 .map_err(|_| ProtocolError::InvalidLength)?;
60
61 let len = u16::from_str_radix(len_str, 16)
62 .map_err(|_| ProtocolError::InvalidLength)? as usize;
63
64 if len == 0 {
65 // Flush packet
66 return Ok((PktLine::Flush, &input[4..]));
67 }
68
69 if len < 4 {
70 return Err(ProtocolError::InvalidLength);
71 }
72
73 if input.len() < len {
74 return Err(ProtocolError::InsufficientData);
75 }
76
77 let data = input[4..len].to_vec();
78 Ok((PktLine::Data(data), &input[len..]))
79 }
80
81 /// Parse all pkt-lines from bytes
82 pub fn parse_all(mut input: &[u8]) -> Result<Vec<Self>, ProtocolError> {
83 let mut packets = Vec::new();
84
85 while !input.is_empty() {
86 let (packet, remaining) = Self::parse(input)?;
87 let is_flush = matches!(packet, PktLine::Flush);
88 packets.push(packet);
89 input = remaining;
90
91 // Stop at flush packet
92 if is_flush {
93 break;
94 }
95 }
96
97 Ok(packets)
98 }
99}
100
101/// Errors that can occur during protocol parsing
102#[derive(Debug)]
103pub enum ProtocolError {
104 /// Not enough data to parse a complete packet
105 InsufficientData,
106 /// Invalid length prefix
107 InvalidLength,
108 /// Invalid UTF-8 in packet data
109 InvalidUtf8,
110 /// IO error
111 Io(std::io::Error),
112}
113
114impl fmt::Display for ProtocolError {
115 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116 match self {
117 Self::InsufficientData => write!(f, "insufficient data for pkt-line"),
118 Self::InvalidLength => write!(f, "invalid pkt-line length"),
119 Self::InvalidUtf8 => write!(f, "invalid UTF-8 in pkt-line"),
120 Self::Io(e) => write!(f, "IO error: {}", e),
121 }
122 }
123}
124
125impl std::error::Error for ProtocolError {}
126
127impl From<std::io::Error> for ProtocolError {
128 fn from(e: std::io::Error) -> Self {
129 Self::Io(e)
130 }
131}
132
133/// Git service type
134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
135pub enum GitService {
136 /// Upload pack (clone/fetch)
137 UploadPack,
138 /// Receive pack (push)
139 ReceivePack,
140}
141
142impl GitService {
143 /// Parse service from query parameter
144 pub fn from_query_param(service: &str) -> Option<Self> {
145 match service {
146 "git-upload-pack" => Some(Self::UploadPack),
147 "git-receive-pack" => Some(Self::ReceivePack),
148 _ => None,
149 }
150 }
151
152 /// Get the service name as used in Git protocol
153 pub fn as_str(&self) -> &'static str {
154 match self {
155 Self::UploadPack => "git-upload-pack",
156 Self::ReceivePack => "git-receive-pack",
157 }
158 }
159
160 /// Get the content type for the service advertisement
161 pub fn advertisement_content_type(&self) -> &'static str {
162 match self {
163 Self::UploadPack => "application/x-git-upload-pack-advertisement",
164 Self::ReceivePack => "application/x-git-receive-pack-advertisement",
165 }
166 }
167
168 /// Get the content type for the service result
169 pub fn result_content_type(&self) -> &'static str {
170 match self {
171 Self::UploadPack => "application/x-git-upload-pack-result",
172 Self::ReceivePack => "application/x-git-receive-pack-result",
173 }
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 #[test]
182 fn test_pktline_encode_flush() {
183 let pkt = PktLine::flush();
184 assert_eq!(pkt.encode(), b"0000");
185 }
186
187 #[test]
188 fn test_pktline_encode_data() {
189 let pkt = PktLine::data(b"hello");
190 assert_eq!(pkt.encode(), b"0009hello");
191 }
192
193 #[test]
194 fn test_pktline_parse_flush() {
195 let (pkt, remaining) = PktLine::parse(b"0000extra").unwrap();
196 assert_eq!(pkt, PktLine::Flush);
197 assert_eq!(remaining, b"extra");
198 }
199
200 #[test]
201 fn test_pktline_parse_data() {
202 let (pkt, remaining) = PktLine::parse(b"0009helloworld").unwrap();
203 assert_eq!(pkt, PktLine::data(b"hello"));
204 assert_eq!(remaining, b"world");
205 }
206
207 #[test]
208 fn test_pktline_parse_insufficient_data() {
209 let result = PktLine::parse(b"000");
210 assert!(matches!(result, Err(ProtocolError::InsufficientData)));
211 }
212
213 #[test]
214 fn test_pktline_parse_invalid_length() {
215 let result = PktLine::parse(b"xxxx");
216 assert!(matches!(result, Err(ProtocolError::InvalidLength)));
217 }
218
219 #[test]
220 fn test_pktline_parse_all() {
221 let input = b"0009hello000aworld\n0000";
222 let packets = PktLine::parse_all(input).unwrap();
223 assert_eq!(packets.len(), 3);
224 assert_eq!(packets[0], PktLine::data(b"hello"));
225 assert_eq!(packets[1], PktLine::data(b"world\n"));
226 assert_eq!(packets[2], PktLine::Flush);
227 }
228
229 #[test]
230 fn test_git_service_from_query() {
231 assert_eq!(
232 GitService::from_query_param("git-upload-pack"),
233 Some(GitService::UploadPack)
234 );
235 assert_eq!(
236 GitService::from_query_param("git-receive-pack"),
237 Some(GitService::ReceivePack)
238 );
239 assert_eq!(GitService::from_query_param("invalid"), None);
240 }
241
242 #[test]
243 fn test_git_service_content_types() {
244 let upload = GitService::UploadPack;
245 assert_eq!(
246 upload.advertisement_content_type(),
247 "application/x-git-upload-pack-advertisement"
248 );
249 assert_eq!(
250 upload.result_content_type(),
251 "application/x-git-upload-pack-result"
252 );
253 }
254} \ No newline at end of file