Skip to main content

git_zero_server/
stream.rs

1//! Host-driven request intake and pull-based responses. HTTP transfer framing,
2//! socket readiness and staging unsent output remain the host's responsibility.
3#[cfg(feature = "read")]
4pub mod preparation;
5
6use crate::{
7    Request, Response,
8    http_backend::{self, Plan},
9    upload_pack,
10};
11use git_zero_core::{Error, Repository, Result, io::SliceWriter, pack};
12
13/// Incrementally collect a bounded request body in caller-owned storage. No
14/// protocol output is possible until `finish` returns a complete body to parse.
15/// Pass a validated Content-Length when known; otherwise the host supplies EOF.
16pub struct RequestBody<'a> {
17    storage: &'a mut [u8],
18    length: usize,
19    expected: Option<usize>,
20    cancelled: bool,
21}
22
23impl<'a> RequestBody<'a> {
24    pub fn new(storage: &'a mut [u8], expected: Option<usize>) -> Result<Self> {
25        if expected.is_some_and(|length| length > storage.len()) {
26            return Err(Error::BufferTooSmall);
27        }
28        Ok(Self {
29            storage,
30            length: 0,
31            expected,
32            cancelled: false,
33        })
34    }
35
36    /// Append all bytes or none. A rejected fragment leaves intake resumable.
37    pub fn push(&mut self, bytes: &[u8]) -> Result<()> {
38        if self.cancelled {
39            return Err(Error::InvalidRequest);
40        }
41        let end = self
42            .length
43            .checked_add(bytes.len())
44            .ok_or(Error::BufferTooSmall)?;
45        if self.expected.is_some_and(|length| end > length) {
46            return Err(Error::InvalidRequest);
47        }
48        self.storage
49            .get_mut(self.length..end)
50            .ok_or(Error::BufferTooSmall)?
51            .copy_from_slice(bytes);
52        self.length = end;
53        Ok(())
54    }
55
56    pub fn received(&self) -> usize {
57        self.length
58    }
59
60    /// Terminal cancellation. Drop this value to reuse its caller-owned storage.
61    pub fn cancel(&mut self) {
62        self.cancelled = true;
63    }
64
65    pub fn finish(self) -> Result<&'a [u8]> {
66        if self.cancelled || self.expected.is_some_and(|length| length != self.length) {
67            return Err(Error::InvalidRequest);
68        }
69        Ok(&self.storage[..self.length])
70    }
71}
72
73// Inline variants keep the cursor allocation-free with a fixed size budget.
74#[allow(clippy::large_enum_variant)]
75enum Body<'a> {
76    Buffered,
77    Pack(pack::Encoder<'a>),
78    Loose(pack::LooseEncoder),
79    #[cfg(feature = "read")]
80    Cached {
81        bytes: &'a [u8],
82        position: usize,
83    },
84}
85
86/// An independent response cursor with bounded inline framing state. The host
87/// owns the repository, request and control buffers. Keep the same stable
88/// repository for every `read`; it may be recreated from immutable inputs.
89/// Discovery/ref/ACK bytes must fit `control`; object bodies never use it.
90/// A completed prepare validates routes, commands, wants and selection budgets
91/// before output. Provider failures after headers require aborting the transport.
92pub struct ResponseEncoder<'a> {
93    response: Response,
94    prefix: &'a [u8],
95    position: usize,
96    body: Body<'a>,
97    band: u8,
98    packet: [u8; 1029],
99    packet_position: usize,
100    packet_length: usize,
101    terminated: bool,
102    cancelled: bool,
103    started: bool,
104}
105
106impl<'a> ResponseEncoder<'a> {
107    /// Prepare a HEAD-only repository. Independent roots need `with_selection`.
108    pub fn new(
109        repository: &impl Repository,
110        request: Request<'_>,
111        control: &'a mut [u8],
112    ) -> Result<Self> {
113        let mut output = SliceWriter::new(control);
114        let plan = http_backend::prepare(repository, request, &mut output, false)?;
115        let Plan::Upload(version) = plan else {
116            return Self::ordinary_response(repository, plan, output.finish());
117        };
118        let upload = upload_pack::prepare_complete(repository, request.body, version)?;
119        upload.acknowledge(repository, &mut output)?;
120        let mut encoder = Self::buffered(upload_pack::response(), output.finish());
121        if upload.done {
122            encoder.body = Body::Pack(pack::Encoder::new(repository, None)?);
123            encoder.band = upload.band();
124        }
125        Ok(encoder)
126    }
127
128    /// Prepare the exact requested closure with the same limits as
129    /// `serve_with_selection`. Selection slots stay borrowed until this cursor
130    /// is dropped; separate storage permits independent overlapping responses.
131    #[cfg(feature = "read")]
132    pub fn with_selection<R: Repository + ?Sized>(
133        repository: &git_zero_core::ValidatedRepository<'_, '_, R>,
134        request: Request<'_>,
135        storage: &'a mut crate::SelectionStorage<'_>,
136        control: &'a mut [u8],
137    ) -> Result<Self> {
138        Self::with_selection_storage(repository, request, storage.reborrow(), control)
139    }
140
141    /// Move a request's storage descriptor into preparation. Only its selected
142    /// index array and control bytes remain borrowed by the resulting cursor.
143    #[cfg(feature = "read")]
144    pub fn with_selection_storage<R: Repository + ?Sized>(
145        repository: &git_zero_core::ValidatedRepository<'_, '_, R>,
146        request: Request<'_>,
147        storage: crate::SelectionStorage<'a>,
148        control: &'a mut [u8],
149    ) -> Result<Self> {
150        Self::with_selection_storage_policy(
151            repository,
152            request,
153            storage,
154            crate::WantOptions::STRICT,
155            control,
156        )
157    }
158
159    /// Additive host-granted selection. Only selected indices/control are pinned;
160    /// the policy cannot be inherited by another cursor or strict constructor.
161    #[cfg(feature = "read")]
162    pub fn with_selection_policy<R: Repository + ?Sized>(
163        repository: &git_zero_core::ValidatedRepository<'_, '_, R>,
164        request: Request<'_>,
165        storage: &'a mut crate::SelectionStorage<'_>,
166        options: crate::WantOptions,
167        control: &'a mut [u8],
168    ) -> Result<Self> {
169        Self::with_selection_storage_policy(
170            repository,
171            request,
172            storage.reborrow(),
173            options,
174            control,
175        )
176    }
177
178    #[cfg(feature = "read")]
179    pub fn with_selection_storage_policy<R: Repository + ?Sized>(
180        repository: &git_zero_core::ValidatedRepository<'_, '_, R>,
181        request: Request<'_>,
182        storage: crate::SelectionStorage<'a>,
183        options: crate::WantOptions,
184        control: &'a mut [u8],
185    ) -> Result<Self> {
186        Self::selected_storage(repository, request, storage, options, None, control)
187    }
188
189    /// Opt in to time/exclusion deepening. Boundary state is temporary; only
190    /// selected indices and prepared control bytes survive in the response.
191    #[cfg(feature = "read")]
192    pub fn with_revisions<R: Repository + ?Sized>(
193        repository: &git_zero_core::ValidatedRepository<'_, '_, R>,
194        request: Request<'_>,
195        storage: &'a mut crate::RevisionStorage<'_>,
196        control: &'a mut [u8],
197    ) -> Result<Self> {
198        Self::with_revision_storage(repository, request, storage.reborrow(), control)
199    }
200
201    #[cfg(feature = "read")]
202    pub fn with_revision_storage<R: Repository + ?Sized>(
203        repository: &git_zero_core::ValidatedRepository<'_, '_, R>,
204        request: Request<'_>,
205        storage: crate::RevisionStorage<'a>,
206        control: &'a mut [u8],
207    ) -> Result<Self> {
208        Self::with_revision_storage_policy(
209            repository,
210            request,
211            storage,
212            crate::WantOptions::STRICT,
213            control,
214        )
215    }
216
217    /// Combine explicit revision budgets with a request-local want grant. Neither
218    /// capability can be inherited by another response or a strict constructor.
219    #[cfg(feature = "read")]
220    pub fn with_revision_storage_policy<R: Repository + ?Sized>(
221        repository: &git_zero_core::ValidatedRepository<'_, '_, R>,
222        request: Request<'_>,
223        storage: crate::RevisionStorage<'a>,
224        options: crate::WantOptions,
225        control: &'a mut [u8],
226    ) -> Result<Self> {
227        Self::selected_storage(
228            repository,
229            request,
230            storage.selection,
231            options,
232            Some((storage.state, storage.limits)),
233            control,
234        )
235    }
236
237    #[cfg(feature = "read")]
238    fn selected_storage<R: Repository + ?Sized>(
239        repository: &git_zero_core::ValidatedRepository<'_, '_, R>,
240        request: Request<'_>,
241        storage: crate::SelectionStorage<'a>,
242        options: crate::WantOptions,
243        revision: Option<(&mut [u8], git_zero_core::RevisionLimits)>,
244        control: &'a mut [u8],
245    ) -> Result<Self> {
246        let mut output = SliceWriter::new(control);
247        let plan = upload_pack::prepare_route(
248            repository,
249            request,
250            options,
251            revision.is_some(),
252            &mut output,
253        )?;
254        let Plan::Upload(version) = plan else {
255            return Self::ordinary_response(repository, plan, output.finish());
256        };
257        let selected = upload_pack::prepare_fetch(
258            repository,
259            request,
260            version,
261            storage,
262            options,
263            revision,
264            &mut output,
265        )?;
266        let mut encoder = Self::buffered(upload_pack::response(), output.finish());
267        if let Some(indices) = selected.indices {
268            encoder.body = Body::Pack(pack::Encoder::new(repository, Some(indices))?);
269            encoder.band = selected.upload.band();
270        }
271        Ok(encoder)
272    }
273
274    fn buffered(response: Response, prefix: &'a [u8]) -> Self {
275        Self {
276            response,
277            prefix,
278            position: 0,
279            body: Body::Buffered,
280            band: 0,
281            packet: [0; 1029],
282            packet_position: 0,
283            packet_length: 0,
284            terminated: false,
285            cancelled: false,
286            started: false,
287        }
288    }
289
290    fn ordinary_response(
291        repository: &impl Repository,
292        plan: Plan,
293        prefix: &'a [u8],
294    ) -> Result<Self> {
295        match plan {
296            Plan::Buffered(response) => Ok(Self::buffered(response, prefix)),
297            Plan::Loose(index) => {
298                let mut encoder = Self::buffered(
299                    Response {
300                        status: 200,
301                        content_type: "application/x-git-loose-object",
302                    },
303                    prefix,
304                );
305                encoder.body = Body::Loose(pack::LooseEncoder::new(repository, index)?);
306                Ok(encoder)
307            }
308            Plan::Upload(_) => unreachable!(),
309        }
310    }
311
312    pub fn response(&self) -> Response {
313        self.response
314    }
315
316    /// Terminal cancellation never emits a successful terminator. Close/reset
317    /// the transport; no subsequent read can access repository objects.
318    pub fn cancel(&mut self) {
319        self.cancelled = true;
320    }
321
322    /// Pull into host staging only when ready. Retain unsent bytes across short
323    /// writes. Empty output is rejected while active. Zero denotes clean EOF.
324    pub fn read(&mut self, repository: &impl Repository, output: &mut [u8]) -> Result<usize> {
325        if self.cancelled {
326            return Err(Error::InvalidRequest);
327        }
328        self.started = true;
329        if self.position < self.prefix.len() {
330            return copy(self.prefix, &mut self.position, output);
331        }
332        if self.band == 0 {
333            return self.read_body(repository, output);
334        }
335        if self.packet_position == self.packet_length && !self.terminated {
336            if output.is_empty() {
337                return Err(Error::BufferTooSmall);
338            }
339            self.next_packet(repository)?;
340        }
341
342        if self.packet_position == self.packet_length {
343            return Ok(0);
344        }
345        copy(
346            &self.packet[..self.packet_length],
347            &mut self.packet_position,
348            output,
349        )
350    }
351
352    fn next_packet(&mut self, repository: &impl Repository) -> Result<()> {
353        let capacity = if self.band == 1 { 995 } else { 1024 };
354        let length = match &mut self.body {
355            Body::Pack(pack) => pack.read(repository, &mut self.packet[5..5 + capacity])?,
356            #[cfg(feature = "read")]
357            Body::Cached { bytes, position } => {
358                if *position == bytes.len() {
359                    0
360                } else {
361                    copy(bytes, position, &mut self.packet[5..5 + capacity])?
362                }
363            }
364            _ => unreachable!(),
365        };
366        self.packet_position = 0;
367        if length == 0 {
368            self.packet[..4].copy_from_slice(b"0000");
369            self.packet_length = 4;
370            self.terminated = true;
371        } else {
372            let size = length + 5;
373            for (index, byte) in self.packet[..4].iter_mut().enumerate() {
374                *byte = b"0123456789abcdef"[(size >> ((3 - index) * 4)) & 15];
375            }
376            self.packet[4] = 1;
377            self.packet_length = size;
378        }
379        Ok(())
380    }
381
382    fn read_body(&mut self, repository: &impl Repository, output: &mut [u8]) -> Result<usize> {
383        match &mut self.body {
384            Body::Buffered => Ok(0),
385            Body::Pack(pack) => pack.read(repository, output),
386            Body::Loose(loose) => loose.read(repository, output),
387            #[cfg(feature = "read")]
388            Body::Cached { bytes, position } => {
389                if *position == bytes.len() {
390                    Ok(0)
391                } else {
392                    copy(bytes, position, output)
393                }
394            }
395        }
396    }
397}
398
399fn copy(bytes: &[u8], position: &mut usize, output: &mut [u8]) -> Result<usize> {
400    if output.is_empty() {
401        return Err(Error::BufferTooSmall);
402    }
403    let count = output.len().min(bytes.len() - *position);
404    output[..count].copy_from_slice(&bytes[*position..*position + count]);
405    *position += count;
406    Ok(count)
407}