Skip to main content

git_zero_server/receive/
advertisement.rs

1use super::Capabilities;
2use git_zero_core::{
3    Error, ObjectFormat, ObjectId, Result,
4    io::{SliceWriter, Write},
5    pktline,
6    repository::validate_ref_name_bytes,
7};
8
9/// Incremental advertisement for hosts whose stable ref projection lives in
10/// external storage. `previous` retains only the last name, never the ref set.
11pub struct Advertisement<'a> {
12    format: ObjectFormat,
13    capabilities: Capabilities,
14    previous: &'a mut [u8],
15    length: usize,
16    started: bool,
17    finished: bool,
18}
19
20impl<'a> Advertisement<'a> {
21    pub fn new(format: ObjectFormat, capabilities: Capabilities, previous: &'a mut [u8]) -> Self {
22        Self {
23            format,
24            capabilities,
25            previous,
26            length: 0,
27            started: false,
28            finished: false,
29        }
30    }
31
32    pub fn start(&mut self, output: &mut impl Write) -> Result<()> {
33        if self.started || self.finished {
34            return Err(Error::InvalidRequest);
35        }
36        self.started = true;
37        pktline::write(output, b"# service=git-receive-pack\n")?;
38        output.write_all(b"0000")
39    }
40
41    pub fn reference(&mut self, id: ObjectId, name: &[u8], output: &mut impl Write) -> Result<()> {
42        if !self.started || self.finished {
43            return Err(Error::InvalidRequest);
44        }
45        if name.len() > self.previous.len() {
46            return Err(Error::BufferTooSmall);
47        }
48        let previous = (self.length != 0).then_some(&self.previous[..self.length]);
49        reference(self.format, self.capabilities, id, name, previous, output)?;
50        self.previous[..name.len()].copy_from_slice(name);
51        self.length = name.len();
52        Ok(())
53    }
54
55    pub fn finish(&mut self, output: &mut impl Write) -> Result<()> {
56        if !self.started || self.finished {
57            return Err(Error::InvalidRequest);
58        }
59        self.finished = true;
60        finish(self.format, self.capabilities, self.length == 0, output)
61    }
62}
63
64/// Write a smart HTTP receive-pack advertisement from a stable, byte-sorted
65/// ref projection. Names are raw Git bytes. The host must discard partial
66/// output on an invalid ref or sink failure. Only opt into host-backed features.
67pub fn write_advertisement<'a>(
68    format: ObjectFormat,
69    capabilities: Capabilities,
70    refs: impl IntoIterator<Item = (ObjectId, &'a [u8])>,
71    output: &mut impl Write,
72) -> Result<()> {
73    pktline::write(output, b"# service=git-receive-pack\n")?;
74    output.write_all(b"0000")?;
75    let mut previous: Option<&[u8]> = None;
76    for (id, name) in refs {
77        reference(format, capabilities, id, name, previous, output)?;
78        previous = Some(name);
79    }
80    finish(format, capabilities, previous.is_none(), output)
81}
82
83fn reference(
84    format: ObjectFormat,
85    capabilities: Capabilities,
86    id: ObjectId,
87    name: &[u8],
88    previous: Option<&[u8]>,
89    output: &mut impl Write,
90) -> Result<()> {
91    validate_ref_name_bytes(name)?;
92    if id.format() != format || id.is_zero() || previous.is_some_and(|previous| previous >= name) {
93        return Err(Error::InvalidReference);
94    }
95    if previous.is_none() {
96        first(format, capabilities, id, name, output)
97    } else {
98        pktline::write_parts(output, &[id.to_hex().as_bytes(), b" ", name, b"\n"])
99    }
100}
101
102fn first(
103    format: ObjectFormat,
104    capabilities: Capabilities,
105    id: ObjectId,
106    name: &[u8],
107    output: &mut impl Write,
108) -> Result<()> {
109    let mut storage = [0; 256];
110    let mut caps = SliceWriter::new(&mut storage);
111    capabilities.write(format, &mut caps)?;
112    pktline::write_parts(
113        output,
114        &[
115            id.to_hex().as_bytes(),
116            b" ",
117            name,
118            b"\0",
119            caps.finish(),
120            b"\n",
121        ],
122    )
123}
124
125pub(super) fn validate_first_name(
126    format: ObjectFormat,
127    capabilities: Capabilities,
128    name: &[u8],
129) -> Result<()> {
130    // Reuse the actual encoder's packet budget. A created ref may sort first
131    // later, even when its incoming command did not carry any capabilities.
132    struct Discard;
133    impl Write for Discard {
134        fn write_all(&mut self, _: &[u8]) -> Result<()> {
135            Ok(())
136        }
137    }
138    first(format, capabilities, format.zero_id(), name, &mut Discard).map_err(|error| {
139        if error == Error::InvalidPacket {
140            Error::InvalidReference
141        } else {
142            error
143        }
144    })
145}
146
147fn finish(
148    format: ObjectFormat,
149    capabilities: Capabilities,
150    empty: bool,
151    output: &mut impl Write,
152) -> Result<()> {
153    if empty {
154        first(
155            format,
156            capabilities,
157            format.zero_id(),
158            b"capabilities^{}",
159            output,
160        )?;
161    }
162    output.write_all(b"0000")
163}