1use sley_core::{GitError, ObjectFormat, ObjectId, Result};
2use std::io::{ErrorKind, Read, Write};
3use std::sync::RwLock;
4
5static PACKET_TRACE_IDENTITY: RwLock<Option<String>> = RwLock::new(None);
6
7pub fn set_packet_trace_identity(prog: &str) {
10 if let Ok(mut guard) = PACKET_TRACE_IDENTITY.write() {
11 *guard = Some(prog.to_string());
12 }
13}
14
15fn packet_trace_prefix() -> String {
16 PACKET_TRACE_IDENTITY
17 .read()
18 .ok()
19 .and_then(|guard| guard.clone())
20 .unwrap_or_else(|| "git".to_string())
21}
22
23fn packet_trace_sink() -> Option<Box<dyn Write>> {
27 let value = std::env::var("GIT_TRACE_PACKET").ok()?;
28 let lower = value.to_ascii_lowercase();
29 match lower.as_str() {
30 "" | "0" | "false" => None,
31 "1" | "2" | "true" => Some(Box::new(std::io::stderr())),
32 _ => {
33 if std::path::Path::new(&value).is_absolute() {
34 std::fs::OpenOptions::new()
35 .create(true)
36 .append(true)
37 .open(&value)
38 .ok()
39 .map(|f| Box::new(f) as Box<dyn Write>)
40 } else {
41 None
42 }
43 }
44 }
45}
46
47fn packet_trace_enabled() -> bool {
49 match std::env::var("GIT_TRACE_PACKET") {
50 Ok(value) => {
51 let lower = value.to_ascii_lowercase();
52 !matches!(lower.as_str(), "" | "0" | "false")
53 }
54 Err(_) => false,
55 }
56}
57
58fn packet_trace(data: &[u8], is_write: bool) {
64 if !packet_trace_enabled() {
65 return;
66 }
67 let Some(mut sink) = packet_trace_sink() else {
68 return;
69 };
70 let rendered: Vec<u8> = if data.starts_with(b"PACK") || data.starts_with(b"\x01PACK") {
75 b"PACK ...".to_vec()
76 } else {
77 data.to_vec()
78 };
79
80 let mut out = format!(
81 "packet: {:>12}{} ",
82 packet_trace_prefix(),
83 if is_write { '>' } else { '<' }
84 );
85 for &byte in &rendered {
86 if byte == b'\n' {
87 continue;
88 }
89 if (0x20..=0x7e).contains(&byte) {
90 out.push(byte as char);
91 } else {
92 out.push_str(&format!("\\{byte:o}"));
93 }
94 }
95 out.push('\n');
96 let _ = sink.write_all(out.as_bytes());
97 let _ = sink.flush();
98}
99
100pub fn trace_packet_read_payload(payload: &[u8]) {
101 packet_trace(payload, false);
102}
103
104pub fn trace_packet_write_payload(payload: &[u8]) {
105 packet_trace(payload, true);
106}
107
108fn packet_trace_frame(frame: &PktLineFrame, is_write: bool) {
111 if !packet_trace_enabled() {
112 return;
113 }
114 match frame {
115 PktLineFrame::Data(payload) => packet_trace(payload, is_write),
116 PktLineFrame::Flush => packet_trace(b"0000", is_write),
117 PktLineFrame::Delimiter => packet_trace(b"0001", is_write),
118 PktLineFrame::ResponseEnd => packet_trace(b"0002", is_write),
119 }
120}
121
122pub const PKT_LINE_MAX_LEN: usize = 65_520;
123
124pub const PKT_LINE_MAX_PAYLOAD_LEN: usize = PKT_LINE_MAX_LEN - 4;
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub enum ProtocolVersion {
128 V0,
129 V1,
130 V2,
131}
132
133#[derive(Debug, Clone, PartialEq, Eq)]
134pub struct PktLine(pub Vec<u8>);
135
136impl PktLine {
137 pub fn encode(&self) -> Vec<u8> {
138 encode_pkt_line_payload(&self.0)
139 }
140
141 pub fn try_encode(&self) -> Result<Vec<u8>> {
142 validate_pkt_line_payload(&self.0)?;
143 Ok(self.encode())
144 }
145}
146
147#[derive(Debug, Clone, PartialEq, Eq)]
148pub enum PktLineFrame {
149 Data(Vec<u8>),
150 Flush,
151 Delimiter,
152 ResponseEnd,
153}
154
155#[derive(Debug, Clone, PartialEq, Eq)]
156pub struct ProtocolErrorLine {
157 pub message: String,
158}
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161pub enum GitService {
162 UploadPack,
163 ReceivePack,
164 UploadArchive,
165}
166
167impl GitService {
168 pub fn as_str(self) -> &'static str {
169 match self {
170 Self::UploadPack => "git-upload-pack",
171 Self::ReceivePack => "git-receive-pack",
172 Self::UploadArchive => "git-upload-archive",
173 }
174 }
175}
176
177#[derive(Debug, Clone, PartialEq, Eq)]
178pub struct RefSpec {
179 pub force: bool,
180 pub negative: bool,
181 pub src: Option<String>,
182 pub dst: Option<String>,
183 pub pattern: bool,
184}
185
186#[derive(Debug, Clone, PartialEq, Eq)]
187pub struct FetchHeadRecord {
188 pub oid: ObjectId,
189 pub not_for_merge: bool,
190 pub description: String,
191}
192
193#[derive(Debug, Clone, PartialEq, Eq)]
194pub struct FetchRefUpdate {
195 pub src: String,
196 pub dst: Option<String>,
197 pub oid: ObjectId,
198 pub not_for_merge: bool,
199 pub force: bool,
200}
201
202#[derive(Debug, Clone, PartialEq, Eq)]
203pub struct PushSourceRef {
204 pub name: String,
205 pub oid: ObjectId,
206}
207
208impl PktLineFrame {
209 pub fn data(payload: impl Into<Vec<u8>>) -> Result<Self> {
210 let payload = payload.into();
211 validate_pkt_line_payload(&payload)?;
212 Ok(Self::Data(payload))
213 }
214
215 pub fn encode(&self) -> Vec<u8> {
216 match self {
217 Self::Data(payload) => encode_pkt_line_payload(payload),
218 Self::Flush => b"0000".to_vec(),
219 Self::Delimiter => b"0001".to_vec(),
220 Self::ResponseEnd => b"0002".to_vec(),
221 }
222 }
223
224 pub fn try_encode(&self) -> Result<Vec<u8>> {
225 match self {
226 Self::Data(payload) => try_encode_pkt_line_payload(payload),
227 Self::Flush | Self::Delimiter | Self::ResponseEnd => Ok(self.encode()),
228 }
229 }
230
231 pub fn parse(input: &[u8]) -> Result<(Self, usize)> {
232 if input.len() < 4 {
233 return Err(GitError::InvalidFormat("truncated pkt-line length".into()));
234 }
235 let len = parse_pkt_len(&input[..4])?;
236 match len {
237 0 => Ok((Self::Flush, 4)),
238 1 => Ok((Self::Delimiter, 4)),
239 2 => Ok((Self::ResponseEnd, 4)),
240 3 => Err(GitError::InvalidFormat(
241 "reserved pkt-line length 0003".into(),
242 )),
243 4..=PKT_LINE_MAX_LEN => {
244 if input.len() < len {
245 return Err(GitError::InvalidFormat(format!(
246 "truncated pkt-line payload: expected {} bytes, got {}",
247 len - 4,
248 input.len().saturating_sub(4)
249 )));
250 }
251 Ok((Self::Data(input[4..len].to_vec()), len))
252 }
253 _ => Err(GitError::InvalidFormat(format!(
254 "pkt-line length exceeds {PKT_LINE_MAX_LEN}: {len}"
255 ))),
256 }
257 }
258}
259
260fn validate_pkt_line_payload(payload: &[u8]) -> Result<()> {
261 if payload.len() > PKT_LINE_MAX_PAYLOAD_LEN {
262 return Err(GitError::InvalidFormat(format!(
263 "pkt-line payload exceeds {PKT_LINE_MAX_PAYLOAD_LEN} bytes"
264 )));
265 }
266 Ok(())
267}
268
269pub(crate) fn pkt_line_header(len: usize) -> [u8; 4] {
270 const HEX: &[u8; 16] = b"0123456789abcdef";
271 [
272 HEX[(len >> 12) & 0xf],
273 HEX[(len >> 8) & 0xf],
274 HEX[(len >> 4) & 0xf],
275 HEX[len & 0xf],
276 ]
277}
278
279fn encode_pkt_line_payload(payload: &[u8]) -> Vec<u8> {
280 let len = payload.len() + 4;
281 let mut out = Vec::with_capacity(len);
282 out.extend_from_slice(&pkt_line_header(len));
283 out.extend_from_slice(payload);
284 out
285}
286
287fn try_encode_pkt_line_payload(payload: &[u8]) -> Result<Vec<u8>> {
288 validate_pkt_line_payload(payload)?;
289 Ok(encode_pkt_line_payload(payload))
290}
291
292pub fn parse_pkt_line_stream(mut input: &[u8]) -> Result<Vec<PktLineFrame>> {
293 let mut frames = Vec::new();
294 while !input.is_empty() {
295 let (frame, consumed) = PktLineFrame::parse(input)?;
296 frames.push(frame);
297 input = &input[consumed..];
298 }
299 Ok(frames)
300}
301
302pub(crate) fn parse_pkt_line_frames_until_flush_from(mut input: &[u8]) -> Result<(Vec<PktLineFrame>, usize)> {
303 let mut frames = Vec::new();
304 let mut total = 0usize;
305 loop {
306 if input.is_empty() {
307 return Err(GitError::InvalidFormat(
308 "pkt-line stream ended before flush".into(),
309 ));
310 }
311 let (frame, consumed) = PktLineFrame::parse(input)?;
312 total += consumed;
313 let done = matches!(frame, PktLineFrame::Flush);
314 frames.push(frame);
315 input = &input[consumed..];
316 if done {
317 return Ok((frames, total));
318 }
319 }
320}
321
322pub fn read_pkt_line_frame(reader: &mut impl Read) -> Result<Option<PktLineFrame>> {
323 let mut header = [0u8; 4];
324 let mut read = 0usize;
325 while read < header.len() {
326 match reader.read(&mut header[read..]) {
327 Ok(0) if read == 0 => return Ok(None),
328 Ok(0) => {
329 return Err(GitError::InvalidFormat("truncated pkt-line length".into()));
330 }
331 Ok(n) => read += n,
332 Err(err) if err.kind() == ErrorKind::Interrupted => {}
333 Err(err) => return Err(err.into()),
334 }
335 }
336
337 let len = parse_pkt_len(&header)?;
338 let frame = match len {
339 0 => PktLineFrame::Flush,
340 1 => PktLineFrame::Delimiter,
341 2 => PktLineFrame::ResponseEnd,
342 3 => {
343 return Err(GitError::InvalidFormat(
344 "reserved pkt-line length 0003".into(),
345 ));
346 }
347 4..=PKT_LINE_MAX_LEN => {
348 let mut payload = vec![0; len - 4];
349 reader.read_exact(&mut payload)?;
350 PktLineFrame::Data(payload)
351 }
352 _ => {
353 return Err(GitError::InvalidFormat(format!(
354 "pkt-line length exceeds {PKT_LINE_MAX_LEN}: {len}"
355 )));
356 }
357 };
358 packet_trace_frame(&frame, false);
359 Ok(Some(frame))
360}
361
362pub fn read_pkt_line_frames(reader: &mut impl Read) -> Result<Vec<PktLineFrame>> {
363 let mut frames = Vec::new();
364 while let Some(frame) = read_pkt_line_frame(reader)? {
365 frames.push(frame);
366 }
367 Ok(frames)
368}
369
370pub fn read_pkt_line_frames_until_flush(reader: &mut impl Read) -> Result<Vec<PktLineFrame>> {
371 read_pkt_line_frames_until_control(reader, |frame| matches!(frame, PktLineFrame::Flush))
372}
373
374pub fn read_pkt_line_frames_until_response_end(
375 reader: &mut impl Read,
376) -> Result<Vec<PktLineFrame>> {
377 read_pkt_line_frames_until_control(reader, |frame| matches!(frame, PktLineFrame::ResponseEnd))
378}
379
380fn read_pkt_line_frames_until_control(
381 reader: &mut impl Read,
382 stop: impl Fn(&PktLineFrame) -> bool,
383) -> Result<Vec<PktLineFrame>> {
384 let mut frames = Vec::new();
385 loop {
386 let Some(frame) = read_pkt_line_frame(reader)? else {
387 return Err(GitError::InvalidFormat(
388 "pkt-line stream ended before control packet".into(),
389 ));
390 };
391 let done = stop(&frame);
392 frames.push(frame);
393 if done {
394 return Ok(frames);
395 }
396 }
397}
398
399pub fn write_pkt_line_frame(writer: &mut impl Write, frame: &PktLineFrame) -> Result<()> {
400 match frame {
401 PktLineFrame::Data(payload) => write_pkt_line_payload(writer, payload)?,
403 PktLineFrame::Flush => {
404 packet_trace(b"0000", true);
405 writer.write_all(b"0000")?;
406 }
407 PktLineFrame::Delimiter => {
408 packet_trace(b"0001", true);
409 writer.write_all(b"0001")?;
410 }
411 PktLineFrame::ResponseEnd => {
412 packet_trace(b"0002", true);
413 writer.write_all(b"0002")?;
414 }
415 }
416 Ok(())
417}
418
419pub fn write_pkt_line_payload(writer: &mut impl Write, payload: &[u8]) -> Result<()> {
420 validate_pkt_line_payload(payload)?;
421 packet_trace(payload, true);
422 let len = payload.len() + 4;
423 writer.write_all(&pkt_line_header(len))?;
424 writer.write_all(payload)?;
425 Ok(())
426}
427
428pub fn write_pkt_line_frames(writer: &mut impl Write, frames: &[PktLineFrame]) -> Result<()> {
429 for frame in frames {
430 write_pkt_line_frame(writer, frame)?;
431 }
432 Ok(())
433}
434
435pub fn parse_error_line(payload: &[u8]) -> Result<ProtocolErrorLine> {
436 let text = parse_protocol_v2_line_text("protocol error line", payload)?;
437 let Some(message) = text.strip_prefix("ERR ") else {
438 return Err(GitError::InvalidFormat(
439 "protocol error line must start with ERR".into(),
440 ));
441 };
442 validate_protocol_error_message(message)?;
443 Ok(ProtocolErrorLine {
444 message: message.to_string(),
445 })
446}
447
448pub fn encode_error_line(error: &ProtocolErrorLine) -> Result<Vec<u8>> {
449 validate_protocol_error_message(&error.message)?;
450 Ok(line_from_str(&format!("ERR {}", error.message)))
451}
452
453pub fn parse_error_frame(frame: &PktLineFrame) -> Result<Option<ProtocolErrorLine>> {
454 match frame {
455 PktLineFrame::Data(payload) if trim_trailing_lf(payload).starts_with(b"ERR ") => {
456 parse_error_line(payload).map(Some)
457 }
458 PktLineFrame::Data(_)
459 | PktLineFrame::Flush
460 | PktLineFrame::Delimiter
461 | PktLineFrame::ResponseEnd => Ok(None),
462 }
463}
464
465pub fn read_error_line(reader: &mut impl Read) -> Result<ProtocolErrorLine> {
466 let Some(frame) = read_pkt_line_frame(reader)? else {
467 return Err(GitError::InvalidFormat(
468 "pkt-line stream ended before protocol error line".into(),
469 ));
470 };
471 match frame {
472 PktLineFrame::Data(payload) => parse_error_line(&payload),
473 _ => Err(GitError::InvalidFormat(
474 "protocol error line must be a data packet".into(),
475 )),
476 }
477}
478
479pub fn write_error_line(writer: &mut impl Write, error: &ProtocolErrorLine) -> Result<()> {
480 write_pkt_line_frame(writer, &PktLineFrame::data(encode_error_line(error)?)?)
481}
482
483pub fn parse_git_service(value: &str) -> Result<GitService> {
484 match value {
485 "git-upload-pack" => Ok(GitService::UploadPack),
486 "git-receive-pack" => Ok(GitService::ReceivePack),
487 "git-upload-archive" => Ok(GitService::UploadArchive),
488 other => Err(GitError::InvalidFormat(format!(
489 "unsupported git service {other}"
490 ))),
491 }
492}
493
494pub fn parse_refspec(value: &str) -> Result<RefSpec> {
495 validate_refspec_value(value)?;
496 let (force, value) = value
497 .strip_prefix('+')
498 .map_or((false, value), |value| (true, value));
499 let (negative, value) = value
500 .strip_prefix('^')
501 .map_or((false, value), |value| (true, value));
502 if force && negative {
503 return Err(GitError::InvalidFormat(
504 "negative refspec must not be forced".into(),
505 ));
506 }
507 let (src, dst) = if negative {
508 if value.contains(':') {
509 return Err(GitError::InvalidFormat(
510 "negative refspec must not have a destination".into(),
511 ));
512 }
513 (Some(value), None)
514 } else if let Some((src, dst)) = value.split_once(':') {
515 (non_empty(src), non_empty(dst))
516 } else {
517 (Some(value), None)
518 };
519 if src.is_none() && dst.is_none() && value != ":" {
520 return Err(GitError::InvalidFormat(
521 "refspec must include a source or destination".into(),
522 ));
523 }
524 if negative && src.is_none() {
525 return Err(GitError::InvalidFormat(
526 "negative refspec is missing a source".into(),
527 ));
528 }
529 if let Some(src) = src {
530 validate_refspec_endpoint("refspec source", src)?;
531 }
532 if let Some(dst) = dst {
533 validate_refspec_endpoint("refspec destination", dst)?;
534 }
535 let src_pattern_count = src.map(count_refspec_wildcards).unwrap_or(0);
536 let dst_pattern_count = dst.map(count_refspec_wildcards).unwrap_or(0);
537 if src_pattern_count > 1 || dst_pattern_count > 1 {
538 return Err(GitError::InvalidFormat(
539 "refspec endpoint has too many wildcards".into(),
540 ));
541 }
542 if dst.is_some() && (src_pattern_count == 1) != (dst_pattern_count == 1) {
543 return Err(GitError::InvalidFormat(
544 "refspec wildcard must appear in both source and destination".into(),
545 ));
546 }
547 Ok(RefSpec {
548 force,
549 negative,
550 src: src.map(str::to_string),
551 dst: dst.map(str::to_string),
552 pattern: src_pattern_count == 1 || dst_pattern_count == 1,
553 })
554}
555
556pub fn encode_refspec(refspec: &RefSpec) -> Result<String> {
557 validate_refspec_shape(refspec)?;
558 let mut out = String::new();
559 if refspec.force {
560 out.push('+');
561 }
562 if refspec.negative {
563 out.push('^');
564 }
565 if let Some(src) = &refspec.src {
566 out.push_str(src);
567 }
568 if !refspec.negative && refspec.src.is_none() && refspec.dst.is_none() {
569 out.push(':');
570 } else if !refspec.negative && refspec.dst.is_some() {
571 out.push(':');
572 if let Some(dst) = &refspec.dst {
573 out.push_str(dst);
574 }
575 }
576 Ok(out)
577}
578
579pub fn refspec_matches_source(refspec: &RefSpec, source: &str) -> Result<bool> {
580 Ok(refspec_map_source(refspec, source)?.is_some())
581}
582
583pub fn refspec_map_source(refspec: &RefSpec, source: &str) -> Result<Option<String>> {
584 validate_refspec_shape(refspec)?;
585 validate_refspec_endpoint("refspec match source", source)?;
586 let Some(src) = refspec.src.as_deref() else {
587 return Ok(None);
588 };
589 if refspec.pattern {
590 let Some((src_prefix, src_suffix)) = src.split_once('*') else {
591 return Ok(None);
592 };
593 let Some(middle) = source
594 .strip_prefix(src_prefix)
595 .and_then(|value| value.strip_suffix(src_suffix))
596 else {
597 return Ok(None);
598 };
599 if let Some(dst) = refspec.dst.as_deref() {
600 let (dst_prefix, dst_suffix) = dst.split_once('*').ok_or_else(|| {
601 GitError::InvalidFormat("pattern refspec destination is missing wildcard".into())
602 })?;
603 return Ok(Some(format!("{dst_prefix}{middle}{dst_suffix}")));
604 }
605 return Ok(Some(source.to_string()));
606 }
607 if src == source {
608 return Ok(Some(
609 refspec.dst.clone().unwrap_or_else(|| source.to_string()),
610 ));
611 }
612 Ok(None)
613}
614
615pub fn smart_http_info_refs_path(repository_path: &str, service: GitService) -> Result<String> {
616 validate_smart_http_service(service)?;
617 let repository_path = normalize_http_repository_path(repository_path)?;
618 Ok(format!(
619 "{repository_path}/info/refs?service={}",
620 service.as_str()
621 ))
622}
623
624pub fn smart_http_rpc_path(repository_path: &str, service: GitService) -> Result<String> {
625 validate_smart_http_service(service)?;
626 let repository_path = normalize_http_repository_path(repository_path)?;
627 Ok(format!("{repository_path}/{}", service.as_str()))
628}
629
630pub fn dumb_http_info_refs_path(repository_path: &str) -> Result<String> {
631 let repository_path = normalize_http_repository_path(repository_path)?;
632 Ok(format!("{repository_path}/info/refs"))
633}
634
635pub fn dumb_http_alternates_path(repository_path: &str) -> Result<String> {
636 let repository_path = normalize_http_repository_path(repository_path)?;
637 Ok(format!("{repository_path}/objects/info/http-alternates"))
638}
639
640pub fn dumb_http_packs_path(repository_path: &str) -> Result<String> {
641 let repository_path = normalize_http_repository_path(repository_path)?;
642 Ok(format!("{repository_path}/objects/info/packs"))
643}
644
645pub fn dumb_http_loose_object_path(repository_path: &str, oid: &ObjectId) -> Result<String> {
646 let repository_path = normalize_http_repository_path(repository_path)?;
647 let oid = oid.to_string();
648 let (directory, file) = oid.split_at(2);
649 Ok(format!("{repository_path}/objects/{directory}/{file}"))
650}
651
652pub fn dumb_http_pack_file_path(repository_path: &str, hash: &ObjectId) -> Result<String> {
653 dumb_http_pack_resource_path(repository_path, hash, "pack")
654}
655
656pub fn dumb_http_pack_index_path(repository_path: &str, hash: &ObjectId) -> Result<String> {
657 dumb_http_pack_resource_path(repository_path, hash, "idx")
658}
659
660pub fn smart_http_advertisement_content_type(service: GitService) -> Result<String> {
661 validate_smart_http_service(service)?;
662 Ok(format!("application/x-{}-advertisement", service.as_str()))
663}
664
665pub fn smart_http_rpc_request_content_type(service: GitService) -> Result<String> {
666 validate_smart_http_service(service)?;
667 Ok(format!("application/x-{}-request", service.as_str()))
668}
669
670pub fn smart_http_rpc_result_content_type(service: GitService) -> Result<String> {
671 validate_smart_http_service(service)?;
672 Ok(format!("application/x-{}-result", service.as_str()))
673}
674
675pub fn parse_smart_http_advertisement_content_type(value: &str) -> Result<GitService> {
676 parse_smart_http_content_type(value, "-advertisement")
677}
678
679pub fn parse_smart_http_rpc_request_content_type(value: &str) -> Result<GitService> {
680 parse_smart_http_content_type(value, "-request")
681}
682
683pub fn parse_smart_http_rpc_result_content_type(value: &str) -> Result<GitService> {
684 parse_smart_http_content_type(value, "-result")
685}
686pub(crate) fn parse_pkt_len(bytes: &[u8]) -> Result<usize> {
687 let mut len = 0usize;
688 for byte in bytes {
689 len = (len << 4) | hex_nibble(*byte)? as usize;
690 }
691 Ok(len)
692}
693
694fn hex_nibble(byte: u8) -> Result<u8> {
695 match byte {
696 b'0'..=b'9' => Ok(byte - b'0'),
697 b'a'..=b'f' => Ok(byte - b'a' + 10),
698 b'A'..=b'F' => Ok(byte - b'A' + 10),
699 _ => Err(GitError::InvalidFormat(format!(
700 "invalid pkt-line length byte {byte:#04x}"
701 ))),
702 }
703}
704fn validate_protocol_error_message(message: &str) -> Result<()> {
705 if message.is_empty() {
706 return Err(GitError::InvalidFormat(
707 "protocol error message is empty".into(),
708 ));
709 }
710 if message
711 .bytes()
712 .any(|byte| matches!(byte, b'\n' | b'\r' | 0))
713 {
714 return Err(GitError::InvalidFormat(
715 "protocol error message contains a delimiter byte".into(),
716 ));
717 }
718 Ok(())
719}
720
721pub(crate) fn validate_capability_field(label: &str, value: &str) -> Result<()> {
722 if value.is_empty() {
723 return Err(GitError::InvalidFormat(format!("{label} is empty")));
724 }
725 if value
726 .bytes()
727 .any(|byte| matches!(byte, b' ' | b'\n' | b'\r' | b'\t' | 0))
728 {
729 return Err(GitError::InvalidFormat(format!(
730 "{label} contains a delimiter byte"
731 )));
732 }
733 Ok(())
734}
735
736pub(crate) fn validate_capability_name(value: &str) -> Result<()> {
737 validate_capability_field("capability name", value)?;
738 if value.bytes().any(|byte| byte == b'=') {
739 return Err(GitError::InvalidFormat(
740 "capability name contains a delimiter byte".into(),
741 ));
742 }
743 Ok(())
744}
745
746pub(crate) fn non_empty(value: &str) -> Option<&str> {
747 (!value.is_empty()).then_some(value)
748}
749
750fn validate_refspec_value(value: &str) -> Result<()> {
751 if value.is_empty() {
752 return Err(GitError::InvalidFormat("refspec is empty".into()));
753 }
754 if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
755 return Err(GitError::InvalidFormat(
756 "refspec contains a delimiter byte".into(),
757 ));
758 }
759 Ok(())
760}
761
762pub(crate) fn validate_refspec_endpoint(label: &str, value: &str) -> Result<()> {
763 if value.is_empty() {
764 return Err(GitError::InvalidFormat(format!("{label} is empty")));
765 }
766 if value
767 .bytes()
768 .any(|byte| matches!(byte, b':' | b' ' | b'\t' | b'\n' | b'\r' | 0))
769 {
770 return Err(GitError::InvalidFormat(format!(
771 "{label} contains a delimiter byte"
772 )));
773 }
774 Ok(())
775}
776
777fn count_refspec_wildcards(value: &str) -> usize {
778 value.bytes().filter(|byte| *byte == b'*').count()
779}
780
781pub(crate) fn validate_refspec_shape(refspec: &RefSpec) -> Result<()> {
782 if refspec.force && refspec.negative {
783 return Err(GitError::InvalidFormat(
784 "negative refspec must not be forced".into(),
785 ));
786 }
787 if refspec.negative && refspec.dst.is_some() {
788 return Err(GitError::InvalidFormat(
789 "negative refspec must not have a destination".into(),
790 ));
791 }
792 if refspec.negative && refspec.src.is_none() {
793 return Err(GitError::InvalidFormat(
794 "negative refspec is missing a source".into(),
795 ));
796 }
797 if refspec.src.is_none() && refspec.dst.is_none() && refspec.negative {
798 return Err(GitError::InvalidFormat(
799 "refspec must include a source or destination".into(),
800 ));
801 }
802 if let Some(src) = &refspec.src {
803 validate_refspec_endpoint("refspec source", src)?;
804 }
805 if let Some(dst) = &refspec.dst {
806 validate_refspec_endpoint("refspec destination", dst)?;
807 }
808 let src_pattern_count = refspec
809 .src
810 .as_deref()
811 .map(count_refspec_wildcards)
812 .unwrap_or(0);
813 let dst_pattern_count = refspec
814 .dst
815 .as_deref()
816 .map(count_refspec_wildcards)
817 .unwrap_or(0);
818 if src_pattern_count > 1 || dst_pattern_count > 1 {
819 return Err(GitError::InvalidFormat(
820 "refspec endpoint has too many wildcards".into(),
821 ));
822 }
823 if refspec.dst.is_some() && (src_pattern_count == 1) != (dst_pattern_count == 1) {
824 return Err(GitError::InvalidFormat(
825 "refspec wildcard must appear in both source and destination".into(),
826 ));
827 }
828 if refspec.pattern != (src_pattern_count == 1 || dst_pattern_count == 1) {
829 return Err(GitError::InvalidFormat(
830 "refspec pattern flag does not match endpoints".into(),
831 ));
832 }
833 Ok(())
834}
835
836pub(crate) fn validate_fetch_head_line(value: &[u8]) -> Result<()> {
837 if value.is_empty() {
838 return Err(GitError::InvalidFormat("FETCH_HEAD record is empty".into()));
839 }
840 if !value.ends_with(b"\n") {
841 return Err(GitError::InvalidFormat(
842 "FETCH_HEAD record missing LF".into(),
843 ));
844 }
845 if value.iter().any(|byte| matches!(*byte, b'\r' | 0)) {
846 return Err(GitError::InvalidFormat(
847 "FETCH_HEAD record contains a delimiter byte".into(),
848 ));
849 }
850 Ok(())
851}
852
853pub(crate) fn validate_fetch_head_description_field(value: &str) -> Result<()> {
854 if value.is_empty() {
855 return Err(GitError::InvalidFormat(
856 "FETCH_HEAD description is empty".into(),
857 ));
858 }
859 if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
860 return Err(GitError::InvalidFormat(
861 "FETCH_HEAD description contains a delimiter byte".into(),
862 ));
863 }
864 Ok(())
865}
866fn validate_smart_http_service(service: GitService) -> Result<()> {
867 match service {
868 GitService::UploadPack | GitService::ReceivePack => Ok(()),
869 GitService::UploadArchive => Err(GitError::InvalidFormat(
870 "smart HTTP only supports upload-pack and receive-pack services".into(),
871 )),
872 }
873}
874
875fn normalize_http_repository_path(path: &str) -> Result<String> {
876 if path.is_empty() {
877 return Err(GitError::InvalidFormat(
878 "smart HTTP repository path is empty".into(),
879 ));
880 }
881 if !path.starts_with('/') {
882 return Err(GitError::InvalidFormat(
883 "smart HTTP repository path must start with /".into(),
884 ));
885 }
886 if path
887 .bytes()
888 .any(|byte| matches!(byte, b'\n' | b'\r' | 0 | b'?' | b'#'))
889 {
890 return Err(GitError::InvalidFormat(
891 "smart HTTP repository path contains a delimiter byte".into(),
892 ));
893 }
894 let normalized = path.trim_end_matches('/');
895 Ok(if normalized.is_empty() {
896 "/".into()
897 } else {
898 normalized.to_string()
899 })
900}
901
902fn dumb_http_pack_resource_path(
903 repository_path: &str,
904 hash: &ObjectId,
905 suffix: &str,
906) -> Result<String> {
907 let repository_path = normalize_http_repository_path(repository_path)?;
908 Ok(format!(
909 "{repository_path}/objects/pack/pack-{hash}.{suffix}"
910 ))
911}
912
913fn parse_smart_http_content_type(value: &str, suffix: &str) -> Result<GitService> {
914 let value = value.trim();
915 if value.is_empty() {
916 return Err(GitError::InvalidFormat(
917 "smart HTTP content type is empty".into(),
918 ));
919 }
920 if value.bytes().any(|byte| matches!(byte, b'\n' | b'\r' | 0)) {
921 return Err(GitError::InvalidFormat(
922 "smart HTTP content type contains a delimiter byte".into(),
923 ));
924 }
925 let value = value.to_ascii_lowercase();
926 let service = value
927 .strip_prefix("application/x-")
928 .and_then(|value| value.strip_suffix(suffix))
929 .ok_or_else(|| GitError::InvalidFormat("invalid smart HTTP content type".into()))?;
930 let service = parse_git_service(service)?;
931 validate_smart_http_service(service)?;
932 Ok(service)
933}
934
935pub(crate) fn validate_protocol_v2_token(label: &str, value: &str) -> Result<()> {
936 if value.is_empty() {
937 return Err(GitError::InvalidFormat(format!("{label} is empty")));
938 }
939 if value
940 .bytes()
941 .any(|byte| matches!(byte, b' ' | b'\n' | b'\r' | 0))
942 {
943 return Err(GitError::InvalidFormat(format!(
944 "{label} contains a delimiter byte"
945 )));
946 }
947 Ok(())
948}
949
950pub(crate) fn validate_protocol_v2_line(label: &str, value: &[u8]) -> Result<()> {
951 if value.is_empty() {
952 return Err(GitError::InvalidFormat(format!("{label} is empty")));
953 }
954 if value.iter().any(|byte| matches!(*byte, b'\r' | 0)) {
955 return Err(GitError::InvalidFormat(format!(
956 "{label} contains a delimiter byte"
957 )));
958 }
959 Ok(())
960}
961
962pub(crate) fn parse_protocol_v2_line_text<'a>(label: &str, value: &'a [u8]) -> Result<&'a str> {
963 validate_protocol_v2_line(label, value)?;
964 let value = trim_trailing_lf(value);
965 if value.is_empty() {
966 return Err(GitError::InvalidFormat(format!("{label} is empty")));
967 }
968 if value.iter().any(|byte| matches!(*byte, b'\n' | b'\r' | 0)) {
969 return Err(GitError::InvalidFormat(format!(
970 "{label} contains a delimiter byte"
971 )));
972 }
973 std::str::from_utf8(value).map_err(|err| GitError::InvalidFormat(err.to_string()))
974}
975
976pub(crate) fn parse_oid_argument(
977 format: ObjectFormat,
978 label: &str,
979 value: &str,
980 prefix: &str,
981) -> Result<ObjectId> {
982 let oid = value
983 .strip_prefix(prefix)
984 .ok_or_else(|| GitError::InvalidFormat(format!("invalid {label}")))?;
985 validate_protocol_v2_token(label, oid)?;
986 ObjectId::from_hex(format, oid)
987}
988
989pub(crate) fn line(mut payload: Vec<u8>) -> Vec<u8> {
990 payload.push(b'\n');
991 payload
992}
993
994pub(crate) fn line_from_str(payload: &str) -> Vec<u8> {
995 line(payload.as_bytes().to_vec())
996}
997
998pub(crate) fn trim_trailing_lf(input: &[u8]) -> &[u8] {
999 input.strip_suffix(b"\n").unwrap_or(input)
1000}