Skip to main content

git_zero_server/upload_pack/
mod.rs

1//! Upload-pack: authorize wants, negotiate history and emit a selected pack.
2//! HTTP dispatch lives in `http_backend`; pack encoding lives in git-zero-core.
3#[cfg(feature = "read")]
4use crate::http_backend;
5use crate::{Request, Response, route::Version};
6use git_zero_core::{Error, Repository, Result, io::Write, repository::uses_only_head};
7#[cfg(feature = "read")]
8use git_zero_core::{ObjectId, SelectionOptions, ValidatedRepository, pack};
9
10pub(crate) mod advertisement;
11pub(crate) mod extensions;
12pub(crate) mod negotiation;
13pub(crate) mod policy;
14#[cfg(feature = "read")]
15mod storage;
16
17#[cfg(feature = "read")]
18pub use negotiation::{Acknowledgments, ShallowUpdates};
19pub use negotiation::{PackOutput, write_upload_error};
20pub(crate) use negotiation::{UploadRequest, capabilities, id};
21pub use policy::{WantOptions, WantPolicy};
22#[cfg(feature = "read")]
23pub use storage::{RevisionBuffers, RevisionStorage, SelectionBuffers, SelectionStorage};
24
25/// Serve independent refs, tags and multiple wanted IDs with caller-owned
26/// selection storage. Accept only advertised ref/peeled/HEAD IDs, rejecting
27/// arbitrary or merely reachable wants. Select the exact union before writing
28/// ACKs or pack. Present commit haves exclude their complete reachable history;
29/// unknown and non-commit haves are ignored. Requests are stateless: clients
30/// replay wants and acknowledged haves each round. No-done rounds only negotiate.
31/// Absolute/relative-depth shallow requests and blob filters use the same graph budget.
32/// Shallow haves never imply complete history. Errors
33/// invalidate all output; selection failures produce no output at all.
34///
35/// Requests must contain a complete, bounded body and the upload-pack media type.
36/// Absolute `deepen` requests may have an initial v0 shallow-list probe without
37/// `done`; that probe consumes the same graph budget as the eventual pack.
38/// Relative probes from full clients only flush an empty boundary list and need
39/// no graph work slots, read/parse scratch or structural-read budget.
40/// A client must replay its shallow IDs and negotiation state on each request.
41/// ACK-only rounds validate client boundaries through the repository index and
42/// need no graph work slots, read/parse scratch or structural-read budget.
43/// Boundary output must fit the host's response/control budget. The streaming
44/// encoder prepares these packets before exposing HTTP success metadata.
45///
46/// Blob filters preserve explicitly wanted blobs but do not grant permission
47/// to request unadvertised promised objects. Partial clones currently need
48/// `--no-checkout`. Time/exclusion deepening and other filter families
49/// return `Unsupported`; malformed options and invalid shallow IDs return
50/// `InvalidRequest`. Scratch/slot exhaustion returns `BufferTooSmall`, and graph
51/// read exhaustion returns `LimitExceeded`. A buffered host can replace a failed
52/// response with `write_upload_error`; a failed stream must be cancelled/closed.
53#[cfg(feature = "read")]
54pub fn serve_with_selection<R: Repository + ?Sized>(
55    repository: &ValidatedRepository<'_, '_, R>,
56    request: Request<'_>,
57    storage: &mut SelectionStorage<'_>,
58    output: &mut impl Write,
59) -> Result<Response> {
60    serve_with_selection_policy(repository, request, storage, WantOptions::STRICT, output)
61}
62
63/// Serve selected wants under an explicit host grant and occurrence cap.
64/// The validated repository must be the host-authorized immutable projection.
65/// All wants are checked before ACKs, pack output or host cache integration.
66#[cfg(feature = "read")]
67pub fn serve_with_selection_policy<R: Repository + ?Sized>(
68    repository: &ValidatedRepository<'_, '_, R>,
69    request: Request<'_>,
70    storage: &mut SelectionStorage<'_>,
71    options: WantOptions,
72    output: &mut impl Write,
73) -> Result<Response> {
74    serve_with_selected_pack_policy(
75        repository,
76        request,
77        storage,
78        options,
79        output,
80        |indices, read, packed| {
81            pack::write_selected_with_scratch(repository, indices, read, packed).map(|_| ())
82        },
83    )
84}
85
86/// Serve selected requests using a trusted host's raw-pack writer. Routing,
87/// authorization and bounded selection are identical to `serve_with_selection`.
88/// The writer runs once only when a pack is needed, including an empty selected
89/// set, and must emit exactly `indices` from this same stable repository.
90/// ACK-only rounds and shallow probes never invoke it. Its sink applies the
91/// current request's sideband framing; cache only the raw PACK bytes.
92///
93/// Use `cache::selected::write` with `Selection::Indices(indices)` to integrate
94/// verified caller-owned caches. Authorization and selection precede every hit.
95/// Control packets may already be written when the writer fails: discard the
96/// entire output on any error. Buffer the response before sending HTTP success,
97/// or close the transport on a late failure. The writer retains no references
98/// after this synchronous call, and a failure emits no successful terminator.
99#[cfg(feature = "read")]
100pub fn serve_with_selected_pack<R: Repository + ?Sized, W: Write>(
101    repository: &ValidatedRepository<'_, '_, R>,
102    request: Request<'_>,
103    storage: &mut SelectionStorage<'_>,
104    output: &mut W,
105    write_selected: impl FnOnce(&[usize], &mut [u8], &mut PackOutput<'_, W>) -> Result<()>,
106) -> Result<Response> {
107    serve_with_selected_pack_policy(
108        repository,
109        request,
110        storage,
111        WantOptions::STRICT,
112        output,
113        write_selected,
114    )
115}
116
117/// Apply an explicit host want policy before the selected-pack writer or cache.
118/// Every request, including hits and ACK-only rounds, is checked independently.
119/// Keep the authorized repository projection immutable for the entire call.
120/// Output and writer-failure contracts match `serve_with_selected_pack`.
121#[cfg(feature = "read")]
122pub fn serve_with_selected_pack_policy<R: Repository + ?Sized, W: Write>(
123    repository: &ValidatedRepository<'_, '_, R>,
124    request: Request<'_>,
125    storage: &mut SelectionStorage<'_>,
126    options: WantOptions,
127    output: &mut W,
128    write_selected: impl FnOnce(&[usize], &mut [u8], &mut PackOutput<'_, W>) -> Result<()>,
129) -> Result<Response> {
130    serve_selected_request(
131        repository,
132        request,
133        storage.reborrow(),
134        options,
135        None,
136        output,
137        write_selected,
138    )
139}
140
141/// Serve time/exclusion deepening with explicit caller storage and exclusion limits.
142/// This also supports all existing selected fetch modes. Only this opt-in path
143/// advertises v0/v1 deepen-since/deepen-not. Successful output is invalidated by
144/// any later error; a streaming host must abort on provider/transport failure.
145#[cfg(feature = "read")]
146pub fn serve_with_revisions<R: Repository + ?Sized>(
147    repository: &ValidatedRepository<'_, '_, R>,
148    request: Request<'_>,
149    storage: &mut RevisionStorage<'_>,
150    output: &mut impl Write,
151) -> Result<Response> {
152    let storage = storage.reborrow();
153    serve_selected_request(
154        repository,
155        request,
156        storage.selection,
157        WantOptions::STRICT,
158        Some((storage.state, storage.limits)),
159        output,
160        |indices, read, packed| {
161            pack::write_selected_with_scratch(repository, indices, read, packed).map(|_| ())
162        },
163    )
164}
165
166#[cfg(feature = "read")]
167fn serve_selected_request<R: Repository + ?Sized, W: Write>(
168    repository: &ValidatedRepository<'_, '_, R>,
169    request: Request<'_>,
170    storage: SelectionStorage<'_>,
171    options: WantOptions,
172    revision: Option<(&mut [u8], git_zero_core::RevisionLimits)>,
173    output: &mut W,
174    write_selected: impl FnOnce(&[usize], &mut [u8], &mut PackOutput<'_, W>) -> Result<()>,
175) -> Result<Response> {
176    let version = match prepare_route(repository, request, options, revision.is_some(), output)? {
177        http_backend::Plan::Buffered(response) => return Ok(response),
178        http_backend::Plan::Loose(index) => {
179            return http_backend::write_loose(repository, index, storage.read, output);
180        }
181        http_backend::Plan::Upload(version) => version,
182    };
183    let selected = prepare_fetch(
184        repository, request, version, storage, options, revision, output,
185    )?;
186    if let Some(indices) = selected.indices {
187        let mut packed = selected.upload.output(output);
188        write_selected(indices, selected.read, &mut packed)?;
189        packed.finish()?;
190    }
191    Ok(response())
192}
193
194#[cfg(feature = "read")]
195pub(crate) fn prepare_route(
196    repository: &impl Repository,
197    request: Request<'_>,
198    options: WantOptions,
199    revisions: bool,
200    output: &mut impl Write,
201) -> Result<http_backend::Plan> {
202    http_backend::prepare_with_refs(repository, request, output, |route, output| {
203        if revisions {
204            advertisement::serve_revision_info_refs(repository, route, output)
205        } else {
206            policy::info_refs(repository, route, output, options)
207        }
208    })
209}
210
211#[cfg(feature = "read")]
212pub(crate) struct SelectedFetch<'storage, 'body> {
213    pub(crate) upload: UploadRequest<'body>,
214    pub(crate) indices: Option<&'storage [usize]>,
215    read: &'storage mut [u8],
216}
217
218#[cfg(feature = "read")]
219fn select_fetch<'indices, R: Repository + ?Sized>(
220    repository: &ValidatedRepository<'_, '_, R>,
221    upload: &UploadRequest<'_>,
222    wants: &[ObjectId],
223    work: &mut [Option<git_zero_core::ValidationMetadata>],
224    indices: &'indices mut [usize],
225    scratch: (
226        &mut [u8],
227        &mut [u8],
228        Option<&mut git_zero_core::RevisionState<'_>>,
229    ),
230    options: SelectionOptions,
231) -> Result<&'indices [usize]> {
232    if upload.extensions.revision() {
233        repository.select_revision_fetch(
234            wants,
235            (
236                upload.common(repository),
237                upload.shallow(),
238                upload.exclusion_ids(repository),
239            ),
240            work,
241            indices,
242            (scratch.0, scratch.1, scratch.2.ok_or(Error::Unsupported)?),
243            (
244                options,
245                upload.extensions.options,
246                git_zero_core::RevisionOptions {
247                    since: upload.extensions.since,
248                },
249            ),
250        )
251    } else {
252        repository.select_bounded_fetch(
253            wants,
254            (upload.common(repository), upload.shallow()),
255            work,
256            indices,
257            (scratch.0, scratch.1),
258            (options, upload.extensions.options),
259        )
260    }
261}
262
263#[cfg(feature = "read")]
264pub(crate) fn prepare_fetch<'storage, 'body, R: Repository + ?Sized>(
265    repository: &ValidatedRepository<'_, '_, R>,
266    request: Request<'body>,
267    version: Version,
268    storage: SelectionStorage<'storage>,
269    options: WantOptions,
270    revision: Option<(&mut [u8], git_zero_core::RevisionLimits)>,
271    output: &mut impl Write,
272) -> Result<SelectedFetch<'storage, 'body>> {
273    let (upload, count) = UploadRequest::parse_selected(
274        repository,
275        request.body,
276        version,
277        storage.wants,
278        options,
279        revision.is_some(),
280    )?;
281    let mut revision =
282        revision.map(|(state, limits)| git_zero_core::RevisionState::new(state, limits));
283    if upload.extensions.exclusions > revision.as_ref().map_or(0, |state| state.max_exclusions()) {
284        return Err(Error::LimitExceeded);
285    }
286    let boundary_probe = version != Version::V2 && !upload.done && upload.extensions.history();
287    let select_boundaries = boundary_probe
288        && (upload.extensions.revision()
289            || !upload.extensions.options.deepen_relative
290            || upload.shallow().next().is_some());
291    let indices = if upload.done || select_boundaries {
292        let options = SelectionOptions {
293            graph_read_limit: storage.graph_read_limit,
294            include_tags: upload.include_tags(),
295        };
296        Some(select_fetch(
297            repository,
298            &upload,
299            &storage.wants[..count],
300            storage.objects,
301            storage.indices,
302            (storage.read, storage.parse, revision.as_mut()),
303            options,
304        )?)
305    } else {
306        upload.validate_shallow(repository)?;
307        if upload.extensions.revision() {
308            for id in upload.exclusion_ids(repository) {
309                id?;
310            }
311        }
312        None
313    };
314    if indices.is_some() {
315        if upload.extensions.revision() {
316            upload.revision_info(
317                repository.object_format(),
318                revision
319                    .as_ref()
320                    .ok_or(Error::Unsupported)?
321                    .changes(storage.objects),
322                output,
323            )?;
324        } else {
325            upload.shallow_info(
326                repository.object_format(),
327                &storage.objects[..repository.object_count()],
328                output,
329            )?;
330        }
331    } else if boundary_probe {
332        output.write_all(b"0000")?;
333    }
334    upload.acknowledge(repository, output)?;
335    Ok(SelectedFetch {
336        indices: indices.filter(|_| upload.done),
337        upload,
338        read: storage.read,
339    })
340}
341
342pub(crate) fn response() -> Response {
343    Response {
344        status: 200,
345        content_type: "application/x-git-upload-pack-result",
346    }
347}
348
349#[cfg(feature = "read")]
350pub(super) fn advertised(repository: &impl Repository, id: ObjectId) -> bool {
351    repository.head_state().id() == Some(id)
352        || (0..repository.reference_count()).any(|index| {
353            repository.reference_id(index) == Some(id)
354                || repository.reference_peeled(index) == Some(id)
355        })
356}
357
358/// Validate the HEAD-only complete-pack contract before either output path.
359pub(crate) fn prepare_complete<'a>(
360    repository: &impl Repository,
361    body: &'a [u8],
362    version: Version,
363) -> Result<UploadRequest<'a>> {
364    if !uses_only_head(repository) {
365        return Err(Error::Unsupported);
366    }
367    let head = repository.head_state().id().ok_or(Error::InvalidRequest)?;
368    let upload = UploadRequest::parse(body, repository.object_format(), version, |id| {
369        (id == head).then_some(()).ok_or(Error::InvalidRequest)
370    })?;
371    upload.require_complete()?;
372    Ok(upload)
373}
374
375pub(crate) fn write_upload<W: Write>(
376    repository: &impl Repository,
377    request: Request<'_>,
378    version: Version,
379    scratch: &mut [u8],
380    output: &mut W,
381    write_pack: impl FnOnce(&mut [u8], &mut PackOutput<'_, W>) -> Result<()>,
382) -> Result<()> {
383    let upload = prepare_complete(repository, request.body, version)?;
384    upload.acknowledge(repository, output)?;
385    if upload.done {
386        let mut packed = upload.output(output);
387        write_pack(scratch, &mut packed)?;
388        packed.finish()?;
389    }
390    Ok(())
391}