veridian_kernel/devtools/git/
transport.rs1use alloc::{
7 string::{String, ToString},
8 vec::Vec,
9};
10
11use super::objects::ObjectId;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub(crate) enum TransportProtocol {
16 Http,
18 Git,
20 Ssh,
22}
23
24#[derive(Debug, Clone)]
26pub(crate) struct RemoteRef {
27 pub(crate) id: ObjectId,
28 pub(crate) name: String,
29}
30
31#[derive(Debug, Clone)]
33pub(crate) struct RemoteConfig {
34 pub(crate) name: String,
35 pub(crate) url: String,
36 pub(crate) protocol: TransportProtocol,
37}
38
39impl RemoteConfig {
40 pub(crate) fn new(name: &str, url: &str) -> Self {
41 let protocol = if url.starts_with("https://") || url.starts_with("http://") {
42 TransportProtocol::Http
43 } else if url.starts_with("git://") {
44 TransportProtocol::Git
45 } else if url.starts_with("ssh://") || url.contains('@') {
46 TransportProtocol::Ssh
47 } else {
48 TransportProtocol::Http
49 };
50
51 Self {
52 name: name.to_string(),
53 url: url.to_string(),
54 protocol,
55 }
56 }
57
58 pub(crate) fn info_refs_url(&self) -> String {
60 let base = self.url.trim_end_matches('/');
61 alloc::format!("{}/info/refs?service=git-upload-pack", base)
62 }
63
64 pub(crate) fn upload_pack_url(&self) -> String {
66 let base = self.url.trim_end_matches('/');
67 alloc::format!("{}/git-upload-pack", base)
68 }
69
70 pub(crate) fn receive_pack_url(&self) -> String {
72 let base = self.url.trim_end_matches('/');
73 alloc::format!("{}/git-receive-pack", base)
74 }
75}
76
77pub mod pktline {
79 use alloc::vec::Vec;
80
81 pub(crate) fn encode(data: &[u8]) -> Vec<u8> {
83 let len = data.len() + 4;
84 let mut buf = Vec::with_capacity(len);
85 buf.extend_from_slice(alloc::format!("{:04x}", len).as_bytes());
86 buf.extend_from_slice(data);
87 buf
88 }
89
90 pub(crate) fn flush() -> Vec<u8> {
92 b"0000".to_vec()
93 }
94
95 pub(crate) fn delim() -> Vec<u8> {
97 b"0001".to_vec()
98 }
99
100 pub(crate) fn decode(data: &[u8]) -> Option<(Vec<u8>, usize)> {
102 if data.len() < 4 {
103 return None;
104 }
105
106 let len_str = core::str::from_utf8(&data[..4]).ok()?;
107 let len = usize::from_str_radix(len_str, 16).ok()?;
108
109 if len == 0 {
110 return Some((Vec::new(), 4)); }
112 if len == 1 {
113 return Some((Vec::new(), 4)); }
115 if len < 4 || data.len() < len {
116 return None;
117 }
118
119 let line_data = data[4..len].to_vec();
120 Some((line_data, len))
121 }
122
123 pub(crate) fn parse_all(mut data: &[u8]) -> Vec<Vec<u8>> {
125 let mut lines = Vec::new();
126 while !data.is_empty() {
127 match decode(data) {
128 Some((line, consumed)) => {
129 if !line.is_empty() {
130 lines.push(line);
131 }
132 data = &data[consumed..];
133 }
134 None => break,
135 }
136 }
137 lines
138 }
139}
140
141pub(crate) fn parse_refs_advertisement(data: &[u8]) -> Vec<RemoteRef> {
143 let mut refs = Vec::new();
144
145 let lines = pktline::parse_all(data);
146 for line in &lines {
147 let text = match core::str::from_utf8(line) {
148 Ok(t) => t,
149 Err(_) => continue,
150 };
151
152 if text.starts_with('#') {
154 continue;
155 }
156
157 let text = text.split('\0').next().unwrap_or(text);
159 let parts: Vec<&str> = text.splitn(2, ' ').collect();
160 if parts.len() == 2 {
161 let hex = parts[0].trim();
162 let name = parts[1].trim();
163 if hex.len() == 40 {
164 if let Some(id) = ObjectId::from_hex(hex) {
165 refs.push(RemoteRef {
166 id,
167 name: name.to_string(),
168 });
169 }
170 }
171 }
172 }
173
174 refs
175}
176
177pub(crate) fn build_want_request(wants: &[ObjectId], haves: &[ObjectId]) -> Vec<u8> {
179 let mut buf = Vec::new();
180
181 for (i, want) in wants.iter().enumerate() {
182 let line = if i == 0 {
183 alloc::format!("want {} multi_ack_detailed side-band-64k ofs-delta\n", want)
184 } else {
185 alloc::format!("want {}\n", want)
186 };
187 buf.extend_from_slice(&pktline::encode(line.as_bytes()));
188 }
189
190 buf.extend_from_slice(&pktline::flush());
191
192 for have in haves {
193 let line = alloc::format!("have {}\n", have);
194 buf.extend_from_slice(&pktline::encode(line.as_bytes()));
195 }
196
197 let done = b"done\n";
198 buf.extend_from_slice(&pktline::encode(done));
199
200 buf
201}
202
203pub(crate) fn build_push_request(old_id: &ObjectId, new_id: &ObjectId, ref_name: &str) -> Vec<u8> {
205 let mut buf = Vec::new();
206 let line = alloc::format!("{} {} {}\0report-status\n", old_id, new_id, ref_name);
207 buf.extend_from_slice(&pktline::encode(line.as_bytes()));
208 buf.extend_from_slice(&pktline::flush());
209 buf
210}
211
212#[derive(Debug, Clone, Copy)]
214pub(crate) struct PackHeader {
215 pub(crate) version: u32,
216 pub(crate) num_objects: u32,
217}
218
219pub(crate) fn parse_pack_header(data: &[u8]) -> Option<PackHeader> {
221 if data.len() < 12 {
222 return None;
223 }
224 if &data[0..4] != b"PACK" {
225 return None;
226 }
227
228 let version = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
229 let num_objects = u32::from_be_bytes([data[8], data[9], data[10], data[11]]);
230
231 Some(PackHeader {
232 version,
233 num_objects,
234 })
235}
236
237#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn test_remote_config_http() {
247 let remote = RemoteConfig::new("origin", "https://github.com/user/repo.git");
248 assert_eq!(remote.protocol, TransportProtocol::Http);
249 }
250
251 #[test]
252 fn test_remote_config_git() {
253 let remote = RemoteConfig::new("origin", "git://github.com/user/repo.git");
254 assert_eq!(remote.protocol, TransportProtocol::Git);
255 }
256
257 #[test]
258 fn test_remote_config_ssh() {
259 let remote = RemoteConfig::new("origin", "ssh://git@github.com/user/repo.git");
260 assert_eq!(remote.protocol, TransportProtocol::Ssh);
261 }
262
263 #[test]
264 fn test_remote_urls() {
265 let remote = RemoteConfig::new("origin", "https://example.com/repo");
266 assert!(remote.info_refs_url().contains("info/refs"));
267 assert!(remote.upload_pack_url().contains("git-upload-pack"));
268 assert!(remote.receive_pack_url().contains("git-receive-pack"));
269 }
270
271 #[test]
272 fn test_pktline_encode() {
273 let encoded = pktline::encode(b"hello\n");
274 assert_eq!(&encoded[..4], b"000a");
275 assert_eq!(&encoded[4..], b"hello\n");
276 }
277
278 #[test]
279 fn test_pktline_flush() {
280 assert_eq!(pktline::flush(), b"0000");
281 }
282
283 #[test]
284 fn test_pktline_delim() {
285 assert_eq!(pktline::delim(), b"0001");
286 }
287
288 #[test]
289 fn test_pktline_decode() {
290 let data = b"000ahello\n";
291 let (line, consumed) = pktline::decode(data).unwrap();
292 assert_eq!(&line, b"hello\n");
293 assert_eq!(consumed, 10);
294 }
295
296 #[test]
297 fn test_pktline_decode_flush() {
298 let data = b"0000";
299 let (line, consumed) = pktline::decode(data).unwrap();
300 assert!(line.is_empty());
301 assert_eq!(consumed, 4);
302 }
303
304 #[test]
305 fn test_pktline_decode_too_short() {
306 assert!(pktline::decode(b"00").is_none());
307 }
308
309 #[test]
310 fn test_parse_pack_header() {
311 let mut data = Vec::new();
312 data.extend_from_slice(b"PACK");
313 data.extend_from_slice(&2u32.to_be_bytes());
314 data.extend_from_slice(&42u32.to_be_bytes());
315
316 let header = parse_pack_header(&data).unwrap();
317 assert_eq!(header.version, 2);
318 assert_eq!(header.num_objects, 42);
319 }
320
321 #[test]
322 fn test_parse_pack_header_invalid() {
323 assert!(parse_pack_header(b"NOTPACK").is_none());
324 assert!(parse_pack_header(b"PAC").is_none());
325 }
326
327 #[test]
328 fn test_build_want_request() {
329 let want = ObjectId::from_hex("da39a3ee5e6b4b0d3255bfef95601890afd80709").unwrap();
330 let req = build_want_request(&[want], &[]);
331 assert!(!req.is_empty());
332 let text = String::from_utf8_lossy(&req);
334 assert!(text.contains("want"));
335 assert!(text.contains("done"));
336 }
337
338 #[test]
339 fn test_build_push_request() {
340 let old = ObjectId::ZERO;
341 let new = ObjectId::from_hex("da39a3ee5e6b4b0d3255bfef95601890afd80709").unwrap();
342 let req = build_push_request(&old, &new, "refs/heads/main");
343 let text = String::from_utf8_lossy(&req);
344 assert!(text.contains("refs/heads/main"));
345 }
346
347 #[test]
348 fn test_parse_refs_advertisement() {
349 let hex = "da39a3ee5e6b4b0d3255bfef95601890afd80709";
350 let line = alloc::format!("{} refs/heads/main\0multi_ack\n", hex);
351 let pkt = pktline::encode(line.as_bytes());
352
353 let refs = parse_refs_advertisement(&pkt);
354 assert_eq!(refs.len(), 1);
355 assert_eq!(refs[0].name, "refs/heads/main");
356 assert_eq!(refs[0].id.to_hex(), hex);
357 }
358
359 #[test]
360 fn test_pktline_parse_all() {
361 let mut buf = Vec::new();
362 buf.extend_from_slice(&pktline::encode(b"line1\n"));
363 buf.extend_from_slice(&pktline::encode(b"line2\n"));
364 buf.extend_from_slice(&pktline::flush());
365
366 let lines = pktline::parse_all(&buf);
367 assert_eq!(lines.len(), 2);
368 }
369
370 #[test]
371 fn test_transport_protocol_eq() {
372 assert_eq!(TransportProtocol::Http, TransportProtocol::Http);
373 assert_ne!(TransportProtocol::Http, TransportProtocol::Git);
374 }
375}