Skip to main content

git_zero_server/upload_pack/
negotiation.rs

1//! Stateless v0 negotiation. Parse the complete bounded body before output.
2use super::extensions;
3use git_zero_core::{
4    Error, ObjectFormat, ObjectId, ObjectKind, Repository, Result,
5    io::Write,
6    pktline::{self, Packet},
7};
8
9pub(crate) fn capabilities(selection: bool) -> &'static [u8] {
10    if selection {
11        git_zero_core::repository::UPLOAD_CAPABILITIES
12    } else {
13        b" multi_ack multi_ack_detailed side-band side-band-64k no-progress"
14    }
15}
16
17#[derive(Clone, Copy, Default)]
18struct Capabilities(u16);
19
20const CAPABILITY_FLAGS: &[(&[u8], u16)] = &[
21    (b"multi_ack", 1),
22    (b"multi_ack_detailed", 2),
23    (b"side-band", 4),
24    (b"side-band-64k", 8),
25    (b"no-progress", 16),
26    (b"shallow", 64),
27    (b"filter", 128),
28    (b"deepen-relative", 256),
29    (b"ofs-delta", 512),
30    (b"include-tag", 1024),
31    (b"deepen-since", 2048),
32    (b"deepen-not", 4096),
33];
34
35fn capability_flag(capability: &[u8], format: ObjectFormat) -> Result<u16> {
36    if capability == format.capability() {
37        return Ok(32);
38    }
39    if matches!(
40        capability,
41        b"object-format=sha1" | b"object-format=sha256" | b""
42    ) {
43        return Err(Error::InvalidRequest);
44    }
45    CAPABILITY_FLAGS
46        .iter()
47        .find_map(|&(name, flag)| (name == capability).then_some(flag))
48        .ok_or(Error::Unsupported)
49}
50
51impl Capabilities {
52    fn parse(bytes: &[u8], format: ObjectFormat) -> Result<Self> {
53        let mut seen = 0_u16;
54        // libgit2 can leave a trailing separator; empty fields select nothing.
55        for capability in bytes
56            .split(|&byte| byte == b' ')
57            .filter(|part| !part.is_empty())
58        {
59            let flag = capability_flag(capability, format)?;
60            if seen & flag != 0 {
61                return Err(Error::InvalidRequest);
62            }
63            seen |= flag;
64        }
65        if seen & 12 == 12 {
66            return Err(Error::InvalidRequest);
67        }
68        Ok(Self(seen))
69    }
70
71    fn want(&mut self, value: &[u8], first: bool, format: ObjectFormat) -> Result<ObjectId> {
72        let mut hex = value;
73        if let Some(split) = value.iter().position(|&byte| byte == b' ') {
74            if !first {
75                return Err(Error::InvalidRequest);
76            }
77            *self = Self::parse(&value[split + 1..], format)?;
78            hex = &value[..split];
79        }
80        id(hex, format)
81    }
82
83    fn ack(self) -> u8 {
84        // Clients can select both advertised modes; detailed takes precedence.
85        if self.0 & 2 != 0 {
86            2
87        } else {
88            (self.0 & 1) as u8
89        }
90    }
91    fn band(self) -> u8 {
92        ((self.0 >> 2) & 3) as u8
93    }
94}
95
96pub(crate) struct UploadRequest<'a> {
97    pub done: bool,
98    capabilities: Capabilities,
99    haves: &'a [u8],
100    v2: bool,
101    #[cfg(feature = "read")]
102    shallow_body: &'a [u8],
103    pub extensions: extensions::Extensions,
104    #[cfg(feature = "read")]
105    include_tags: bool,
106}
107
108pub(crate) fn id(bytes: &[u8], format: ObjectFormat) -> Result<ObjectId> {
109    let id = ObjectId::from_hex(bytes)?;
110    if id.format() != format || id.is_zero() {
111        return Err(Error::InvalidRequest);
112    }
113    Ok(id)
114}
115
116fn request_line(line: &[u8]) -> &[u8] {
117    line.strip_suffix(b"\n").unwrap_or(line)
118}
119
120fn unexpected_command(line: &[u8]) -> Error {
121    if line.starts_with(b"want ") || line.starts_with(b"have ") || line == b"done" {
122        Error::InvalidRequest
123    } else {
124        Error::Unsupported
125    }
126}
127
128fn parse_wants(
129    bytes: &mut &[u8],
130    format: ObjectFormat,
131    mut want: impl FnMut(ObjectId) -> Result<()>,
132    extensions: &mut extensions::Extensions,
133) -> Result<Capabilities> {
134    let mut wanted = false;
135    let mut extended = false;
136    let mut capabilities = Capabilities::default();
137    while let Some(packet) = pktline::read(bytes)? {
138        let Packet::Data(line) = packet else {
139            if packet != Packet::Flush {
140                return Err(Error::InvalidRequest);
141            }
142            // Shallow commands use the server's advertisement; Git does not
143            // echo `shallow` in its want capabilities. Filter is negotiated.
144            extensions.options.deepen_relative |= capabilities.0 & 256 != 0;
145            extensions.require_filter_capability(capabilities.0 & 128 != 0)?;
146            return wanted.then_some(capabilities).ok_or(Error::InvalidRequest);
147        };
148        let line = request_line(line);
149        if let Some(value) = line.strip_prefix(b"want ") {
150            if extended {
151                return Err(Error::InvalidRequest);
152            }
153            let id = capabilities.want(value, !wanted, format)?;
154            if !extensions.revisions_enabled && capabilities.0 & 6144 != 0 {
155                return Err(Error::Unsupported);
156            }
157            want(id)?;
158            wanted = true;
159        } else if wanted {
160            extensions.argument(line, format)?;
161            extended = true;
162        } else {
163            return Err(unexpected_command(line));
164        }
165    }
166    Err(Error::InvalidRequest)
167}
168
169fn parse_haves(mut bytes: &[u8], format: ObjectFormat) -> Result<bool> {
170    let empty = bytes.is_empty();
171    while let Some(packet) = pktline::read(&mut bytes)? {
172        let Packet::Data(line) = packet else {
173            return bytes
174                .is_empty()
175                .then_some(false)
176                .ok_or(Error::InvalidRequest);
177        };
178        let line = request_line(line);
179        if line == b"done" && bytes.is_empty() {
180            return Ok(true);
181        }
182        let value = line
183            .strip_prefix(b"have ")
184            .ok_or_else(|| unexpected_command(line))?;
185        id(value, format)?;
186    }
187    empty.then_some(false).ok_or(Error::InvalidRequest)
188}
189
190impl<'a> UploadRequest<'a> {
191    pub fn parse(
192        bytes: &'a [u8],
193        format: ObjectFormat,
194        version: crate::route::Version,
195        want: impl FnMut(ObjectId) -> Result<()>,
196    ) -> Result<Self> {
197        Self::parse_mode(bytes, format, version, false, want)
198    }
199
200    fn parse_mode(
201        mut bytes: &'a [u8],
202        format: ObjectFormat,
203        version: crate::route::Version,
204        revisions_enabled: bool,
205        want: impl FnMut(ObjectId) -> Result<()>,
206    ) -> Result<Self> {
207        let mut extensions = extensions::Extensions::new(revisions_enabled);
208        #[cfg(feature = "read")]
209        let shallow_body = bytes;
210        if version == crate::route::Version::V2 {
211            let command = crate::v2::Command::parse(bytes, format)?.ok_or(Error::InvalidRequest)?;
212            if command.name != b"fetch" {
213                return Err(Error::InvalidRequest);
214            }
215            let fetch = crate::v2::Fetch::parse(command.args, format, want, &mut extensions)?;
216            extensions.validate()?;
217            return Ok(Self {
218                done: fetch.done,
219                capabilities: Capabilities(u16::from(fetch.ofs_delta) * 512),
220                haves: command.args,
221                v2: true,
222                #[cfg(feature = "read")]
223                shallow_body: command.args,
224                extensions,
225                #[cfg(feature = "read")]
226                include_tags: fetch.include_tags,
227            });
228        }
229        let capabilities = parse_wants(&mut bytes, format, want, &mut extensions)?;
230        extensions.validate()?;
231        Ok(Self {
232            done: parse_haves(bytes, format)?,
233            capabilities,
234            haves: bytes,
235            v2: false,
236            #[cfg(feature = "read")]
237            shallow_body,
238            extensions,
239            #[cfg(feature = "read")]
240            include_tags: capabilities.0 & 1024 != 0,
241        })
242    }
243
244    #[cfg(feature = "read")]
245    pub fn ofs_delta(&self) -> bool {
246        self.capabilities.0 & 512 != 0
247    }
248
249    pub fn require_complete(&self) -> Result<()> {
250        // A caller-supplied complete pack cannot promise follow-tag selection.
251        // Only the graph-backed API advertises include-tag.
252        if self.extensions.active() || self.capabilities.0 & (192 | 1024 | 6144) != 0 {
253            return Err(Error::Unsupported);
254        }
255        Ok(())
256    }
257
258    #[cfg(feature = "read")]
259    pub(crate) fn exclusion_ids<'r>(
260        &'r self,
261        repository: &'r impl Repository,
262    ) -> impl Iterator<Item = Result<ObjectId>> + 'r {
263        packet_values(self.shallow_body, b"deepen-not ")
264            .map(move |name| exclusion_ref(repository, name))
265    }
266
267    #[cfg(feature = "read")]
268    pub(crate) fn revision_info(
269        &self,
270        format: ObjectFormat,
271        changes: impl Iterator<Item = git_zero_core::ShallowChange>,
272        output: &mut impl Write,
273    ) -> Result<()> {
274        let mut updates = ShallowUpdates::new(self, format);
275        if updates.enabled {
276            for change in changes {
277                let (id, unshallow) = match change {
278                    git_zero_core::ShallowChange::Shallow(id) => (id, false),
279                    git_zero_core::ShallowChange::Unshallow(id) => (id, true),
280                };
281                updates.change(id, unshallow, output)?;
282            }
283        }
284        updates.finish(output)
285    }
286
287    #[cfg(feature = "read")]
288    pub fn shallow_info(
289        &self,
290        format: ObjectFormat,
291        objects: &[Option<git_zero_core::ValidationMetadata>],
292        output: &mut impl Write,
293    ) -> Result<()> {
294        self.revision_info(
295            format,
296            objects.iter().flatten().filter_map(shallow_change),
297            output,
298        )
299    }
300
301    #[cfg(feature = "read")]
302    pub(crate) fn parse_selected<R: Repository + ?Sized>(
303        repository: &git_zero_core::ValidatedRepository<'_, '_, R>,
304        body: &'a [u8],
305        version: crate::route::Version,
306        wants: &mut [ObjectId],
307        options: crate::WantOptions,
308        revisions_enabled: bool,
309    ) -> Result<(Self, usize)> {
310        let mut count = 0;
311        let mut remaining = options.max_want_lines;
312        let upload = Self::parse_mode(
313            body,
314            repository.object_format(),
315            version,
316            revisions_enabled,
317            |id| {
318                remaining = remaining.checked_sub(1).ok_or(Error::LimitExceeded)?;
319                let allowed = match options.policy {
320                    crate::WantPolicy::AdvertisedOnly => super::advertised(repository, id),
321                    crate::WantPolicy::AllVisibleReachable => repository.find_index(id).is_some(),
322                };
323                if !allowed {
324                    return Err(Error::InvalidRequest);
325                }
326                if !wants[..count].contains(&id) {
327                    *wants.get_mut(count).ok_or(Error::BufferTooSmall)? = id;
328                    count += 1;
329                }
330                Ok(())
331            },
332        )?;
333        Ok((upload, count))
334    }
335
336    /// Revisit validated packets without retaining an array of have IDs. Only
337    /// present commits are acknowledged or used to exclude complete history.
338    pub fn common<'r>(
339        &'r self,
340        repository: &'r impl Repository,
341    ) -> impl Iterator<Item = Result<ObjectId>> + 'r {
342        packet_values(self.haves, b"have ")
343            .filter_map(|hex| ObjectId::from_hex(hex).ok())
344            .filter_map(move |id| common_commit(repository, id).transpose())
345    }
346
347    #[cfg(feature = "read")]
348    pub fn shallow(&self) -> impl Iterator<Item = Result<ObjectId>> + '_ {
349        packet_values(self.shallow_body, b"shallow ").map(ObjectId::from_hex)
350    }
351
352    #[cfg(feature = "read")]
353    pub fn validate_shallow(&self, repository: &impl Repository) -> Result<()> {
354        for id in self.shallow() {
355            if common_commit(repository, id?)?.is_none() {
356                return Err(Error::InvalidRequest);
357            }
358        }
359        Ok(())
360    }
361
362    #[cfg(feature = "read")]
363    pub fn include_tags(&self) -> bool {
364        self.include_tags
365    }
366
367    pub fn acknowledge(&self, repository: &impl Repository, output: &mut impl Write) -> Result<()> {
368        let mut acknowledgments = Acknowledgments::new(self);
369        acknowledgments.start(output)?;
370        for id in self.common(repository) {
371            acknowledgments.common(id?, output)?;
372        }
373        acknowledgments.finish(output)
374    }
375
376    pub fn output<'w, W: Write>(&self, output: &'w mut W) -> PackOutput<'w, W> {
377        PackOutput {
378            output,
379            band: self.band(),
380            buffer: [0; 1024],
381            length: 0,
382        }
383    }
384
385    pub(crate) fn band(&self) -> u8 {
386        // V2 always frames its packfile section on sideband channel 1.
387        if self.v2 { 2 } else { self.capabilities.band() }
388    }
389}
390
391#[cfg(feature = "read")]
392fn exclusion_matches(full: &[u8], name: &[u8]) -> bool {
393    [
394        (b"".as_slice(), b"".as_slice()),
395        (b"refs/", b""),
396        (b"refs/tags/", b""),
397        (b"refs/heads/", b""),
398        (b"refs/remotes/", b""),
399        (b"refs/remotes/", b"/HEAD"),
400    ]
401    .iter()
402    .any(|&(prefix, suffix)| {
403        full.strip_prefix(prefix)
404            .and_then(|rest| rest.strip_suffix(suffix))
405            == Some(name)
406    })
407}
408
409#[cfg(feature = "read")]
410fn exclusion_ref(repository: &impl Repository, name: &[u8]) -> Result<ObjectId> {
411    let mut found = if name == b"HEAD" {
412        repository.head_state().id()
413    } else {
414        None
415    };
416    for index in 0..repository.reference_count() {
417        let full = repository
418            .reference_name(index)
419            .ok_or(Error::InvalidReference)?
420            .as_bytes();
421        if !exclusion_matches(full, name) {
422            continue;
423        }
424        if found.is_some() {
425            return Err(Error::InvalidRequest);
426        }
427        // Validation already authenticated symbolic aliases and their final ID.
428        found = Some(
429            repository
430                .reference_id(index)
431                .ok_or(Error::InvalidReference)?,
432        );
433    }
434    found.ok_or(Error::InvalidRequest)
435}
436
437#[cfg(feature = "read")]
438fn shallow_change(
439    object: &git_zero_core::ValidationMetadata,
440) -> Option<git_zero_core::ShallowChange> {
441    if object.is_shallow() {
442        Some(git_zero_core::ShallowChange::Shallow(object.id))
443    } else if object.is_unshallow() {
444        Some(git_zero_core::ShallowChange::Unshallow(object.id))
445    } else {
446        None
447    }
448}
449
450// The request parser already checked packet framing and ID syntax. These
451// helpers keep packet filtering separate from fallible repository metadata.
452fn packet_values<'a>(mut bytes: &'a [u8], prefix: &'static [u8]) -> impl Iterator<Item = &'a [u8]> {
453    core::iter::from_fn(move || pktline::read(&mut bytes).ok().flatten()).filter_map(
454        move |packet| {
455            let Packet::Data(line) = packet else {
456                return None;
457            };
458            request_line(line).strip_prefix(prefix)
459        },
460    )
461}
462
463pub(crate) fn common_commit(
464    repository: &impl Repository,
465    id: ObjectId,
466) -> Result<Option<ObjectId>> {
467    let Some(index) = repository.find_index(id) else {
468        return Ok(None);
469    };
470    Ok((repository.object_info(index)?.kind == ObjectKind::Commit).then_some(id))
471}
472
473/// Complete-pack sink passed to host pack writers. It forwards raw pack bytes
474/// or frames channel 1 according to the client's negotiated sideband mode.
475/// Uses a fixed 1 KiB buffer; packet sizes stay within both protocol limits.
476/// The server flushes the final packet and terminator after the writer succeeds.
477pub struct PackOutput<'a, W: Write> {
478    output: &'a mut W,
479    band: u8,
480    buffer: [u8; 1024],
481    length: usize,
482}
483
484impl<W: Write> PackOutput<'_, W> {
485    fn flush(&mut self) -> Result<()> {
486        if self.length != 0 {
487            pktline::write_parts(self.output, &[b"\x01", &self.buffer[..self.length]])?;
488            self.length = 0;
489        }
490        Ok(())
491    }
492
493    pub(crate) fn finish(&mut self) -> Result<()> {
494        if self.band != 0 {
495            self.flush()?;
496            self.output.write_all(b"0000")?;
497        }
498        Ok(())
499    }
500}
501
502impl<W: Write> Write for PackOutput<'_, W> {
503    fn write_all(&mut self, mut bytes: &[u8]) -> Result<()> {
504        if self.band == 0 {
505            return self.output.write_all(bytes);
506        }
507        let capacity = if self.band == 1 {
508            995
509        } else {
510            self.buffer.len()
511        };
512        while !bytes.is_empty() {
513            let count = bytes.len().min(capacity - self.length);
514            self.buffer[self.length..self.length + count].copy_from_slice(&bytes[..count]);
515            self.length += count;
516            bytes = &bytes[count..];
517            if self.length == capacity {
518                self.flush()?;
519            }
520        }
521        Ok(())
522    }
523}
524
525/// Write a terminal v0 ERR packet as a replacement for a failed buffered upload
526/// response. Discard the failed response first. No backend details are exposed.
527/// Hosts own the HTTP status; never append this to a partial raw pack.
528pub fn write_upload_error(error: Error, output: &mut impl Write) -> Result<()> {
529    let message: &[u8] = match error {
530        Error::BufferTooSmall | Error::LimitExceeded => b"request exceeds resource limits",
531        Error::Backend => b"repository unavailable",
532        Error::Unsupported => b"unsupported upload request",
533        _ => b"invalid upload request",
534    };
535    pktline::write_parts(output, &[b"ERR ", message, b"\n"])
536}
537
538/// Stream host-computed shallow changes before acknowledgments/pack framing.
539/// The host must validate commit IDs and may only unshallow a client boundary.
540#[derive(Clone, Copy, Eq, PartialEq)]
541enum Phase {
542    Initial,
543    Started,
544    Finished,
545}
546
547/// A v0/v1 depth probe ends with a flush even without changes; v2 emits a
548/// section only for changes in a final response. Errors are terminal.
549#[cfg(feature = "read")]
550pub struct ShallowUpdates {
551    format: ObjectFormat,
552    v2: bool,
553    enabled: bool,
554    phase: Phase,
555    failure: Option<Error>,
556}
557#[cfg(feature = "read")]
558impl ShallowUpdates {
559    pub(crate) fn new(request: &UploadRequest<'_>, format: ObjectFormat) -> Self {
560        Self {
561            format,
562            v2: request.v2,
563            enabled: request.extensions.history() && (!request.v2 || request.done),
564            phase: Phase::Initial,
565            failure: None,
566        }
567    }
568    /// Whether this protocol round carries depth-boundary announcements.
569    pub const fn is_enabled(&self) -> bool {
570        self.enabled
571    }
572    pub fn change(&mut self, id: ObjectId, unshallow: bool, output: &mut impl Write) -> Result<()> {
573        let result = self.write_change(id, unshallow, output);
574        self.record(result)
575    }
576    fn write_change(
577        &mut self,
578        id: ObjectId,
579        unshallow: bool,
580        output: &mut impl Write,
581    ) -> Result<()> {
582        self.ready()?;
583        if self.phase == Phase::Finished || !self.enabled {
584            return Err(Error::InvalidRequest);
585        }
586        if id.is_zero() || id.format() != self.format {
587            return Err(Error::InvalidObjectId);
588        }
589        if self.phase == Phase::Initial && self.v2 {
590            pktline::write(output, b"shallow-info\n")?;
591        }
592        self.phase = Phase::Started;
593        let marker: &[u8] = if unshallow {
594            b"unshallow "
595        } else {
596            b"shallow "
597        };
598        pktline::write_parts(output, &[marker, id.to_hex().as_bytes(), b"\n"])
599    }
600    pub fn finish(&mut self, output: &mut impl Write) -> Result<()> {
601        let result = self.write_finish(output);
602        self.record(result)
603    }
604    fn write_finish(&mut self, output: &mut impl Write) -> Result<()> {
605        self.ready()?;
606        if self.phase == Phase::Finished {
607            return Err(Error::InvalidRequest);
608        }
609        let started = self.phase == Phase::Started;
610        self.phase = Phase::Finished;
611        if self.enabled && (!self.v2 || started) {
612            output.write_all(if self.v2 { b"0001" } else { b"0000" })?;
613        }
614        Ok(())
615    }
616    fn ready(&self) -> Result<()> {
617        self.failure.map_or(Ok(()), Err)
618    }
619    fn record(&mut self, result: Result<()>) -> Result<()> {
620        if let Err(error) = result {
621            self.failure.get_or_insert(error);
622        }
623        result
624    }
625}
626
627/// Feed only host-verified common commit IDs. Sinks may change between calls.
628/// The host must not acknowledge private/unreachable IDs or claim an early cut.
629pub struct Acknowledgments {
630    v2: bool,
631    done: bool,
632    mode: u8,
633    suppress: bool,
634    last: Option<ObjectId>,
635    phase: Phase,
636}
637impl Acknowledgments {
638    pub(crate) fn new(request: &UploadRequest<'_>) -> Self {
639        Self {
640            v2: request.v2,
641            done: request.done,
642            mode: request.capabilities.ack(),
643            suppress: !request.v2 && request.extensions.history() && request.haves.is_empty(),
644            last: None,
645            phase: Phase::Initial,
646        }
647    }
648    pub fn start(&mut self, output: &mut impl Write) -> Result<()> {
649        if self.phase != Phase::Initial {
650            return Err(Error::InvalidRequest);
651        }
652        self.phase = Phase::Started;
653        if self.v2 {
654            pktline::write(
655                output,
656                if self.done {
657                    b"packfile\n"
658                } else {
659                    b"acknowledgments\n"
660                },
661            )?;
662        }
663        Ok(())
664    }
665    pub fn common(&mut self, id: ObjectId, output: &mut impl Write) -> Result<()> {
666        if self.phase != Phase::Started || id.is_zero() {
667            return Err(Error::InvalidRequest);
668        }
669        if self.suppress
670            || (self.v2 && self.done)
671            || (!self.v2 && self.mode == 0 && self.last.is_some())
672        {
673            return Ok(());
674        }
675        let suffix: &[u8] = if self.v2 || self.mode == 0 {
676            b"\n"
677        } else if self.mode == 1 {
678            b" continue\n"
679        } else {
680            b" common\n"
681        };
682        pktline::write_parts(output, &[b"ACK ", id.to_hex().as_bytes(), suffix])?;
683        self.last = Some(id);
684        Ok(())
685    }
686    pub fn finish(&mut self, output: &mut impl Write) -> Result<()> {
687        if self.phase != Phase::Started {
688            return Err(Error::InvalidRequest);
689        }
690        self.phase = Phase::Finished;
691        if self.v2 {
692            if !self.done {
693                if self.last.is_none() {
694                    pktline::write(output, b"NAK\n")?;
695                }
696                output.write_all(b"0000")?;
697            }
698        } else if !self.suppress && !(self.mode == 0 && self.last.is_some()) {
699            if let Some(id) = self.last.filter(|_| self.done) {
700                pktline::write_parts(output, &[b"ACK ", id.to_hex().as_bytes(), b"\n"])?;
701            } else {
702                pktline::write(output, b"NAK\n")?;
703            }
704        }
705        Ok(())
706    }
707}