Skip to main content

tig_core/
protov2.rs

1//! git protocol version 2 (gitprotocol-v2) の request 構築と response 解析。
2//!
3//! sans-io 方針のため、本モジュールはバイト列の変換のみを行い、HTTP の送受信は
4//! 呼び出し側の責務とする。smart HTTP (stateless-rpc) での使用を想定し、
5//! - capability advertisement: `GET $URL/info/refs?service=git-upload-pack`
6//! - ls-refs / fetch: `POST $URL/git-upload-pack`
7//!
8//! の各 body を扱う。GET / POST とも `Git-Protocol: version=2` header を要する。
9
10use alloc::vec::Vec;
11
12use crate::err::{Error, Result};
13use crate::oid::Oid;
14use crate::pkt::{self, Pkt, PktReader};
15
16/// capability advertisement の解析結果。使用するものだけを保持する。
17#[derive(Debug, Default, Clone, PartialEq, Eq)]
18pub struct Advertisement {
19    pub ls_refs: bool,
20    pub fetch: bool,
21    /// fetch が shallow (deepen) を受けるか。
22    pub fetch_shallow: bool,
23}
24
25/// `info/refs?service=git-upload-pack` の response body を解析する。
26pub fn parse_advertisement(body: &[u8]) -> Result<Advertisement> {
27    let mut r = PktReader::new(body);
28
29    // smart HTTP では "# service=git-upload-pack" 行と flush が前置される。
30    let mut first = r.expect_next()?;
31    if let Pkt::Data(data) = first
32        && pkt::trim_line(data).starts_with(b"# service=")
33    {
34        loop {
35            match r.expect_next()? {
36                Pkt::Flush => break,
37                Pkt::Data(_) => {}
38                _ => return Err(Error::Corrupt("service announcement")),
39            }
40        }
41        first = r.expect_next()?;
42    }
43
44    match first {
45        Pkt::Data(data) if pkt::trim_line(data) == b"version 2" => {}
46        _ => return Err(Error::Unsupported("protocol version (v2 required)")),
47    }
48
49    let mut adv = Advertisement::default();
50    loop {
51        match r.expect_next()? {
52            Pkt::Flush => return Ok(adv),
53            Pkt::Data(data) => {
54                let line = pkt::trim_line(data);
55                let (name, value) = match line.iter().position(|&b| b == b'=') {
56                    Some(eq) => (&line[..eq], &line[eq + 1..]),
57                    None => (line, &b""[..]),
58                };
59                match name {
60                    b"ls-refs" => adv.ls_refs = true,
61                    b"fetch" => {
62                        adv.fetch = true;
63                        adv.fetch_shallow = value.split(|&b| b == b' ').any(|f| f == b"shallow");
64                    }
65                    _ => {}
66                }
67            }
68            _ => return Err(Error::Corrupt("capability advertisement")),
69        }
70    }
71}
72
73/// ls-refs command の request body を構築する。
74///
75/// `prefixes` が空の場合は全 ref を要求する。symrefs と peel は常に要求する
76/// (HEAD の解決と annotated tag の情報のため)。
77pub fn ls_refs_request(prefixes: &[&[u8]]) -> Vec<u8> {
78    let mut out = Vec::new();
79    pkt::write_line(&mut out, b"command=ls-refs");
80    pkt::write_delim(&mut out);
81    pkt::write_line(&mut out, b"peel");
82    pkt::write_line(&mut out, b"symrefs");
83    for prefix in prefixes {
84        let mut line = Vec::with_capacity(11 + prefix.len());
85        line.extend_from_slice(b"ref-prefix ");
86        line.extend_from_slice(prefix);
87        pkt::write_line(&mut out, &line);
88    }
89    pkt::write_flush(&mut out);
90    out
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct RefEntry {
95    pub name: Vec<u8>,
96    pub oid: Oid,
97    /// annotated tag の指す先 (peel 属性)。
98    pub peeled: Option<Oid>,
99    /// symref (例: HEAD) の指す先の ref 名。
100    pub symref_target: Option<Vec<u8>>,
101}
102
103/// ls-refs の response body を解析する。
104pub fn parse_ls_refs(body: &[u8]) -> Result<Vec<RefEntry>> {
105    let mut r = PktReader::new(body);
106    let mut refs = Vec::new();
107    loop {
108        match r.expect_next()? {
109            Pkt::Flush | Pkt::ResponseEnd => return Ok(refs),
110            Pkt::Data(data) => {
111                let line = pkt::trim_line(data);
112                let mut fields = line.split(|&b| b == b' ');
113                let oid_hex = fields.next().ok_or(Error::Corrupt("ls-refs line"))?;
114                let name = fields.next().ok_or(Error::Corrupt("ls-refs line"))?;
115                let mut entry = RefEntry {
116                    name: name.to_vec(),
117                    oid: Oid::from_hex(oid_hex)?,
118                    peeled: None,
119                    symref_target: None,
120                };
121                for attr in fields {
122                    if let Some(v) = attr.strip_prefix(b"peeled:") {
123                        entry.peeled = Some(Oid::from_hex(v)?);
124                    } else if let Some(v) = attr.strip_prefix(b"symref-target:") {
125                        entry.symref_target = Some(v.to_vec());
126                    }
127                }
128                refs.push(entry);
129            }
130            _ => return Err(Error::Corrupt("ls-refs response")),
131        }
132    }
133}
134
135/// fetch command の request body を構築する。
136///
137/// negotiation は行わず常に done を送る (clone 相当)。`depth` を与えると
138/// shallow fetch (deepen) になる。delta の base は pack 内参照 (ofs-delta) を
139/// 許可する。
140pub fn fetch_request(wants: &[Oid], depth: Option<u32>) -> Vec<u8> {
141    let mut out = Vec::new();
142    pkt::write_line(&mut out, b"command=fetch");
143    pkt::write_delim(&mut out);
144    pkt::write_line(&mut out, b"no-progress");
145    pkt::write_line(&mut out, b"ofs-delta");
146    if let Some(depth) = depth {
147        let mut line = Vec::new();
148        line.extend_from_slice(b"deepen ");
149        push_decimal(&mut line, depth);
150        pkt::write_line(&mut out, &line);
151    }
152    for want in wants {
153        let mut line = Vec::with_capacity(45);
154        line.extend_from_slice(b"want ");
155        push_hex(&mut line, want);
156        pkt::write_line(&mut out, &line);
157    }
158    pkt::write_line(&mut out, b"done");
159    pkt::write_flush(&mut out);
160    out
161}
162
163#[derive(Debug, Default, PartialEq, Eq)]
164pub struct FetchResponse {
165    /// 履歴を打ち切った commit (shallow-info の shallow 行)。
166    pub shallow: Vec<Oid>,
167    pub pack: Vec<u8>,
168}
169
170/// fetch の response body を解析し、sideband を剥がした packfile を返す。
171pub fn parse_fetch_response(body: &[u8]) -> Result<FetchResponse> {
172    let mut r = PktReader::new(body);
173    let mut resp = FetchResponse::default();
174
175    // section header (テキスト行) を読み、対応する section を処理する。
176    loop {
177        match r.expect_next()? {
178            Pkt::Flush | Pkt::ResponseEnd => {
179                if resp.pack.is_empty() {
180                    return Err(Error::Corrupt("fetch response without packfile"));
181                }
182                return Ok(resp);
183            }
184            Pkt::Data(data) => match pkt::trim_line(data) {
185                b"shallow-info" => parse_shallow_info(&mut r, &mut resp)?,
186                b"acknowledgments" => skip_section(&mut r)?,
187                b"packfile" => {
188                    parse_packfile_section(&mut r, &mut resp)?;
189                    return Ok(resp);
190                }
191                _ => return Err(Error::Unsupported("fetch response section")),
192            },
193            _ => return Err(Error::Corrupt("fetch response")),
194        }
195    }
196}
197
198fn parse_shallow_info(r: &mut PktReader<'_>, resp: &mut FetchResponse) -> Result<()> {
199    loop {
200        match r.expect_next()? {
201            Pkt::Delim => return Ok(()),
202            Pkt::Flush | Pkt::ResponseEnd => return Err(Error::Corrupt("shallow-info section")),
203            Pkt::Data(data) => {
204                let line = pkt::trim_line(data);
205                if let Some(v) = line.strip_prefix(b"shallow ") {
206                    resp.shallow.push(Oid::from_hex(v)?);
207                }
208                // unshallow は clone では現れない。現れても無害なため無視する。
209            }
210        }
211    }
212}
213
214fn skip_section(r: &mut PktReader<'_>) -> Result<()> {
215    loop {
216        match r.expect_next()? {
217            Pkt::Delim => return Ok(()),
218            Pkt::Flush | Pkt::ResponseEnd => return Err(Error::Corrupt("truncated section")),
219            _ => {}
220        }
221    }
222}
223
224fn parse_packfile_section(r: &mut PktReader<'_>, resp: &mut FetchResponse) -> Result<()> {
225    loop {
226        match r.expect_next()? {
227            Pkt::Flush | Pkt::ResponseEnd => {
228                if resp.pack.is_empty() {
229                    return Err(Error::Corrupt("empty packfile section"));
230                }
231                return Ok(());
232            }
233            Pkt::Data(data) => {
234                let (&band, payload) = data.split_first().ok_or(Error::Corrupt("sideband pkt"))?;
235                match band {
236                    1 => resp.pack.extend_from_slice(payload),
237                    2 => {} // progress。no-progress を送っているが、来ても無視する
238                    3 => return Err(Error::Corrupt("remote error (sideband 3)")),
239                    _ => return Err(Error::Corrupt("sideband channel")),
240                }
241            }
242            _ => return Err(Error::Corrupt("packfile section")),
243        }
244    }
245}
246
247fn push_decimal(out: &mut Vec<u8>, mut value: u32) {
248    let mut digits = [0u8; 10];
249    let mut n = 0;
250    loop {
251        digits[n] = b'0' + (value % 10) as u8;
252        value /= 10;
253        n += 1;
254        if value == 0 {
255            break;
256        }
257    }
258    for i in (0..n).rev() {
259        out.push(digits[i]);
260    }
261}
262
263fn push_hex(out: &mut Vec<u8>, oid: &Oid) {
264    const HEX: &[u8; 16] = b"0123456789abcdef";
265    for &b in oid.as_bytes() {
266        out.push(HEX[usize::from(b >> 4)]);
267        out.push(HEX[usize::from(b & 0xf)]);
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274
275    fn oid(n: u8) -> Oid {
276        Oid::from_bytes([n; 20])
277    }
278
279    fn adv_body(with_service: bool, caps: &[&[u8]]) -> Vec<u8> {
280        let mut b = Vec::new();
281        if with_service {
282            pkt::write_line(&mut b, b"# service=git-upload-pack");
283            pkt::write_flush(&mut b);
284        }
285        pkt::write_line(&mut b, b"version 2");
286        for c in caps {
287            pkt::write_line(&mut b, c);
288        }
289        pkt::write_flush(&mut b);
290        b
291    }
292
293    #[test]
294    fn advertisement_with_service_prefix() {
295        let body = adv_body(
296            true,
297            &[
298                b"agent=git/2.54.0",
299                b"ls-refs=unborn",
300                b"fetch=shallow wait-for-done",
301            ],
302        );
303        let adv = parse_advertisement(&body).unwrap();
304        assert!(adv.ls_refs && adv.fetch && adv.fetch_shallow);
305    }
306
307    #[test]
308    fn advertisement_without_service_prefix() {
309        let body = adv_body(false, &[b"ls-refs", b"fetch"]);
310        let adv = parse_advertisement(&body).unwrap();
311        assert!(adv.ls_refs && adv.fetch && !adv.fetch_shallow);
312    }
313
314    #[test]
315    fn advertisement_v1_rejected() {
316        let mut body = Vec::new();
317        pkt::write_line(&mut body, b"version 1");
318        pkt::write_flush(&mut body);
319        assert!(parse_advertisement(&body).is_err());
320    }
321
322    #[test]
323    fn ls_refs_request_encoding() {
324        let body = ls_refs_request(&[b"refs/heads/"]);
325        let expected =
326            b"0014command=ls-refs\n00010009peel\n000csymrefs\n001bref-prefix refs/heads/\n0000";
327        assert_eq!(body, expected);
328    }
329
330    #[test]
331    fn ls_refs_response_parse() {
332        let hex1 = "1111111111111111111111111111111111111111";
333        let hex2 = "2222222222222222222222222222222222222222";
334        let mut body = Vec::new();
335        pkt::write_line(
336            &mut body,
337            format!("{hex1} HEAD symref-target:refs/heads/main").as_bytes(),
338        );
339        pkt::write_line(
340            &mut body,
341            format!("{hex1} refs/tags/v1 peeled:{hex2}").as_bytes(),
342        );
343        pkt::write_flush(&mut body);
344
345        let refs = parse_ls_refs(&body).unwrap();
346        assert_eq!(refs.len(), 2);
347        assert_eq!(refs[0].name, b"HEAD");
348        assert_eq!(
349            refs[0].symref_target.as_deref(),
350            Some(&b"refs/heads/main"[..])
351        );
352        assert_eq!(refs[1].oid, oid(0x11));
353        assert_eq!(refs[1].peeled, Some(oid(0x22)));
354    }
355
356    #[test]
357    fn fetch_request_contents() {
358        let body = fetch_request(&[oid(0xab)], Some(1));
359        let text = String::from_utf8_lossy(&body);
360        assert!(text.contains("command=fetch"));
361        assert!(text.contains("deepen 1"));
362        assert!(text.contains(&format!("want {}", oid(0xab))));
363        assert!(text.contains("done"));
364    }
365
366    #[test]
367    fn fetch_response_sideband() {
368        let mut body = Vec::new();
369        pkt::write_line(&mut body, b"shallow-info");
370        pkt::write_line(
371            &mut body,
372            b"shallow 1111111111111111111111111111111111111111",
373        );
374        pkt::write_delim(&mut body);
375        pkt::write_line(&mut body, b"packfile");
376        pkt::write_data(&mut body, &[1, b'P', b'A']);
377        pkt::write_data(&mut body, &[2, b'p', b'r', b'o', b'g']);
378        pkt::write_data(&mut body, &[1, b'C', b'K']);
379        pkt::write_flush(&mut body);
380
381        let resp = parse_fetch_response(&body).unwrap();
382        assert_eq!(resp.shallow, alloc::vec![oid(0x11)]);
383        assert_eq!(resp.pack, b"PACK");
384    }
385
386    #[test]
387    fn fetch_response_error_band() {
388        let mut body = Vec::new();
389        pkt::write_line(&mut body, b"packfile");
390        pkt::write_data(&mut body, &[3, b'n', b'g']);
391        pkt::write_flush(&mut body);
392        assert!(parse_fetch_response(&body).is_err());
393    }
394}