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