⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/devtools/git/
transport.rs

1//! Git Network Transport
2//!
3//! Implements the Git smart HTTP protocol for clone, fetch, push, and pull
4//! operations. Wraps the existing `net::http` and `net::tls` modules.
5
6use alloc::{
7    string::{String, ToString},
8    vec::Vec,
9};
10
11use super::objects::ObjectId;
12
13/// Transport protocol
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub(crate) enum TransportProtocol {
16    /// Git smart HTTP/HTTPS
17    Http,
18    /// Git protocol (git://)
19    Git,
20    /// SSH (not yet implemented)
21    Ssh,
22}
23
24/// Remote repository reference
25#[derive(Debug, Clone)]
26pub(crate) struct RemoteRef {
27    pub(crate) id: ObjectId,
28    pub(crate) name: String,
29}
30
31/// Remote connection configuration
32#[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    /// Get info/refs URL for smart HTTP
59    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    /// Get upload-pack URL
65    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    /// Get receive-pack URL
71    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
77/// Git pkt-line protocol helpers
78pub mod pktline {
79    use alloc::vec::Vec;
80
81    /// Encode a pkt-line (4-char hex length prefix + data)
82    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    /// Encode a flush packet (0000)
91    pub(crate) fn flush() -> Vec<u8> {
92        b"0000".to_vec()
93    }
94
95    /// Encode a delimiter packet (0001)
96    pub(crate) fn delim() -> Vec<u8> {
97        b"0001".to_vec()
98    }
99
100    /// Decode a pkt-line from a buffer, returning (line_data, bytes_consumed)
101    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)); // Flush
111        }
112        if len == 1 {
113            return Some((Vec::new(), 4)); // Delimiter
114        }
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    /// Parse all pkt-lines from a buffer
124    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
141/// Parse server advertisement (info/refs response)
142pub(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        // Skip service declaration line
153        if text.starts_with('#') {
154            continue;
155        }
156
157        // Format: "sha1 refname\0capabilities" or "sha1 refname"
158        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
177/// Build a want/have negotiation request
178pub(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
203/// Build a push request
204pub(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/// Packfile header
213#[derive(Debug, Clone, Copy)]
214pub(crate) struct PackHeader {
215    pub(crate) version: u32,
216    pub(crate) num_objects: u32,
217}
218
219/// Parse packfile header ("PACK" + version + object count)
220pub(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// ---------------------------------------------------------------------------
238// Tests
239// ---------------------------------------------------------------------------
240
241#[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        // Should contain "want" and "done"
333        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}