Skip to main content

git_zero_client/
smart.rs

1//! Protocol-v0 discovery, bounded fetch negotiation and raw-pack responses.
2use super::{Protocol, Reference, References};
3use git_zero_core::{
4    Error, ObjectFormat, ObjectId, Result,
5    io::{SliceWriter, Write},
6    pack,
7    pktline::{self, Packet},
8};
9
10/// A validated smart-HTTP advertisement. Empty repositories are valid but have
11/// no HEAD. Capabilities and reference names borrow the caller's response.
12pub struct SmartAdvertisement<'a> {
13    bytes: &'a [u8],
14    body_offset: usize,
15    head: Option<ObjectId>,
16    capabilities: &'a [u8],
17    format: Option<ObjectFormat>,
18}
19
20impl<'a> SmartAdvertisement<'a> {
21    pub fn parse(data: &'a [u8]) -> Result<Self> {
22        let body = advertisement_body(data)?;
23        let mut first = body;
24        let capabilities = match pktline::read(&mut first)? {
25            Some(Packet::Data(line)) => line.splitn(2, |&b| b == 0).nth(1).unwrap_or_default(),
26            _ => &[],
27        };
28        let mut head = None;
29        let mut refs = References {
30            protocol: Protocol::Smart,
31            bytes: body,
32            failed: false,
33            format: None,
34        };
35        for entry in &mut refs {
36            let entry = entry?;
37            if entry.name == "HEAD" && head.replace(entry.id).is_some() {
38                return Err(Error::InvalidReference);
39            }
40        }
41        Ok(Self {
42            bytes: data,
43            body_offset: data.len() - body.len(),
44            head,
45            capabilities,
46            format: refs.format,
47        })
48    }
49
50    pub fn head(&self) -> Result<ObjectId> {
51        self.head.ok_or(Error::ObjectNotFound)
52    }
53    /// Resolve HEAD or a normalized fully qualified ref, including tags.
54    pub fn resolve_reference(&self, name: &str) -> Result<ObjectId> {
55        if name == "HEAD" {
56            return self.head();
57        }
58        git_zero_core::repository::validate_ref_name(name)?;
59        let mut found = None;
60        for entry in self.references() {
61            let entry = entry?;
62            if entry.name == name && found.replace(entry.id).is_some() {
63                return Err(Error::InvalidReference);
64            }
65        }
66        found.ok_or(Error::ObjectNotFound)
67    }
68    pub fn object_format(&self) -> Option<ObjectFormat> {
69        self.format
70    }
71    pub fn supports_shallow(&self) -> bool {
72        self.supports("shallow")
73    }
74    /// Test an exact advertised capability token, without accepting prefixes.
75    pub fn supports(&self, capability: &str) -> bool {
76        self.capabilities
77            .split(|b| b.is_ascii_whitespace())
78            .any(|flag| flag == capability.as_bytes())
79    }
80    /// Request only capabilities advertised by this origin. An origin without
81    /// shallow support falls back to a full fetch within the host's budgets.
82    pub fn fetch_options(&self, shallow_depth: Option<u32>) -> FetchOptions {
83        FetchOptions {
84            shallow_depth: shallow_depth.filter(|_| self.supports_shallow()),
85            ofs_delta: self.supports("ofs-delta"),
86            no_progress: self.supports("no-progress"),
87        }
88    }
89    /// Validate protocol permission to request an ID. Unadvertised IDs require
90    /// an allow-*-sha1-in-want capability; the server still checks whether the ID
91    /// is an allowed hidden tip or reachable object under that capability.
92    pub fn validate_want(&self, id: ObjectId) -> Result<()> {
93        if self.format.is_some_and(|format| format != id.format()) {
94            return Err(Error::InvalidRequest);
95        }
96        if self.supports("allow-tip-sha1-in-want") || self.supports("allow-reachable-sha1-in-want")
97        {
98            return Ok(());
99        }
100        for reference in self.references() {
101            if reference?.id == id {
102                return Ok(());
103            }
104        }
105        Err(Error::InvalidRequest)
106    }
107    pub fn references(&self) -> References<'a> {
108        References {
109            protocol: Protocol::Smart,
110            bytes: &self.bytes[self.body_offset..],
111            failed: false,
112            format: self.format,
113        }
114    }
115}
116
117pub(super) fn advertisement_body(mut data: &[u8]) -> Result<&[u8]> {
118    if !matches!(
119        pktline::read(&mut data)?,
120        Some(Packet::Data(b"# service=git-upload-pack\n"))
121    ) || !matches!(pktline::read(&mut data)?, Some(Packet::Flush))
122    {
123        return Err(Error::InvalidPacket);
124    }
125    Ok(data)
126}
127
128pub(super) fn next_reference<'a>(
129    bytes: &mut &'a [u8],
130    format: &mut Option<ObjectFormat>,
131) -> Result<Option<Reference<'a>>> {
132    loop {
133        match pktline::read(bytes)? {
134            Some(Packet::Data(line)) => {
135                let mut parser = References {
136                    protocol: Protocol::Smart,
137                    bytes: &[],
138                    failed: false,
139                    format: *format,
140                };
141                let reference = parser.parse(line)?;
142                *format = parser.format;
143                if reference.id.is_zero() && reference.name == "capabilities^{}" {
144                    continue;
145                }
146                if reference.name != "HEAD" {
147                    git_zero_core::repository::validate_ref_name(
148                        reference.name.strip_suffix("^{}").unwrap_or(reference.name),
149                    )?;
150                }
151                return Ok(Some(reference));
152            }
153            Some(Packet::Flush) if bytes.is_empty() => return Ok(None),
154            _ => return Err(Error::InvalidPacket),
155        }
156    }
157}
158
159/// Resume a smart advertisement at a byte cursor. Start at zero; successful
160/// reads advance past one ref, including peeled tag pseudo-refs. EOF is stable.
161/// Validate the complete advertisement with SmartAdvertisement::parse first
162/// when exposing refs incrementally to callers.
163pub fn read_smart_reference<'a>(
164    data: &'a [u8],
165    offset: &mut usize,
166) -> Result<Option<Reference<'a>>> {
167    if *offset == data.len() && *offset != 0 {
168        return Ok(None);
169    }
170    let body = advertisement_body(data)?;
171    let mut first = body;
172    let mut parser = References {
173        protocol: Protocol::Smart,
174        bytes: &[],
175        failed: false,
176        format: None,
177    };
178    if let Some(Packet::Data(line)) = pktline::read(&mut first)? {
179        parser.parse(line)?;
180    }
181    let mut bytes = if *offset == 0 {
182        body
183    } else {
184        data.get(*offset..).ok_or(Error::InvalidPacket)?
185    };
186    let entry = next_reference(&mut bytes, &mut parser.format)?;
187    *offset = data.len() - bytes.len();
188    Ok(entry)
189}
190
191#[derive(Clone, Copy, Debug, Default)]
192pub struct FetchOptions {
193    /// None fetches full history; Some(1) fetches each wanted tip's snapshot.
194    /// The host must check the advertisement's shallow capability first.
195    pub shallow_depth: Option<u32>,
196    pub ofs_delta: bool,
197    pub no_progress: bool,
198}
199
200/// Encode a protocol-v0 request without thin packs or sideband. Duplicate
201/// wants are omitted using bounded quadratic probes over caller-owned IDs.
202/// Invalid inputs emit nothing; discard partial output on storage/I/O error.
203pub fn write_fetch_request(
204    wants: &[ObjectId],
205    options: FetchOptions,
206    output: &mut impl Write,
207) -> Result<()> {
208    write_fetch_request_iter(wants.iter().copied(), options, output)
209}
210
211/// Encode a cloneable iterator of IDs, avoiding temporary ID arrays in adapters.
212/// Duplicate probes reread the iterator; it must yield the same IDs on every
213/// clone. All wants use one format. Validation precedes any output.
214pub fn write_fetch_request_iter(
215    wants: impl Iterator<Item = ObjectId> + Clone,
216    options: FetchOptions,
217    output: &mut impl Write,
218) -> Result<()> {
219    let format = wants.clone().next().ok_or(Error::InvalidRequest)?.format();
220    if options.shallow_depth == Some(0) || wants.clone().any(|want| want.format() != format) {
221        return Err(Error::InvalidRequest);
222    }
223    for (position, want) in wants.clone().enumerate() {
224        if wants
225            .clone()
226            .take(position)
227            .any(|previous| previous == want)
228        {
229            continue;
230        }
231        let mut storage = [0; 128];
232        let mut line = SliceWriter::new(&mut storage);
233        line.write_all(b"want ")?;
234        line.write_all(&want.to_hex())?;
235        if position == 0 {
236            if format == ObjectFormat::Sha256 {
237                line.write_all(b" ")?;
238                line.write_all(format.capability())?;
239            }
240            if options.ofs_delta {
241                line.write_all(b" ofs-delta")?;
242            }
243            if options.no_progress {
244                line.write_all(b" no-progress")?;
245            }
246            if options.shallow_depth.is_some() {
247                line.write_all(b" shallow")?;
248            }
249        }
250        line.write_all(b"\n")?;
251        pktline::write(output, line.as_bytes())?;
252    }
253    if let Some(depth) = options.shallow_depth {
254        use core::fmt::Write as _;
255        let mut storage = [0; 32];
256        let mut line = SliceWriter::new(&mut storage);
257        writeln!(&mut line, "deepen {depth}").map_err(|_| Error::BufferTooSmall)?;
258        pktline::write(output, line.as_bytes())?;
259    }
260    output.write_all(b"0000")?;
261    pktline::write(output, b"done\n")
262}
263
264/// Extract a checksummed, non-sideband pack after optional shallow negotiation.
265/// ACK/have negotiation and protocol v2 are intentionally outside this parser.
266pub fn read_pack_response(bytes: &[u8]) -> Result<&[u8]> {
267    read_pack_response_with_format(bytes, ObjectFormat::Sha1)
268}
269
270/// Validate a response in the repository's negotiated object format, including
271/// each shallow boundary ID and the full-width pack checksum.
272pub fn read_pack_response_with_format(mut bytes: &[u8], format: ObjectFormat) -> Result<&[u8]> {
273    loop {
274        match pktline::read(&mut bytes)? {
275            Some(Packet::Data(b"NAK\n")) => break,
276            Some(Packet::Data(line)) if line.starts_with(b"shallow ") => {
277                if ObjectId::from_hex(&line.strip_suffix(b"\n").unwrap_or(line)[8..])?.format()
278                    != format
279                {
280                    return Err(Error::InvalidRequest);
281                }
282            }
283            Some(Packet::Flush) => {}
284            _ => return Err(Error::InvalidPacket),
285        }
286    }
287    pack::validate_with_format(bytes, format)?;
288    Ok(bytes)
289}