Skip to main content

git_zero_server/
http_backend.rs

1//! Smart/dumb HTTP dispatch, raw refs and loose objects, analogous to Git's http-backend.
2//! The host owns HTTP transfer framing, authentication and transport I/O.
3use crate::{
4    Request, Response, route,
5    upload_pack::{self, PackOutput, advertisement},
6    v2,
7};
8use git_zero_core::{
9    Error, ObjectId, Repository, Result, io::Write, pack, reader::DEFAULT_READ_BUFFER_SIZE,
10};
11
12/// Default HEAD body. The server writes `Repository::head_state` dynamically.
13pub const HEAD: &[u8] = b"ref: refs/heads/main\n";
14
15/// Always send `Cache-Control: no-store` for discovery and upload-pack responses.
16/// A failed call invalidates all output; hosts must discard it before replying.
17/// Reserves 1 KiB of read scratch on the stack for streaming-only providers.
18pub fn serve(
19    repository: &impl Repository,
20    request: Request<'_>,
21    output: &mut impl Write,
22) -> Result<Response> {
23    serve_with_scratch(
24        repository,
25        request,
26        &mut [0; DEFAULT_READ_BUFFER_SIZE],
27        output,
28    )
29}
30
31/// Serve generated/file-backed objects with an explicit read-buffer budget.
32/// Discovery/ref responses and borrowed bodies need no scratch; nonempty
33/// streamed bodies need at least one byte. Errors invalidate all output.
34pub fn serve_with_scratch(
35    repository: &impl Repository,
36    request: Request<'_>,
37    scratch: &mut [u8],
38    output: &mut impl Write,
39) -> Result<Response> {
40    serve_with_pack(repository, request, scratch, output, |scratch, output| {
41        pack::write_with_scratch(repository, scratch, output).map(|_| ())
42    })
43}
44
45/// Serve with a host-provided complete-pack writer. It is called once only for
46/// a valid upload-pack request ending in `done`, after writing ACK/NAK packets.
47/// The writer must emit a complete checksummed pack containing exactly this
48/// repository's objects, and respect the repository's stability contract.
49/// All refs must alias HEAD; independent roots return `Unsupported` before
50/// output or invoking the writer. Use `serve_with_selection` for those roots.
51/// The supplied sink handles negotiated sideband framing. Complete-pack hooks
52/// may resend common history; use selection to omit it. Other routes and no-done
53/// requests use the ordinary behavior. On error discard the entire response.
54pub fn serve_with_pack<W: Write>(
55    repository: &impl Repository,
56    request: Request<'_>,
57    scratch: &mut [u8],
58    output: &mut W,
59    write_pack: impl FnOnce(&mut [u8], &mut PackOutput<'_, W>) -> Result<()>,
60) -> Result<Response> {
61    match prepare(repository, request, output, false)? {
62        Plan::Buffered(response) => Ok(response),
63        Plan::Loose(index) => write_loose(repository, index, scratch, output),
64        Plan::Upload(version) => {
65            upload_pack::write_upload(repository, request, version, scratch, output, write_pack)?;
66            Ok(upload_pack::response())
67        }
68    }
69}
70
71pub(crate) fn write_loose(
72    repository: &impl Repository,
73    index: usize,
74    scratch: &mut [u8],
75    output: &mut impl Write,
76) -> Result<Response> {
77    pack::write_loose_with_scratch(repository, index, scratch, output)?;
78    Ok(Response {
79        status: 200,
80        content_type: "application/x-git-loose-object",
81    })
82}
83
84/// A validated HTTP route, shared by buffered output and pull-based encoders.
85pub(crate) enum Plan {
86    Buffered(Response),
87    Loose(usize),
88    Upload(route::Version),
89}
90
91pub(crate) fn prepare(
92    repository: &impl Repository,
93    request: Request<'_>,
94    output: &mut impl Write,
95    selection: bool,
96) -> Result<Plan> {
97    prepare_with_refs(repository, request, output, |route, output| {
98        advertisement::serve_info_refs(repository, route, output, selection)
99    })
100}
101
102/// The caller chooses discovery capabilities; all HTTP routing stays shared.
103pub(crate) fn prepare_with_refs<W: Write>(
104    repository: &impl Repository,
105    request: Request<'_>,
106    output: &mut W,
107    advertise: impl FnOnce(&route::Route<'_>, &mut W) -> Result<Response>,
108) -> Result<Plan> {
109    let route = route::validate(request)?;
110    if let Some(response) = route.rejection {
111        return Ok(Plan::Buffered(response));
112    }
113    let status = match (request.method, route.path) {
114        ("GET", "HEAD") => {
115            write_head(repository, output)?;
116            200
117        }
118        ("GET", "info/refs") => {
119            return advertise(&route, output).map(Plan::Buffered);
120        }
121        ("GET", "objects/info/packs") => 200,
122        ("GET", path) if path.starts_with("refs/") => serve_reference(repository, path, output)?,
123        ("GET", path) if path.starts_with("objects/") => {
124            if let Some(index) = loose_index(repository, &path.as_bytes()[8..]) {
125                return Ok(Plan::Loose(index));
126            }
127            404
128        }
129        ("POST", "git-upload-pack") => {
130            if route.version == route::Version::V2
131                && v2::serve_refs(repository, request.body, output)?
132            {
133                return Ok(Plan::Buffered(upload_pack::response()));
134            }
135            return Ok(Plan::Upload(route.version));
136        }
137        ("GET" | "POST", _) => 404,
138        _ => 405,
139    };
140    Ok(Plan::Buffered(Response {
141        status,
142        content_type: "text/plain; charset=utf-8",
143    }))
144}
145
146fn write_head(repository: &impl Repository, output: &mut impl Write) -> Result<()> {
147    if let Some(target) = repository.head_state().target() {
148        output.write_all(b"ref: ")?;
149        output.write_all(target.as_bytes())?;
150    } else {
151        output.write_all(&repository.head().to_hex())?;
152    }
153    output.write_all(b"\n")
154}
155
156fn serve_reference(
157    repository: &impl Repository,
158    path: &str,
159    output: &mut impl Write,
160) -> Result<u16> {
161    let index = (0..repository.reference_count()).find(|&index| {
162        repository
163            .reference_name(index)
164            .is_some_and(|name| route::decoded(path).eq(name.bytes().map(Ok)))
165    });
166    let Some(index) = index else { return Ok(404) };
167    if let Some(target) = repository.reference_symbolic_target(index) {
168        output.write_all(b"ref: ")?;
169        output.write_all(target.as_bytes())?;
170    } else {
171        output.write_all(
172            &repository
173                .reference_id(index)
174                .ok_or(Error::InvalidObject)?
175                .to_hex(),
176        )?;
177    }
178    output.write_all(b"\n")?;
179    Ok(200)
180}
181
182fn loose_index(repository: &impl Repository, path: &[u8]) -> Option<usize> {
183    let width = repository.object_format().hex_length();
184    if path.len() != width + 1 || path[2] != b'/' {
185        return None;
186    }
187    let mut storage = [0; 64];
188    let hex = &mut storage[..width];
189    hex[..2].copy_from_slice(&path[..2]);
190    hex[2..].copy_from_slice(&path[3..]);
191    ObjectId::from_hex(hex)
192        .ok()
193        .and_then(|id| repository.find_index(id))
194}