1pub use crate::upload_pack::{Acknowledgments, ShallowUpdates};
5use crate::{route::Version, upload_pack::UploadRequest, v2};
6use git_zero_core::{
7 Error, FetchOptions, ObjectFormat, ObjectId, Result,
8 io::Write,
9 pktline::{self, Packet},
10 repository::validate_ref_name_bytes,
11};
12
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14pub enum Command {
15 Noop,
16 Refs,
17 Fetch,
18}
19#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20pub enum ObjectRequest {
21 Want(ObjectId),
22 Have(ObjectId),
23 Shallow(ObjectId),
24}
25
26#[expect(
27 clippy::large_enum_variant,
28 reason = "Allocation-free requests keep the bounded ref-prefix index inline"
29)]
30enum Parsed<'a> {
31 Noop,
32 Refs(v2::Refs<'a>),
33 Fetch(UploadRequest<'a>),
34}
35
36pub struct Request<'a> {
39 parsed: Parsed<'a>,
40 objects: &'a [u8],
41 format: ObjectFormat,
42}
43impl<'a> Request<'a> {
44 pub fn parse(body: &'a [u8], format: ObjectFormat, version: u8) -> Result<Self> {
45 let version = version_value(version)?;
46 let (parsed, objects) = if version == Version::V2 {
47 match v2::Command::parse(body, format)? {
48 None => (Parsed::Noop, &body[..0]),
49 Some(command) if command.name == b"ls-refs" => {
50 (Parsed::Refs(v2::Refs::parse(command.args)?), &body[..0])
51 }
52 Some(command) => (
53 Parsed::Fetch(UploadRequest::parse(body, format, version, |_| Ok(()))?),
54 command.args,
55 ),
56 }
57 } else {
58 (
59 Parsed::Fetch(UploadRequest::parse(body, format, version, |_| Ok(()))?),
60 body,
61 )
62 };
63 Ok(Self {
64 parsed,
65 objects,
66 format,
67 })
68 }
69 pub fn command(&self) -> Command {
70 match self.parsed {
71 Parsed::Noop => Command::Noop,
72 Parsed::Refs(_) => Command::Refs,
73 Parsed::Fetch(_) => Command::Fetch,
74 }
75 }
76 pub fn done(&self) -> bool {
77 matches!(&self.parsed, Parsed::Fetch(request) if request.done)
78 }
79 pub fn options(&self) -> Option<FetchOptions> {
80 match &self.parsed {
81 Parsed::Fetch(request) => Some(request.extensions.options),
82 _ => None,
83 }
84 }
85 pub fn require_complete(&self) -> Result<()> {
86 match &self.parsed {
87 Parsed::Fetch(request) => request.require_complete(),
88 _ => Ok(()),
89 }
90 }
91 pub fn ofs_delta(&self) -> bool {
93 matches!(&self.parsed, Parsed::Fetch(request) if request.ofs_delta())
94 }
95 pub fn include_tags(&self) -> bool {
96 matches!(&self.parsed, Parsed::Fetch(request) if request.include_tags())
97 }
98 pub fn band(&self) -> u8 {
99 match &self.parsed {
100 Parsed::Fetch(request) => request.band(),
101 _ => 0,
102 }
103 }
104 pub fn objects(&self) -> Objects<'a> {
105 Objects {
106 bytes: self.objects,
107 }
108 }
109 pub fn acknowledgments(&self) -> Result<Acknowledgments> {
110 let Parsed::Fetch(request) = &self.parsed else {
111 return Err(Error::InvalidRequest);
112 };
113 Ok(Acknowledgments::new(request))
114 }
115 pub fn shallow_updates(&self) -> Result<ShallowUpdates> {
116 let Parsed::Fetch(request) = &self.parsed else {
117 return Err(Error::InvalidRequest);
118 };
119 Ok(ShallowUpdates::new(request, self.format))
120 }
121 pub fn reference(
122 &self,
123 id: Option<ObjectId>,
124 name: &[u8],
125 symbolic: Option<&[u8]>,
126 peeled: Option<ObjectId>,
127 output: &mut impl Write,
128 ) -> Result<()> {
129 let Parsed::Refs(request) = &self.parsed else {
130 return Err(Error::InvalidRequest);
131 };
132 if name != b"HEAD" {
133 validate_ref_name_bytes(name)?;
134 }
135 if let Some(target) = symbolic {
136 validate_ref_name_bytes(target)?;
137 }
138 if id
139 .into_iter()
140 .chain(peeled)
141 .any(|id| id.format() != self.format || id.is_zero())
142 {
143 return Err(Error::InvalidObjectId);
144 }
145 request.write_ref(id, name, symbolic, peeled, output)
146 }
147}
148
149pub struct Objects<'a> {
150 bytes: &'a [u8],
151}
152impl Iterator for Objects<'_> {
153 type Item = ObjectRequest;
154 fn next(&mut self) -> Option<Self::Item> {
155 while let Some(packet) = pktline::read(&mut self.bytes).ok().flatten() {
156 let Packet::Data(line) = packet else {
157 continue;
158 };
159 let line = line.strip_suffix(b"\n").unwrap_or(line);
160 let (marker, value) = crate::upload_pack::extensions::argument_parts(line);
161 let Some(value) = value else {
162 continue;
163 };
164 let value = value.split(|&byte| byte == b' ').next()?;
165 let constructor: fn(ObjectId) -> ObjectRequest = match marker {
166 b"want" => ObjectRequest::Want,
167 b"have" => ObjectRequest::Have,
168 b"shallow" => ObjectRequest::Shallow,
169 _ => continue,
170 };
171 return ObjectId::from_hex(value).ok().map(constructor);
173 }
174 None
175 }
176}
177
178fn version_value(version: u8) -> Result<Version> {
179 match version {
180 0 => Ok(Version::V0),
181 1 => Ok(Version::V1),
182 2 => Ok(Version::V2),
183 _ => Err(Error::Unsupported),
184 }
185}
186
187pub fn protocol_version(header: Option<&str>) -> Result<u8> {
188 Ok(match crate::route::version(header)? {
189 Version::V0 => 0,
190 Version::V1 => 1,
191 Version::V2 => 2,
192 })
193}
194
195pub fn advertise(
196 format: ObjectFormat,
197 version: u8,
198 selection: bool,
199 output: &mut impl Write,
200) -> Result<()> {
201 let version = version_value(version)?;
202 pktline::write(output, b"# service=git-upload-pack\n")?;
203 output.write_all(b"0000")?;
204 if version == Version::V2 {
205 v2::advertise(format, output, selection)?;
206 } else if version == Version::V1 {
207 pktline::write(output, b"version 1\n")?;
208 }
209 Ok(())
210}
211
212#[derive(Clone, Copy, Debug, Default)]
214pub struct Features {
215 pub selection: bool,
216 pub reachable_wants: bool,
219}
220
221pub fn reference(
225 format: ObjectFormat,
226 id: Option<ObjectId>,
227 name: &[u8],
228 first: bool,
229 head: Option<&[u8]>,
230 features: Features,
231 output: &mut impl Write,
232) -> Result<()> {
233 if name != b"HEAD" && id.is_some() {
234 validate_ref_name_bytes(name)?;
235 }
236 if id.is_some_and(|id| id.format() != format || id.is_zero()) {
237 return Err(Error::InvalidObjectId);
238 }
239 if let Some(target) = head {
240 validate_ref_name_bytes(target)?;
241 }
242 let id = id.unwrap_or_else(|| format.zero_id()).to_hex();
243 let name = if name.is_empty() {
244 b"capabilities^{}".as_slice()
245 } else {
246 name
247 };
248 if first {
249 let target: &[u8] = head.unwrap_or_default();
250 let symref: &[u8] = if head.is_some() {
251 b" symref=HEAD:"
252 } else {
253 b""
254 };
255 pktline::write_parts(
256 output,
257 &[
258 id.as_bytes(),
259 b" ",
260 name,
261 b"\0",
262 format.capability(),
263 crate::upload_pack::capabilities(features.selection),
264 if features.reachable_wants {
265 crate::upload_pack::policy::REACHABLE
266 } else {
267 b""
268 },
269 symref,
270 target,
271 b"\n",
272 ],
273 )
274 } else {
275 pktline::write_parts(output, &[id.as_bytes(), b" ", name, b"\n"])
276 }
277}
278
279pub fn pack_chunk(band: u8, bytes: &[u8], output: &mut impl Write) -> Result<()> {
282 let capacity = match band {
283 0 => return output.write_all(bytes),
284 1 => 995,
285 2 => pktline::MAX_LENGTH - 5,
286 _ => return Err(Error::InvalidRequest),
287 };
288 for bytes in bytes.chunks(capacity) {
289 pktline::write_parts(output, &[b"\x01", bytes])?;
290 }
291 Ok(())
292}
293
294pub fn peeled_reference(
295 format: ObjectFormat,
296 id: ObjectId,
297 name: &[u8],
298 output: &mut impl Write,
299) -> Result<()> {
300 validate_ref_name_bytes(name)?;
301 if id.format() != format || id.is_zero() {
302 return Err(Error::InvalidObjectId);
303 }
304 pktline::write_parts(output, &[id.to_hex().as_bytes(), b" ", name, b"^{}\n"])
305}