1use alloc::vec::Vec;
10
11use crate::err::{Error, Result};
12use crate::oid::Oid;
13use crate::pkt::{self, Pkt, PktReader};
14
15pub const ZERO_OID: Oid = Oid::from_bytes([0; 20]);
17
18#[derive(Debug, Default, Clone)]
20pub struct ReceiveAdvertisement {
21 pub refs: Vec<(Vec<u8>, Oid)>,
23 pub report_status: bool,
24 pub side_band_64k: bool,
25 pub delete_refs: bool,
26}
27
28pub fn parse_receive_advertisement(body: &[u8]) -> Result<ReceiveAdvertisement> {
30 let mut r = PktReader::new(body);
31
32 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 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 if name != b"capabilities^{}" {
87 refs.push((name.to_vec(), oid));
88 }
89 Ok(())
90}
91
92#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct Command {
95 pub old: Oid,
97 pub new: Oid,
98 pub name: Vec<u8>,
99}
100
101pub 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#[derive(Debug, Default, PartialEq, Eq)]
129pub struct PushReport {
130 pub unpack_ok: bool,
131 pub results: Vec<(Vec<u8>, Option<Vec<u8>>)>,
133}
134
135impl PushReport {
136 pub fn is_success(&self) -> bool {
138 self.unpack_ok && self.results.iter().all(|(_, err)| err.is_none())
139 }
140}
141
142pub 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 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}