1#![no_std]
8#![forbid(unsafe_code)]
9
10use git_zero_core::{
11 Error, Object, ObjectFormat, ObjectId, Result,
12 http::Request,
13 io::{SliceWriter, Write},
14 zlib::{InflateLimits, InflateStatus, Inflater, WINDOW_SIZE},
15};
16
17mod reference;
18mod smart;
19pub use reference::{reference_name, resolve_reference};
20pub use smart::{
21 FetchOptions, SmartAdvertisement, read_pack_response, read_pack_response_with_format,
22 read_smart_reference, write_fetch_request, write_fetch_request_iter,
23};
24
25pub trait Transport {
26 fn request(&mut self, request: Request<'_>, response: &mut [u8]) -> Result<(u16, usize)>;
30}
31
32pub fn loose_wire_capacity(body_limit: usize) -> Result<usize> {
35 let source = body_limit.checked_add(32).ok_or(Error::LimitExceeded)?;
36 usize::try_from(git_zero_core::zlib::compress_bound(source as u64)?)
37 .map_err(|_| Error::LimitExceeded)
38}
39
40#[derive(Clone, Copy, Debug, Eq, PartialEq)]
41pub enum Protocol {
42 Smart,
43 Dumb,
44}
45
46#[derive(Clone, Copy, Debug)]
47pub struct Reference<'a> {
48 pub id: ObjectId,
49 pub name: &'a str,
50}
51
52pub struct Discovery<'a> {
53 pub protocol: Protocol,
54 bytes: &'a [u8],
55 format: Option<ObjectFormat>,
56}
57
58impl<'a> Discovery<'a> {
59 pub fn parse(data: &'a [u8]) -> Result<Self> {
62 let (protocol, bytes, format) = if data
63 .get(4..)
64 .is_some_and(|tail| tail.starts_with(b"# service="))
65 {
66 let advertisement = SmartAdvertisement::parse(data)?;
67 (
68 Protocol::Smart,
69 smart::advertisement_body(data)?,
70 advertisement.object_format(),
71 )
72 } else {
73 (Protocol::Dumb, data, None)
74 };
75 let mut discovery = Self {
76 protocol,
77 bytes,
78 format,
79 };
80 let mut references = discovery.references();
81 for reference in references.by_ref() {
82 reference?;
83 }
84 discovery.format = references.format;
85 Ok(discovery)
86 }
87 pub fn references(&self) -> References<'a> {
88 References {
89 protocol: self.protocol,
90 bytes: self.bytes,
91 failed: false,
92 format: self.format,
93 }
94 }
95
96 pub fn head(&self) -> Result<ObjectId> {
99 let mut first = None;
100 for reference in self.references() {
101 let reference = reference?;
102 if reference.name == "HEAD" {
103 return Ok(reference.id);
104 }
105 first.get_or_insert(reference.id);
106 }
107 first.ok_or(Error::ObjectNotFound)
108 }
109}
110
111pub struct References<'a> {
112 protocol: Protocol,
113 bytes: &'a [u8],
114 failed: bool,
115 format: Option<ObjectFormat>,
116}
117
118impl<'a> References<'a> {
119 fn parse(&mut self, line: &'a [u8]) -> Result<Reference<'a>> {
120 let mut parts = line.splitn(2, |&byte| byte == 0);
121 let reference = parts.next().ok_or(Error::InvalidRequest)?;
122 let reference = reference.strip_suffix(b"\n").unwrap_or(reference);
123 let separator = reference
124 .iter()
125 .position(|&byte| matches!(byte, b' ' | b'\t'))
126 .ok_or(Error::InvalidRequest)?;
127 if reference.len() < separator + 2 {
128 return Err(Error::InvalidRequest);
129 }
130 let id = ObjectId::from_hex(&reference[..separator])?;
131 let advertised = if self.protocol == Protocol::Smart {
132 parts.next().and_then(|capabilities| {
133 capabilities
134 .strip_suffix(b"\n")
135 .unwrap_or(capabilities)
136 .split(|&byte| byte == b' ')
137 .find_map(|capability| capability.strip_prefix(b"object-format="))
138 })
139 } else {
140 None
141 };
142 let format = match advertised {
143 Some(b"sha1") => ObjectFormat::Sha1,
144 Some(b"sha256") => ObjectFormat::Sha256,
145 Some(_) => return Err(Error::Unsupported),
146 None => id.format(),
147 };
148 let expected = *self.format.get_or_insert(format);
151 if format != expected || id.format() != expected {
152 return Err(Error::InvalidRequest);
153 }
154 Ok(Reference {
155 id,
156 name: core::str::from_utf8(&reference[separator + 1..])
157 .map_err(|_| Error::InvalidRequest)?,
158 })
159 }
160}
161
162impl<'a> Iterator for References<'a> {
163 type Item = Result<Reference<'a>>;
164 fn next(&mut self) -> Option<Self::Item> {
165 if self.failed {
166 return None;
167 }
168 if self.protocol == Protocol::Smart {
169 let result = smart::next_reference(&mut self.bytes, &mut self.format);
170 if !matches!(result, Ok(Some(_))) {
171 self.failed = true;
172 }
173 return result.transpose();
174 }
175 loop {
176 let line = match self.protocol {
177 Protocol::Smart => unreachable!("handled above"),
178 Protocol::Dumb => {
179 if self.bytes.is_empty() {
180 return None;
181 }
182 let end = self
183 .bytes
184 .iter()
185 .position(|&byte| byte == b'\n')
186 .unwrap_or(self.bytes.len());
187 let line = &self.bytes[..end];
188 self.bytes = self.bytes.get(end + 1..).unwrap_or_default();
189 line
190 }
191 };
192 let result = self.parse(line);
193 if let Ok(reference) = &result {
194 if self.protocol == Protocol::Smart
195 && reference.id.is_zero()
196 && reference.name == "capabilities^{}"
197 {
198 continue;
199 }
200 }
201 if result.is_err() {
202 self.failed = true;
203 }
204 return Some(result);
205 }
206 }
207}
208
209pub fn discover<'a>(
210 transport: &mut impl Transport,
211 storage: &'a mut [u8],
212) -> Result<Discovery<'a>> {
213 let (status, length) = transport.request(
214 Request {
215 content_type: None,
216 git_protocol: None,
217 method: "GET",
218 path: "info/refs",
219 query: Some("service=git-upload-pack"),
220 body: &[],
221 },
222 storage,
223 )?;
224 if status != 200 {
225 return Err(Error::Backend);
226 }
227 Discovery::parse(storage.get(..length).ok_or(Error::BufferTooSmall)?)
228}
229
230pub fn read_head(
231 transport: &mut impl Transport,
232 discovery: &Discovery<'_>,
233 storage: &mut [u8],
234) -> Result<ObjectId> {
235 if discovery.protocol == Protocol::Smart {
236 return discovery.head();
237 }
238 let (status, length) = transport.request(
239 Request {
240 content_type: None,
241 git_protocol: None,
242 method: "GET",
243 path: "HEAD",
244 query: None,
245 body: &[],
246 },
247 storage,
248 )?;
249 if status != 200 {
250 return Err(Error::Backend);
251 }
252 let bytes = storage.get(..length).ok_or(Error::BufferTooSmall)?;
253 let line = bytes.strip_suffix(b"\n").unwrap_or(bytes);
254 if let Some(name) = line.strip_prefix(b"ref: ") {
255 for reference in discovery.references() {
256 let reference = reference?;
257 if reference.name.as_bytes() == name {
258 return Ok(reference.id);
259 }
260 }
261 Err(Error::ObjectNotFound)
262 } else {
263 let id = ObjectId::from_hex(line)?;
264 if discovery.format.is_some_and(|format| format != id.format()) {
265 return Err(Error::InvalidRequest);
266 }
267 Ok(id)
268 }
269}
270
271pub fn fetch_pack<'a>(
273 transport: &mut impl Transport,
274 want: ObjectId,
275 request_storage: &mut [u8],
276 response_storage: &'a mut [u8],
277) -> Result<&'a [u8]> {
278 fetch_pack_with_options(
279 transport,
280 &[want],
281 FetchOptions::default(),
282 request_storage,
283 response_storage,
284 )
285}
286
287pub fn fetch_pack_with_options<'a>(
290 transport: &mut impl Transport,
291 wants: &[ObjectId],
292 options: FetchOptions,
293 request_storage: &mut [u8],
294 response_storage: &'a mut [u8],
295) -> Result<&'a [u8]> {
296 let mut request = SliceWriter::new(request_storage);
297 write_fetch_request(wants, options, &mut request)?;
298 let (status, length) = transport.request(
299 Request {
300 content_type: Some("application/x-git-upload-pack-request"),
301 git_protocol: None,
302 method: "POST",
303 path: "git-upload-pack",
304 query: None,
305 body: request.as_bytes(),
306 },
307 response_storage,
308 )?;
309 if status != 200 {
310 return Err(Error::Backend);
311 }
312 let bytes = response_storage
313 .get(..length)
314 .ok_or(Error::BufferTooSmall)?;
315 read_pack_response_with_format(bytes, wants[0].format())
316}
317
318pub fn fetch_loose<'a>(
320 transport: &mut impl Transport,
321 id: ObjectId,
322 response_storage: &'a mut [u8],
323) -> Result<&'a [u8]> {
324 let hex = id.to_hex();
325 let mut path_storage = [0; 73];
326 let mut path = SliceWriter::new(&mut path_storage);
327 path.write_all(b"objects/")?;
328 path.write_all(&hex[..2])?;
329 path.write_all(b"/")?;
330 path.write_all(&hex[2..])?;
331 let path = core::str::from_utf8(path.as_bytes()).map_err(|_| Error::InvalidRequest)?;
332 let (status, length) = transport.request(
333 Request {
334 content_type: None,
335 git_protocol: None,
336 method: "GET",
337 path,
338 query: None,
339 body: &[],
340 },
341 response_storage,
342 )?;
343 if status == 404 {
344 return Err(Error::ObjectNotFound);
345 }
346 if status != 200 {
347 return Err(Error::Backend);
348 }
349 response_storage.get(..length).ok_or(Error::BufferTooSmall)
350}
351
352pub fn decode_loose<'a>(
357 compressed: &[u8],
358 expected: ObjectId,
359 storage: &'a mut [u8],
360 window: &mut [u8; WINDOW_SIZE],
361 limits: InflateLimits,
362) -> Result<Object<'a>> {
363 let mut decoder = Inflater::new(window, limits);
364 let progress = decoder.update(compressed, storage, true)?;
365 if progress.status == InflateStatus::NeedOutput {
366 return Err(Error::BufferTooSmall);
367 }
368 if progress.status != InflateStatus::Finished || progress.consumed != compressed.len() {
369 return Err(Error::InvalidObject);
370 }
371 Object::from_loose(&storage[..progress.written], expected)
372}
373
374pub fn fetch_loose_object<'a>(
377 transport: &mut impl Transport,
378 expected: ObjectId,
379 response_storage: &mut [u8],
380 object_storage: &'a mut [u8],
381 window: &mut [u8; WINDOW_SIZE],
382 limits: InflateLimits,
383) -> Result<Object<'a>> {
384 let compressed = fetch_loose(transport, expected, response_storage)?;
385 decode_loose(compressed, expected, object_storage, window, limits)
386}