1use sley_core::{Capability, GitError, ObjectFormat, ObjectId, Result};
2use std::io::{Read, Write};
3
4use crate::pktline::{
5 FetchHeadRecord, FetchRefUpdate, PktLineFrame, ProtocolVersion, RefSpec, line_from_str,
6 parse_oid_argument, parse_protocol_v2_line_text, read_pkt_line_frames_until_flush,
7 refspec_map_source, refspec_matches_source, trim_trailing_lf, validate_capability_field,
8 validate_fetch_head_description_field, validate_fetch_head_line, validate_protocol_v2_token,
9 validate_refspec_shape, write_pkt_line_payload,
10};
11
12use crate::v1::{encode_v1_version_frame, is_v1_version_payload, write_v1_version_line};
13
14pub fn fetch_head_ref_description(refname: &str) -> Result<String> {
15 validate_fetch_head_description_field(refname)?;
16 if refname == "HEAD" {
20 Ok(String::new())
21 } else if let Some(branch) = refname.strip_prefix("refs/heads/") {
22 Ok(format!("branch '{branch}'"))
23 } else if let Some(tag) = refname.strip_prefix("refs/tags/") {
24 Ok(format!("tag '{tag}'"))
25 } else if let Some(rest) = refname.strip_prefix("refs/remotes/") {
26 Ok(format!("remote-tracking branch '{rest}'"))
27 } else {
28 Ok(format!("'{refname}'"))
29 }
30}
31
32pub fn fetch_head_remote_description(refname: &str, remote: &str) -> Result<String> {
33 validate_fetch_head_description_field(remote)?;
34 let what = fetch_head_ref_description(refname)?;
37 if what.is_empty() {
38 Ok(remote.to_string())
39 } else {
40 Ok(format!("{what} of {remote}"))
41 }
42}
43
44pub fn parse_fetch_head(format: ObjectFormat, input: &[u8]) -> Result<Vec<FetchHeadRecord>> {
45 if input.is_empty() {
46 return Ok(Vec::new());
47 }
48 input
49 .split_inclusive(|byte| *byte == b'\n')
50 .map(|line| parse_fetch_head_record(format, line))
51 .collect()
52}
53
54pub fn encode_fetch_head(records: &[FetchHeadRecord]) -> Result<Vec<u8>> {
55 let mut out = Vec::new();
56 for record in records {
57 validate_fetch_head_description_field(&record.description)?;
58 out.extend_from_slice(record.oid.to_string().as_bytes());
59 out.push(b'\t');
60 if record.not_for_merge {
61 out.extend_from_slice(b"not-for-merge");
62 }
63 out.push(b'\t');
64 out.extend_from_slice(record.description.as_bytes());
65 out.push(b'\n');
66 }
67 Ok(out)
68}
69
70pub fn read_fetch_head(
71 format: ObjectFormat,
72 reader: &mut impl Read,
73) -> Result<Vec<FetchHeadRecord>> {
74 let mut input = Vec::new();
75 reader.read_to_end(&mut input)?;
76 parse_fetch_head(format, &input)
77}
78
79pub fn write_fetch_head(writer: &mut impl Write, records: &[FetchHeadRecord]) -> Result<()> {
80 for record in records {
81 validate_fetch_head_description_field(&record.description)?;
82 writer.write_all(record.oid.to_string().as_bytes())?;
83 writer.write_all(b"\t")?;
84 if record.not_for_merge {
85 writer.write_all(b"not-for-merge")?;
86 }
87 writer.write_all(b"\t")?;
88 writer.write_all(record.description.as_bytes())?;
89 writer.write_all(b"\n")?;
90 }
91 Ok(())
92}
93
94fn find_advertised_ref_by_name_abbrev<'a>(
100 refs: &'a [RefAdvertisement],
101 name: &str,
102) -> Option<&'a RefAdvertisement> {
103 let mut best: Option<(&RefAdvertisement, usize)> = None;
104 for reference in refs {
105 let score = fetch_refname_match_score(name, &reference.name);
106 if score > best.map(|(_, score)| score).unwrap_or(0) {
107 best = Some((reference, score));
108 }
109 }
110 best.map(|(reference, _)| reference)
111}
112
113fn fetch_refname_match_score(abbrev: &str, full: &str) -> usize {
116 let expansions = [
117 abbrev.to_string(),
118 format!("refs/{abbrev}"),
119 format!("refs/tags/{abbrev}"),
120 format!("refs/heads/{abbrev}"),
121 format!("refs/remotes/{abbrev}"),
122 format!("refs/remotes/{abbrev}/HEAD"),
123 ];
124 for (index, candidate) in expansions.iter().enumerate() {
125 if candidate == full {
126 return expansions.len() - index;
127 }
128 }
129 0
130}
131
132pub fn refname_matches(abbrev: &str, full: &str) -> bool {
137 fetch_refname_match_score(abbrev, full) > 0
138}
139
140fn fetch_local_ref_name(name: &str) -> String {
144 if name.starts_with("refs/") {
145 name.to_string()
146 } else if name.starts_with("heads/")
147 || name.starts_with("tags/")
148 || name.starts_with("remotes/")
149 {
150 format!("refs/{name}")
151 } else {
152 format!("refs/heads/{name}")
153 }
154}
155
156pub fn plan_fetch_ref_updates(
157 format: ObjectFormat,
158 refs: &[RefAdvertisement],
159 refspecs: &[RefSpec],
160 auto_follow_tags: bool,
161) -> Result<Vec<FetchRefUpdate>> {
162 let negative = refspecs
163 .iter()
164 .filter(|refspec| refspec.negative)
165 .collect::<Vec<_>>();
166 let mut updates = Vec::new();
167 for refspec in refspecs.iter().filter(|refspec| !refspec.negative) {
168 validate_refspec_shape(refspec)?;
169 let Some(src) = refspec.src.as_deref() else {
170 return Err(GitError::InvalidFormat(
171 "fetch refspec is missing a source".into(),
172 ));
173 };
174 if refspec.pattern {
175 for reference in refs {
176 if refspec_is_excluded(&negative, &reference.name)? {
177 continue;
178 }
179 if let Some(dst) = refspec_map_source(refspec, &reference.name)? {
180 updates.push(FetchRefUpdate {
181 src: reference.name.clone(),
182 dst: Some(dst),
183 oid: reference.oid,
184 not_for_merge: false,
185 force: refspec.force,
186 });
187 }
188 }
189 continue;
190 }
191 if refspec_is_excluded(&negative, src)? {
192 continue;
193 }
194 let exact_oid = ObjectId::from_hex(format, src).ok();
195 let (source, oid) = match (find_advertised_ref_by_name_abbrev(refs, src), exact_oid) {
196 (Some(reference), _) => (reference.name.clone(), reference.oid),
197 (None, Some(oid)) => (src.to_string(), oid),
198 (None, None) => {
199 return Err(GitError::reference_not_found(format!("remote ref {src}")));
200 }
201 };
202 updates.push(FetchRefUpdate {
203 src: source,
204 dst: refspec.dst.as_deref().map(fetch_local_ref_name),
205 oid,
206 not_for_merge: false,
207 force: refspec.force,
208 });
209 }
210 if auto_follow_tags && updates.iter().any(|update| update.dst.is_some()) {
211 let fetched_oids = updates.iter().map(|update| update.oid).collect::<Vec<_>>();
212 let fetched_srcs = updates
213 .iter()
214 .map(|update| update.src.clone())
215 .collect::<Vec<_>>();
216 for reference in refs {
217 if reference.name.starts_with("refs/tags/")
218 && fetched_oids.iter().any(|oid| oid == &reference.oid)
219 && !fetched_srcs.contains(&reference.name)
220 && !refspec_is_excluded(&negative, &reference.name)?
221 {
222 updates.push(FetchRefUpdate {
223 src: reference.name.clone(),
224 dst: Some(reference.name.clone()),
225 oid: reference.oid,
226 not_for_merge: true,
227 force: false,
228 });
229 }
230 }
231 }
232 Ok(updates)
233}
234
235pub fn fetch_ref_updates_to_fetch_head(
236 updates: &[FetchRefUpdate],
237 remote: &str,
238) -> Result<Vec<FetchHeadRecord>> {
239 updates
240 .iter()
241 .map(|update| {
242 Ok(FetchHeadRecord {
243 oid: update.oid,
244 not_for_merge: update.not_for_merge,
245 description: fetch_head_remote_description(&update.src, remote)?,
246 })
247 })
248 .collect()
249}
250
251#[derive(Debug, Clone, PartialEq, Eq)]
252pub struct TransportHandshake {
253 pub protocol: ProtocolVersion,
254 pub capabilities: Vec<Capability>,
255}
256
257#[derive(Debug, Clone, PartialEq, Eq)]
258pub struct RefAdvertisement {
259 pub oid: ObjectId,
260 pub name: String,
261 pub capabilities: Vec<Capability>,
262}
263
264#[derive(Debug, Clone, PartialEq, Eq)]
265pub struct DumbHttpRefRecord {
266 pub oid: ObjectId,
267 pub name: String,
268 pub peeled: bool,
269}
270
271#[derive(Debug, Clone, PartialEq, Eq)]
272pub struct DumbHttpPackRecord {
273 pub hash: ObjectId,
274}
275
276#[derive(Debug, Clone, PartialEq, Eq)]
277pub struct RefAdvertisementSet {
278 pub protocol: ProtocolVersion,
279 pub refs: Vec<RefAdvertisement>,
280 pub shallow: Vec<ObjectId>,
281}
282pub fn parse_capabilities(input: &[u8]) -> Result<Vec<Capability>> {
283 let input = trim_trailing_lf(input);
284 if input.is_empty() {
285 return Ok(Vec::new());
286 }
287 let text =
288 std::str::from_utf8(input).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
289 text.split(' ')
290 .map(parse_capability_token)
291 .collect::<Result<Vec<_>>>()
292}
293
294pub fn encode_capabilities(capabilities: &[Capability]) -> Result<Vec<u8>> {
295 let mut out = Vec::new();
296 for (idx, capability) in capabilities.iter().enumerate() {
297 validate_capability_field("capability name", &capability.name)?;
298 if idx != 0 {
299 out.push(b' ');
300 }
301 out.extend_from_slice(capability.name.as_bytes());
302 if let Some(value) = &capability.value {
303 validate_capability_field("capability value", value)?;
304 out.push(b'=');
305 out.extend_from_slice(value.as_bytes());
306 }
307 }
308 Ok(out)
309}
310
311pub fn parse_ref_advertisement(format: ObjectFormat, payload: &[u8]) -> Result<RefAdvertisement> {
312 let payload = trim_trailing_lf(payload);
313 let (reference, capabilities) = match payload.iter().position(|byte| *byte == 0) {
314 Some(idx) => (&payload[..idx], parse_capabilities(&payload[idx + 1..])?),
315 None => (payload, Vec::new()),
316 };
317 let text =
318 std::str::from_utf8(reference).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
319 let (oid, name) = text
320 .split_once(' ')
321 .ok_or_else(|| GitError::InvalidFormat("advertised ref is missing name".into()))?;
322 if name.is_empty() {
323 return Err(GitError::InvalidFormat(
324 "advertised ref name is empty".into(),
325 ));
326 }
327 Ok(RefAdvertisement {
328 oid: ObjectId::from_hex(format, oid)?,
329 name: name.to_string(),
330 capabilities,
331 })
332}
333
334pub fn encode_ref_advertisement(advertisement: &RefAdvertisement) -> Result<Vec<u8>> {
335 validate_protocol_v2_token("advertised ref name", &advertisement.name)?;
336 let mut out = advertisement.oid.to_string().into_bytes();
337 out.push(b' ');
338 out.extend_from_slice(advertisement.name.as_bytes());
339 if !advertisement.capabilities.is_empty() {
340 out.push(0);
341 out.extend_from_slice(&encode_capabilities(&advertisement.capabilities)?);
342 }
343 out.push(b'\n');
344 Ok(out)
345}
346
347pub fn parse_ref_advertisements(
348 format: ObjectFormat,
349 frames: &[PktLineFrame],
350) -> Result<Vec<RefAdvertisement>> {
351 Ok(parse_ref_advertisement_set(format, frames)?.refs)
352}
353
354pub fn parse_ref_advertisement_set(
355 format: ObjectFormat,
356 frames: &[PktLineFrame],
357) -> Result<RefAdvertisementSet> {
358 let mut set = RefAdvertisementSet {
359 protocol: ProtocolVersion::V0,
360 refs: Vec::new(),
361 shallow: Vec::new(),
362 };
363 let mut saw_flush = false;
364 let mut in_shallow = false;
365 for (idx, frame) in frames.iter().enumerate() {
366 match frame {
367 PktLineFrame::Data(payload) if !saw_flush => {
368 let trimmed = trim_trailing_lf(payload);
369 if is_v1_version_payload(payload) {
370 if idx != 0 {
371 return Err(GitError::InvalidFormat(
372 "advertised ref protocol version must be the first line".into(),
373 ));
374 }
375 set.protocol = ProtocolVersion::V1;
376 continue;
377 }
378 if trimmed.starts_with(b"version ") {
379 return Err(GitError::InvalidFormat(
380 "unsupported advertised ref protocol version".into(),
381 ));
382 }
383 if trimmed.starts_with(b"shallow ") {
384 if set.refs.is_empty() {
385 return Err(GitError::InvalidFormat(
386 "advertised shallow refs must follow advertised refs".into(),
387 ));
388 }
389 let text = parse_protocol_v2_line_text("advertised shallow ref", payload)?;
390 set.shallow.push(parse_oid_argument(
391 format,
392 "advertised shallow ref",
393 text,
394 "shallow ",
395 )?);
396 in_shallow = true;
397 continue;
398 }
399 if in_shallow {
400 return Err(GitError::InvalidFormat(
401 "advertised refs must not follow shallow refs".into(),
402 ));
403 }
404 let advertisement = parse_ref_advertisement(format, payload)?;
405 if !set.refs.is_empty() && !advertisement.capabilities.is_empty() {
406 return Err(GitError::InvalidFormat(
407 "advertised ref capabilities must appear on the first ref".into(),
408 ));
409 }
410 set.refs.push(advertisement);
411 }
412 PktLineFrame::Data(_) => {
413 return Err(GitError::InvalidFormat(
414 "advertised ref stream has data after flush".into(),
415 ));
416 }
417 PktLineFrame::Flush => {
418 saw_flush = true;
419 if idx + 1 != frames.len() {
420 return Err(GitError::InvalidFormat(
421 "advertised ref stream has frames after flush".into(),
422 ));
423 }
424 }
425 PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
426 return Err(GitError::InvalidFormat(
427 "advertised ref stream contains a non-flush control packet".into(),
428 ));
429 }
430 }
431 }
432 if !saw_flush {
433 return Err(GitError::InvalidFormat(
434 "advertised ref stream missing flush".into(),
435 ));
436 }
437 Ok(set)
438}
439
440pub fn encode_ref_advertisements(advertisements: &[RefAdvertisement]) -> Result<Vec<PktLineFrame>> {
441 encode_ref_advertisement_set(&RefAdvertisementSet {
442 protocol: ProtocolVersion::V0,
443 refs: advertisements.to_vec(),
444 shallow: Vec::new(),
445 })
446}
447
448pub fn encode_ref_advertisement_set(set: &RefAdvertisementSet) -> Result<Vec<PktLineFrame>> {
449 let mut frames = Vec::new();
450 match set.protocol {
451 ProtocolVersion::V0 => {}
452 ProtocolVersion::V1 => frames.push(encode_v1_version_frame()?),
453 ProtocolVersion::V2 => {
454 return Err(GitError::InvalidFormat(
455 "protocol v2 does not use v0/v1 advertised-ref streams".into(),
456 ));
457 }
458 }
459 if set.refs.is_empty() && !set.shallow.is_empty() {
460 return Err(GitError::InvalidFormat(
461 "advertised shallow refs require advertised refs".into(),
462 ));
463 }
464 for (idx, advertisement) in set.refs.iter().enumerate() {
465 if idx != 0 && !advertisement.capabilities.is_empty() {
466 return Err(GitError::InvalidFormat(
467 "advertised ref capabilities must appear on the first ref".into(),
468 ));
469 }
470 frames.push(PktLineFrame::data(encode_ref_advertisement(
471 advertisement,
472 )?)?);
473 }
474 for oid in &set.shallow {
475 frames.push(PktLineFrame::data(line_from_str(&format!(
476 "shallow {oid}"
477 )))?);
478 }
479 frames.push(PktLineFrame::Flush);
480 Ok(frames)
481}
482
483pub fn read_ref_advertisements(
484 format: ObjectFormat,
485 reader: &mut impl Read,
486) -> Result<Vec<RefAdvertisement>> {
487 let frames = read_pkt_line_frames_until_flush(reader)?;
488 parse_ref_advertisements(format, &frames)
489}
490
491pub fn read_ref_advertisement_set(
492 format: ObjectFormat,
493 reader: &mut impl Read,
494) -> Result<RefAdvertisementSet> {
495 let frames = read_pkt_line_frames_until_flush(reader)?;
496 parse_ref_advertisement_set(format, &frames)
497}
498
499pub fn write_ref_advertisements(
500 writer: &mut impl Write,
501 advertisements: &[RefAdvertisement],
502) -> Result<()> {
503 write_ref_advertisement_stream(writer, ProtocolVersion::V0, advertisements, &[])
504}
505
506pub fn write_ref_advertisement_set(
507 writer: &mut impl Write,
508 set: &RefAdvertisementSet,
509) -> Result<()> {
510 write_ref_advertisement_stream(writer, set.protocol, &set.refs, &set.shallow)
511}
512
513fn write_ref_advertisement_stream(
514 writer: &mut impl Write,
515 protocol: ProtocolVersion,
516 refs: &[RefAdvertisement],
517 shallow: &[ObjectId],
518) -> Result<()> {
519 match protocol {
520 ProtocolVersion::V0 => {}
521 ProtocolVersion::V1 => write_v1_version_line(writer)?,
522 ProtocolVersion::V2 => {
523 return Err(GitError::InvalidFormat(
524 "protocol v2 does not use v0/v1 advertised-ref streams".into(),
525 ));
526 }
527 }
528 if refs.is_empty() && !shallow.is_empty() {
529 return Err(GitError::InvalidFormat(
530 "advertised shallow refs require advertised refs".into(),
531 ));
532 }
533 for (idx, advertisement) in refs.iter().enumerate() {
534 if idx != 0 && !advertisement.capabilities.is_empty() {
535 return Err(GitError::InvalidFormat(
536 "advertised ref capabilities must appear on the first ref".into(),
537 ));
538 }
539 write_pkt_line_payload(writer, &encode_ref_advertisement(advertisement)?)?;
540 }
541 for oid in shallow {
542 write_pkt_line_payload(writer, &line_from_str(&format!("shallow {oid}")))?;
543 }
544 writer.write_all(b"0000")?;
545 Ok(())
546}
547
548pub fn parse_dumb_http_info_refs(
549 format: ObjectFormat,
550 input: &[u8],
551) -> Result<Vec<DumbHttpRefRecord>> {
552 if input.is_empty() {
553 return Ok(Vec::new());
554 }
555 input
556 .split_inclusive(|byte| *byte == b'\n')
557 .map(|line| parse_dumb_http_info_ref_record(format, line))
558 .collect()
559}
560
561pub fn encode_dumb_http_info_refs(records: &[DumbHttpRefRecord]) -> Result<Vec<u8>> {
562 let mut out = Vec::new();
563 for record in records {
564 validate_dumb_http_ref_name(&record.name)?;
565 out.extend_from_slice(record.oid.to_string().as_bytes());
566 out.push(b'\t');
567 out.extend_from_slice(record.name.as_bytes());
568 if record.peeled {
569 out.extend_from_slice(b"^{}");
570 }
571 out.push(b'\n');
572 }
573 Ok(out)
574}
575
576pub fn read_dumb_http_info_refs(
577 format: ObjectFormat,
578 reader: &mut impl Read,
579) -> Result<Vec<DumbHttpRefRecord>> {
580 let mut input = Vec::new();
581 reader.read_to_end(&mut input)?;
582 parse_dumb_http_info_refs(format, &input)
583}
584
585pub fn write_dumb_http_info_refs(
586 writer: &mut impl Write,
587 records: &[DumbHttpRefRecord],
588) -> Result<()> {
589 for record in records {
590 validate_dumb_http_ref_name(&record.name)?;
591 writer.write_all(record.oid.to_string().as_bytes())?;
592 writer.write_all(b"\t")?;
593 writer.write_all(record.name.as_bytes())?;
594 if record.peeled {
595 writer.write_all(b"^{}")?;
596 }
597 writer.write_all(b"\n")?;
598 }
599 Ok(())
600}
601
602pub fn parse_dumb_http_alternates(input: &[u8]) -> Result<Vec<String>> {
603 if input.is_empty() {
604 return Ok(Vec::new());
605 }
606 input
607 .split_inclusive(|byte| *byte == b'\n')
608 .map(parse_dumb_http_alternate)
609 .collect()
610}
611
612pub fn encode_dumb_http_alternates(alternates: &[String]) -> Result<Vec<u8>> {
613 let mut out = Vec::new();
614 for alternate in alternates {
615 validate_dumb_http_alternate(alternate)?;
616 out.extend_from_slice(alternate.as_bytes());
617 out.push(b'\n');
618 }
619 Ok(out)
620}
621
622pub fn read_dumb_http_alternates(reader: &mut impl Read) -> Result<Vec<String>> {
623 let mut input = Vec::new();
624 reader.read_to_end(&mut input)?;
625 parse_dumb_http_alternates(&input)
626}
627
628pub fn write_dumb_http_alternates(writer: &mut impl Write, alternates: &[String]) -> Result<()> {
629 for alternate in alternates {
630 validate_dumb_http_alternate(alternate)?;
631 writer.write_all(alternate.as_bytes())?;
632 writer.write_all(b"\n")?;
633 }
634 Ok(())
635}
636
637pub fn parse_dumb_http_packs(
638 format: ObjectFormat,
639 input: &[u8],
640) -> Result<Vec<DumbHttpPackRecord>> {
641 if input.is_empty() {
642 return Ok(Vec::new());
643 }
644 input
645 .split_inclusive(|byte| *byte == b'\n')
646 .map(|line| parse_dumb_http_pack_record(format, line))
647 .collect()
648}
649
650pub fn encode_dumb_http_packs(records: &[DumbHttpPackRecord]) -> Result<Vec<u8>> {
651 let mut out = Vec::new();
652 for record in records {
653 out.extend_from_slice(format!("P pack-{}.pack\n", record.hash).as_bytes());
654 }
655 Ok(out)
656}
657
658pub fn read_dumb_http_packs(
659 format: ObjectFormat,
660 reader: &mut impl Read,
661) -> Result<Vec<DumbHttpPackRecord>> {
662 let mut input = Vec::new();
663 reader.read_to_end(&mut input)?;
664 parse_dumb_http_packs(format, &input)
665}
666
667pub fn write_dumb_http_packs(
668 writer: &mut impl Write,
669 records: &[DumbHttpPackRecord],
670) -> Result<()> {
671 for record in records {
672 writer.write_all(format!("P pack-{}.pack\n", record.hash).as_bytes())?;
673 }
674 Ok(())
675}
676fn parse_capability_token(token: &str) -> Result<Capability> {
677 if token.is_empty() {
678 return Err(GitError::InvalidFormat("empty capability token".into()));
679 }
680 let (name, value) = token
681 .split_once('=')
682 .map_or((token, None), |(name, value)| (name, Some(value)));
683 validate_capability_field("capability name", name)?;
684 if let Some(value) = value {
685 validate_capability_field("capability value", value)?;
686 }
687 Ok(Capability {
688 name: name.to_string(),
689 value: value.map(str::to_string),
690 })
691}
692
693fn parse_fetch_head_record(format: ObjectFormat, line: &[u8]) -> Result<FetchHeadRecord> {
694 validate_fetch_head_line(line)?;
695 let line = trim_trailing_lf(line);
696 let mut fields = line.splitn(3, |byte| *byte == b'\t');
697 let oid = fields
698 .next()
699 .ok_or_else(|| GitError::InvalidFormat("FETCH_HEAD record is missing oid".into()))?;
700 let merge_marker = fields.next().ok_or_else(|| {
701 GitError::InvalidFormat("FETCH_HEAD record is missing merge marker".into())
702 })?;
703 let description = fields.next().ok_or_else(|| {
704 GitError::InvalidFormat("FETCH_HEAD record is missing description".into())
705 })?;
706 let oid = std::str::from_utf8(oid).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
707 validate_protocol_v2_token("FETCH_HEAD oid", oid)?;
708 let not_for_merge = match merge_marker {
709 b"" => false,
710 b"not-for-merge" => true,
711 _ => {
712 return Err(GitError::InvalidFormat(
713 "FETCH_HEAD record has invalid merge marker".into(),
714 ));
715 }
716 };
717 let description =
718 std::str::from_utf8(description).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
719 validate_fetch_head_description_field(description)?;
720 Ok(FetchHeadRecord {
721 oid: ObjectId::from_hex(format, oid)?,
722 not_for_merge,
723 description: description.to_string(),
724 })
725}
726
727fn refspec_is_excluded(negative: &[&RefSpec], source: &str) -> Result<bool> {
728 for refspec in negative {
729 if refspec_matches_source(refspec, source)? {
730 return Ok(true);
731 }
732 }
733 Ok(false)
734}
735
736fn parse_dumb_http_info_ref_record(format: ObjectFormat, line: &[u8]) -> Result<DumbHttpRefRecord> {
737 validate_dumb_http_info_ref_line(line)?;
738 let line = trim_trailing_lf(line);
739 let tab = line
740 .iter()
741 .position(|byte| *byte == b'\t')
742 .ok_or_else(|| GitError::InvalidFormat("dumb HTTP ref record is missing name".into()))?;
743 let (oid, name) = (&line[..tab], &line[tab + 1..]);
744 let oid = std::str::from_utf8(oid).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
745 validate_protocol_v2_token("dumb HTTP ref oid", oid)?;
746 let name = std::str::from_utf8(name).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
747 let (name, peeled) = name
748 .strip_suffix("^{}")
749 .map_or((name, false), |name| (name, true));
750 validate_dumb_http_ref_name(name)?;
751 Ok(DumbHttpRefRecord {
752 oid: ObjectId::from_hex(format, oid)?,
753 name: name.to_string(),
754 peeled,
755 })
756}
757
758fn parse_dumb_http_alternate(line: &[u8]) -> Result<String> {
759 validate_dumb_http_alternate_line(line)?;
760 let line = trim_trailing_lf(line);
761 let alternate =
762 std::str::from_utf8(line).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
763 validate_dumb_http_alternate(alternate)?;
764 Ok(alternate.to_string())
765}
766
767fn parse_dumb_http_pack_record(format: ObjectFormat, line: &[u8]) -> Result<DumbHttpPackRecord> {
768 validate_dumb_http_info_ref_line(line)?;
769 let line = parse_protocol_v2_line_text("dumb HTTP pack record", line)?;
770 let pack_name = line
771 .strip_prefix("P ")
772 .ok_or_else(|| GitError::InvalidFormat("dumb HTTP pack record must start with P".into()))?;
773 let hash = pack_name
774 .strip_prefix("pack-")
775 .and_then(|value| value.strip_suffix(".pack"))
776 .ok_or_else(|| GitError::InvalidFormat("invalid dumb HTTP pack name".into()))?;
777 validate_protocol_v2_token("dumb HTTP pack hash", hash)?;
778 Ok(DumbHttpPackRecord {
779 hash: ObjectId::from_hex(format, hash)?,
780 })
781}
782
783fn validate_dumb_http_info_ref_line(value: &[u8]) -> Result<()> {
784 if value.is_empty() {
785 return Err(GitError::InvalidFormat(
786 "dumb HTTP ref record is empty".into(),
787 ));
788 }
789 if !value.ends_with(b"\n") {
790 return Err(GitError::InvalidFormat(
791 "dumb HTTP ref record missing LF".into(),
792 ));
793 }
794 if value.iter().any(|byte| matches!(*byte, b'\r' | 0)) {
795 return Err(GitError::InvalidFormat(
796 "dumb HTTP ref record contains a delimiter byte".into(),
797 ));
798 }
799 Ok(())
800}
801
802fn validate_dumb_http_ref_name(value: &str) -> Result<()> {
803 validate_protocol_v2_token("dumb HTTP ref name", value)?;
804 if value.ends_with("^{}") {
805 return Err(GitError::InvalidFormat(
806 "dumb HTTP ref name must not include peeled suffix".into(),
807 ));
808 }
809 Ok(())
810}
811
812fn validate_dumb_http_alternate_line(value: &[u8]) -> Result<()> {
813 if value.is_empty() {
814 return Err(GitError::InvalidFormat(
815 "dumb HTTP alternate is empty".into(),
816 ));
817 }
818 if !value.ends_with(b"\n") {
819 return Err(GitError::InvalidFormat(
820 "dumb HTTP alternate missing LF".into(),
821 ));
822 }
823 if value.iter().any(|byte| matches!(*byte, b'\r' | 0)) {
824 return Err(GitError::InvalidFormat(
825 "dumb HTTP alternate contains a delimiter byte".into(),
826 ));
827 }
828 Ok(())
829}
830
831fn validate_dumb_http_alternate(value: &str) -> Result<()> {
832 if value.is_empty() {
833 return Err(GitError::InvalidFormat(
834 "dumb HTTP alternate is empty".into(),
835 ));
836 }
837 if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
838 return Err(GitError::InvalidFormat(
839 "dumb HTTP alternate contains a delimiter byte".into(),
840 ));
841 }
842 Ok(())
843}
844
845#[cfg(test)]
846mod tests {
847 use super::*;
848 use crate::parse_refspec;
849
850 #[test]
851 fn exact_oid_fetch_refspec_does_not_require_an_advertised_ref() -> Result<()> {
852 let oid = ObjectId::from_hex(
853 ObjectFormat::Sha1,
854 "1111111111111111111111111111111111111111",
855 )?;
856 let refspec = parse_refspec(&format!("{oid}:refs/heads/exact"))?;
857
858 let updates = plan_fetch_ref_updates(ObjectFormat::Sha1, &[], &[refspec], false)?;
859
860 assert_eq!(updates.len(), 1);
861 assert_eq!(updates[0].src, oid.to_string());
862 assert_eq!(updates[0].dst.as_deref(), Some("refs/heads/exact"));
863 assert_eq!(updates[0].oid, oid);
864 Ok(())
865 }
866
867 #[test]
868 fn exact_oid_fetch_refspec_uses_the_repository_object_format() -> Result<()> {
869 let sha1 = "1111111111111111111111111111111111111111";
870 let refspec = parse_refspec(&format!("{sha1}:refs/heads/exact"))?;
871 assert!(plan_fetch_ref_updates(ObjectFormat::Sha256, &[], &[refspec], false).is_err());
872 Ok(())
873 }
874}