Skip to main content

tig_core/
protov0.rs

1//! receive-pack (push) の protocol version 0。
2//!
3//! push は protocol v2 に定義が無く、smart HTTP でも v0 の receive-pack を使う。
4//! - advertisement: `GET $URL/info/refs?service=git-receive-pack`
5//! - update: `POST $URL/git-receive-pack` (command 列 + flush + packfile)
6//!
7//! sans-io 方針は fetch 側と同じで、本 module はバイト列の変換のみを行う。
8
9use alloc::vec::Vec;
10
11use crate::err::{Error, Result};
12use crate::oid::Oid;
13use crate::pkt::{self, Pkt, PktReader};
14
15/// ref の新規作成・削除で使う全零の oid。
16pub const ZERO_OID: Oid = Oid::from_bytes([0; 20]);
17
18/// receive-pack の advertisement。使用する capability だけを保持する。
19#[derive(Debug, Default, Clone)]
20pub struct ReceiveAdvertisement {
21    /// remote に存在する ref (空 repository では空)。
22    pub refs: Vec<(Vec<u8>, Oid)>,
23    pub report_status: bool,
24    pub side_band_64k: bool,
25    pub delete_refs: bool,
26}
27
28/// `info/refs?service=git-receive-pack` の response body を解析する。
29pub fn parse_receive_advertisement(body: &[u8]) -> Result<ReceiveAdvertisement> {
30    let mut r = PktReader::new(body);
31
32    // smart HTTP の service 前置行。
33    let mut first = r.expect_next()?;
34    if let Pkt::Data(data) = first
35        && pkt::trim_line(data).starts_with(b"# service=")
36    {
37        loop {
38            match r.expect_next()? {
39                Pkt::Flush => break,
40                Pkt::Data(_) => {}
41                _ => return Err(Error::Corrupt("service announcement")),
42            }
43        }
44        first = r.expect_next()?;
45    }
46
47    let mut adv = ReceiveAdvertisement::default();
48
49    // 先頭行: "<oid> <name>\0<cap> <cap> ..."。空 repository では name が
50    // "capabilities^{}" になる。
51    let Pkt::Data(data) = first else {
52        return Err(Error::Corrupt("receive-pack advertisement"));
53    };
54    let line = pkt::trim_line(data);
55    let nul = line
56        .iter()
57        .position(|&b| b == 0)
58        .ok_or(Error::Corrupt("advertisement capabilities"))?;
59    for cap in line[nul + 1..].split(|&b| b == b' ') {
60        match cap {
61            b"report-status" => adv.report_status = true,
62            b"side-band-64k" => adv.side_band_64k = true,
63            b"delete-refs" => adv.delete_refs = true,
64            _ => {}
65        }
66    }
67    push_ref_line(&mut adv.refs, &line[..nul])?;
68
69    loop {
70        match r.expect_next()? {
71            Pkt::Flush => return Ok(adv),
72            Pkt::Data(data) => push_ref_line(&mut adv.refs, pkt::trim_line(data))?,
73            _ => return Err(Error::Corrupt("receive-pack advertisement")),
74        }
75    }
76}
77
78fn push_ref_line(refs: &mut Vec<(Vec<u8>, Oid)>, line: &[u8]) -> Result<()> {
79    let sp = line
80        .iter()
81        .position(|&b| b == b' ')
82        .ok_or(Error::Corrupt("advertisement ref line"))?;
83    let oid = Oid::from_hex(&line[..sp])?;
84    let name = &line[sp + 1..];
85    // 空 repository のダミー行は ref として扱わない。
86    if name != b"capabilities^{}" {
87        refs.push((name.to_vec(), oid));
88    }
89    Ok(())
90}
91
92/// ref の更新 command。
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct Command {
95    /// remote の現在値 (新規作成は [`ZERO_OID`])。
96    pub old: Oid,
97    pub new: Oid,
98    pub name: Vec<u8>,
99}
100
101/// update request の body を構築する。command 列 + flush の後に packfile が
102/// そのまま (pkt 化せずに) 続く。
103pub fn push_request(commands: &[Command], side_band_64k: bool, pack: &[u8]) -> Vec<u8> {
104    let mut out = Vec::with_capacity(pack.len() + commands.len() * 96 + 64);
105    for (i, command) in commands.iter().enumerate() {
106        let mut line = Vec::with_capacity(96);
107        push_hex(&mut line, &command.old);
108        line.push(b' ');
109        push_hex(&mut line, &command.new);
110        line.push(b' ');
111        line.extend_from_slice(&command.name);
112        if i == 0 {
113            line.push(0);
114            line.extend_from_slice(b"report-status");
115            if side_band_64k {
116                line.extend_from_slice(b" side-band-64k");
117            }
118            line.extend_from_slice(b" agent=tig/0.1");
119        }
120        pkt::write_line(&mut out, &line);
121    }
122    pkt::write_flush(&mut out);
123    out.extend_from_slice(pack);
124    out
125}
126
127/// report-status の解析結果。
128#[derive(Debug, Default, PartialEq, Eq)]
129pub struct PushReport {
130    pub unpack_ok: bool,
131    /// ref 名と、失敗した場合の理由。
132    pub results: Vec<(Vec<u8>, Option<Vec<u8>>)>,
133}
134
135impl PushReport {
136    /// unpack と全 ref の更新が成功したか。
137    pub fn is_success(&self) -> bool {
138        self.unpack_ok && self.results.iter().all(|(_, err)| err.is_none())
139    }
140}
141
142/// update request への response (report-status) を解析する。
143/// `side_band_64k` を要求した場合、report は band 1 に包まれて届く。
144pub fn parse_report_status(body: &[u8], side_band_64k: bool) -> Result<PushReport> {
145    let unwrapped;
146    let report_bytes = if side_band_64k {
147        let mut inner = Vec::new();
148        let mut r = PktReader::new(body);
149        loop {
150            match r.next() {
151                None => break,
152                Some(Ok(Pkt::Flush)) | Some(Ok(Pkt::ResponseEnd)) => break,
153                Some(Ok(Pkt::Data(data))) => {
154                    let (&band, payload) =
155                        data.split_first().ok_or(Error::Corrupt("sideband pkt"))?;
156                    match band {
157                        1 => inner.extend_from_slice(payload),
158                        2 => {}
159                        3 => return Err(Error::Corrupt("remote error (sideband 3)")),
160                        _ => return Err(Error::Corrupt("sideband channel")),
161                    }
162                }
163                Some(Ok(Pkt::Delim)) => return Err(Error::Corrupt("report-status")),
164                Some(Err(e)) => return Err(e),
165            }
166        }
167        unwrapped = inner;
168        unwrapped.as_slice()
169    } else {
170        body
171    };
172
173    let mut report = PushReport::default();
174    let mut r = PktReader::new(report_bytes);
175    loop {
176        match r.next() {
177            None | Some(Ok(Pkt::Flush)) => break,
178            Some(Ok(Pkt::Data(data))) => {
179                let line = pkt::trim_line(data);
180                if let Some(v) = line.strip_prefix(b"unpack ") {
181                    report.unpack_ok = v == b"ok";
182                } else if let Some(v) = line.strip_prefix(b"ok ") {
183                    report.results.push((v.to_vec(), None));
184                } else if let Some(v) = line.strip_prefix(b"ng ") {
185                    let (name, msg) = match v.iter().position(|&b| b == b' ') {
186                        Some(sp) => (&v[..sp], v[sp + 1..].to_vec()),
187                        None => (v, Vec::new()),
188                    };
189                    report.results.push((name.to_vec(), Some(msg)));
190                }
191            }
192            Some(Ok(_)) => return Err(Error::Corrupt("report-status")),
193            Some(Err(e)) => return Err(e),
194        }
195    }
196    Ok(report)
197}
198
199fn push_hex(out: &mut Vec<u8>, oid: &Oid) {
200    const HEX: &[u8; 16] = b"0123456789abcdef";
201    for &b in oid.as_bytes() {
202        out.push(HEX[usize::from(b >> 4)]);
203        out.push(HEX[usize::from(b & 0xf)]);
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    fn oid(n: u8) -> Oid {
212        Oid::from_bytes([n; 20])
213    }
214
215    #[test]
216    fn advertisement_with_refs() {
217        let mut b = Vec::new();
218        pkt::write_line(&mut b, b"# service=git-receive-pack");
219        pkt::write_flush(&mut b);
220        let hex = "1111111111111111111111111111111111111111";
221        pkt::write_line(
222            &mut b,
223            format!("{hex} refs/heads/main\0report-status side-band-64k delete-refs").as_bytes(),
224        );
225        pkt::write_line(&mut b, format!("{hex} refs/tags/v1").as_bytes());
226        pkt::write_flush(&mut b);
227
228        let adv = parse_receive_advertisement(&b).unwrap();
229        assert!(adv.report_status && adv.side_band_64k && adv.delete_refs);
230        assert_eq!(adv.refs.len(), 2);
231        assert_eq!(adv.refs[0].0, b"refs/heads/main");
232    }
233
234    #[test]
235    fn advertisement_empty_repository() {
236        let mut b = Vec::new();
237        pkt::write_line(&mut b, b"# service=git-receive-pack");
238        pkt::write_flush(&mut b);
239        pkt::write_line(
240            &mut b,
241            b"0000000000000000000000000000000000000000 capabilities^{}\0report-status",
242        );
243        pkt::write_flush(&mut b);
244
245        let adv = parse_receive_advertisement(&b).unwrap();
246        assert!(adv.report_status);
247        assert!(adv.refs.is_empty());
248    }
249
250    #[test]
251    fn push_request_layout() {
252        let commands = [Command {
253            old: ZERO_OID,
254            new: oid(0x11),
255            name: b"refs/heads/main".to_vec(),
256        }];
257        let body = push_request(&commands, true, b"PACKDATA");
258        let text = String::from_utf8_lossy(&body);
259        assert!(text.contains("report-status side-band-64k"));
260        assert!(body.ends_with(b"PACKDATA"));
261        // command 部は pkt として読める。
262        let mut r = PktReader::new(&body);
263        assert!(matches!(r.expect_next().unwrap(), Pkt::Data(_)));
264        assert_eq!(r.expect_next().unwrap(), Pkt::Flush);
265        assert_eq!(r.rest(), b"PACKDATA");
266    }
267
268    #[test]
269    fn report_status_sideband() {
270        let mut inner = Vec::new();
271        pkt::write_line(&mut inner, b"unpack ok");
272        pkt::write_line(&mut inner, b"ok refs/heads/main");
273        pkt::write_line(&mut inner, b"ng refs/heads/x non-fast-forward");
274        pkt::write_flush(&mut inner);
275
276        let mut body = Vec::new();
277        let mut payload = alloc::vec![1u8];
278        payload.extend_from_slice(&inner);
279        pkt::write_data(&mut body, &payload);
280        pkt::write_flush(&mut body);
281
282        let report = parse_report_status(&body, true).unwrap();
283        assert!(report.unpack_ok);
284        assert!(!report.is_success());
285        assert_eq!(report.results.len(), 2);
286        assert_eq!(
287            report.results[1].1.as_deref(),
288            Some(&b"non-fast-forward"[..])
289        );
290    }
291
292    #[test]
293    fn report_status_plain() {
294        let mut body = Vec::new();
295        pkt::write_line(&mut body, b"unpack ok");
296        pkt::write_line(&mut body, b"ok refs/heads/main");
297        pkt::write_flush(&mut body);
298
299        let report = parse_report_status(&body, false).unwrap();
300        assert!(report.is_success());
301    }
302}