Skip to main content

git_zero_server/
stored.rs

1//! Smart HTTP protocol over an external immutable object/ref catalog. Parsing
2//! never grants access: the host authorizes wants, checks common commit IDs,
3//! selects a closure and supplies authenticated pack representations separately.
4pub use crate::upload_pack::{Acknowledgments, ShallowUpdates};
5use crate::{route::Version, upload_pack::UploadRequest, v2};
6use git_zero_core::{
7    Error, FetchOptions, ObjectFormat, ObjectId, Result,
8    io::Write,
9    pktline::{self, Packet},
10    repository::validate_ref_name_bytes,
11};
12
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14pub enum Command {
15    Noop,
16    Refs,
17    Fetch,
18}
19#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20pub enum ObjectRequest {
21    Want(ObjectId),
22    Have(ObjectId),
23    Shallow(ObjectId),
24}
25
26#[expect(
27    clippy::large_enum_variant,
28    reason = "Allocation-free requests keep the bounded ref-prefix index inline"
29)]
30enum Parsed<'a> {
31    Noop,
32    Refs(v2::Refs<'a>),
33    Fetch(UploadRequest<'a>),
34}
35
36/// A complete validated control body; storage is borrowed, not copied. Revisit
37/// its objects without an object-count array. Bound bytes before parsing it.
38pub struct Request<'a> {
39    parsed: Parsed<'a>,
40    objects: &'a [u8],
41    format: ObjectFormat,
42}
43impl<'a> Request<'a> {
44    pub fn parse(body: &'a [u8], format: ObjectFormat, version: u8) -> Result<Self> {
45        let version = version_value(version)?;
46        let (parsed, objects) = if version == Version::V2 {
47            match v2::Command::parse(body, format)? {
48                None => (Parsed::Noop, &body[..0]),
49                Some(command) if command.name == b"ls-refs" => {
50                    (Parsed::Refs(v2::Refs::parse(command.args)?), &body[..0])
51                }
52                Some(command) => (
53                    Parsed::Fetch(UploadRequest::parse(body, format, version, |_| Ok(()))?),
54                    command.args,
55                ),
56            }
57        } else {
58            (
59                Parsed::Fetch(UploadRequest::parse(body, format, version, |_| Ok(()))?),
60                body,
61            )
62        };
63        Ok(Self {
64            parsed,
65            objects,
66            format,
67        })
68    }
69    pub fn command(&self) -> Command {
70        match self.parsed {
71            Parsed::Noop => Command::Noop,
72            Parsed::Refs(_) => Command::Refs,
73            Parsed::Fetch(_) => Command::Fetch,
74        }
75    }
76    pub fn done(&self) -> bool {
77        matches!(&self.parsed, Parsed::Fetch(request) if request.done)
78    }
79    pub fn options(&self) -> Option<FetchOptions> {
80        match &self.parsed {
81            Parsed::Fetch(request) => Some(request.extensions.options),
82            _ => None,
83        }
84    }
85    pub fn require_complete(&self) -> Result<()> {
86        match &self.parsed {
87            Parsed::Fetch(request) => request.require_complete(),
88            _ => Ok(()),
89        }
90    }
91    /// Whether the client permits offset delta entries in the response pack.
92    pub fn ofs_delta(&self) -> bool {
93        matches!(&self.parsed, Parsed::Fetch(request) if request.ofs_delta())
94    }
95    pub fn include_tags(&self) -> bool {
96        matches!(&self.parsed, Parsed::Fetch(request) if request.include_tags())
97    }
98    pub fn band(&self) -> u8 {
99        match &self.parsed {
100            Parsed::Fetch(request) => request.band(),
101            _ => 0,
102        }
103    }
104    pub fn objects(&self) -> Objects<'a> {
105        Objects {
106            bytes: self.objects,
107        }
108    }
109    pub fn acknowledgments(&self) -> Result<Acknowledgments> {
110        let Parsed::Fetch(request) = &self.parsed else {
111            return Err(Error::InvalidRequest);
112        };
113        Ok(Acknowledgments::new(request))
114    }
115    pub fn shallow_updates(&self) -> Result<ShallowUpdates> {
116        let Parsed::Fetch(request) = &self.parsed else {
117            return Err(Error::InvalidRequest);
118        };
119        Ok(ShallowUpdates::new(request, self.format))
120    }
121    pub fn reference(
122        &self,
123        id: Option<ObjectId>,
124        name: &[u8],
125        symbolic: Option<&[u8]>,
126        peeled: Option<ObjectId>,
127        output: &mut impl Write,
128    ) -> Result<()> {
129        let Parsed::Refs(request) = &self.parsed else {
130            return Err(Error::InvalidRequest);
131        };
132        if name != b"HEAD" {
133            validate_ref_name_bytes(name)?;
134        }
135        if let Some(target) = symbolic {
136            validate_ref_name_bytes(target)?;
137        }
138        if id
139            .into_iter()
140            .chain(peeled)
141            .any(|id| id.format() != self.format || id.is_zero())
142        {
143            return Err(Error::InvalidObjectId);
144        }
145        request.write_ref(id, name, symbolic, peeled, output)
146    }
147}
148
149pub struct Objects<'a> {
150    bytes: &'a [u8],
151}
152impl Iterator for Objects<'_> {
153    type Item = ObjectRequest;
154    fn next(&mut self) -> Option<Self::Item> {
155        while let Some(packet) = pktline::read(&mut self.bytes).ok().flatten() {
156            let Packet::Data(line) = packet else {
157                continue;
158            };
159            let line = line.strip_suffix(b"\n").unwrap_or(line);
160            let (marker, value) = crate::upload_pack::extensions::argument_parts(line);
161            let Some(value) = value else {
162                continue;
163            };
164            let value = value.split(|&byte| byte == b' ').next()?;
165            let constructor: fn(ObjectId) -> ObjectRequest = match marker {
166                b"want" => ObjectRequest::Want,
167                b"have" => ObjectRequest::Have,
168                b"shallow" => ObjectRequest::Shallow,
169                _ => continue,
170            };
171            // The complete request parser has already checked every ID.
172            return ObjectId::from_hex(value).ok().map(constructor);
173        }
174        None
175    }
176}
177
178fn version_value(version: u8) -> Result<Version> {
179    match version {
180        0 => Ok(Version::V0),
181        1 => Ok(Version::V1),
182        2 => Ok(Version::V2),
183        _ => Err(Error::Unsupported),
184    }
185}
186
187pub fn protocol_version(header: Option<&str>) -> Result<u8> {
188    Ok(match crate::route::version(header)? {
189        Version::V0 => 0,
190        Version::V1 => 1,
191        Version::V2 => 2,
192    })
193}
194
195pub fn advertise(
196    format: ObjectFormat,
197    version: u8,
198    selection: bool,
199    output: &mut impl Write,
200) -> Result<()> {
201    let version = version_value(version)?;
202    pktline::write(output, b"# service=git-upload-pack\n")?;
203    output.write_all(b"0000")?;
204    if version == Version::V2 {
205        v2::advertise(format, output, selection)?;
206    } else if version == Version::V1 {
207        pktline::write(output, b"version 1\n")?;
208    }
209    Ok(())
210}
211
212/// Optional wire features supported and authorized by an external catalog host.
213#[derive(Clone, Copy, Debug, Default)]
214pub struct Features {
215    pub selection: bool,
216    /// The host grants retrieval of every visible reachable object, including
217    /// blobs omitted by a partial clone. Authorization remains host-owned.
218    pub reachable_wants: bool,
219}
220
221/// v0/v1 ref projection. The host supplies a stable sorted projection, HEAD
222/// first when resolved; `first` carries capabilities once. None is the empty
223/// repository carrier. Symbolic HEAD targets are raw validated ref bytes.
224pub fn reference(
225    format: ObjectFormat,
226    id: Option<ObjectId>,
227    name: &[u8],
228    first: bool,
229    head: Option<&[u8]>,
230    features: Features,
231    output: &mut impl Write,
232) -> Result<()> {
233    if name != b"HEAD" && id.is_some() {
234        validate_ref_name_bytes(name)?;
235    }
236    if id.is_some_and(|id| id.format() != format || id.is_zero()) {
237        return Err(Error::InvalidObjectId);
238    }
239    if let Some(target) = head {
240        validate_ref_name_bytes(target)?;
241    }
242    let id = id.unwrap_or_else(|| format.zero_id()).to_hex();
243    let name = if name.is_empty() {
244        b"capabilities^{}".as_slice()
245    } else {
246        name
247    };
248    if first {
249        let target: &[u8] = head.unwrap_or_default();
250        let symref: &[u8] = if head.is_some() {
251            b" symref=HEAD:"
252        } else {
253            b""
254        };
255        pktline::write_parts(
256            output,
257            &[
258                id.as_bytes(),
259                b" ",
260                name,
261                b"\0",
262                format.capability(),
263                crate::upload_pack::capabilities(features.selection),
264                if features.reachable_wants {
265                    crate::upload_pack::policy::REACHABLE
266                } else {
267                    b""
268                },
269                symref,
270                target,
271                b"\n",
272            ],
273        )
274    } else {
275        pktline::write_parts(output, &[id.as_bytes(), b" ", name, b"\n"])
276    }
277}
278
279/// Frame authenticated raw pack bytes, preserving both negotiated sideband
280/// limits. The host emits a flush after the pack checksum when band != 0.
281pub fn pack_chunk(band: u8, bytes: &[u8], output: &mut impl Write) -> Result<()> {
282    let capacity = match band {
283        0 => return output.write_all(bytes),
284        1 => 995,
285        2 => pktline::MAX_LENGTH - 5,
286        _ => return Err(Error::InvalidRequest),
287    };
288    for bytes in bytes.chunks(capacity) {
289        pktline::write_parts(output, &[b"\x01", bytes])?;
290    }
291    Ok(())
292}
293
294pub fn peeled_reference(
295    format: ObjectFormat,
296    id: ObjectId,
297    name: &[u8],
298    output: &mut impl Write,
299) -> Result<()> {
300    validate_ref_name_bytes(name)?;
301    if id.format() != format || id.is_zero() {
302        return Err(Error::InvalidObjectId);
303    }
304    pktline::write_parts(output, &[id.to_hex().as_bytes(), b" ", name, b"^{}\n"])
305}