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