anvilsign in

collin/anvil

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4//
5// Copyright (c) 2026 WJQSERVER
6
7use std::collections::BTreeSet;
8use std::collections::HashSet;
9use std::io::Cursor;
10use std::path::Path;
11
12use crate::error::{
13 Error,
14 Result,
15};
16use crate::pack::UploadPackRequest;
17use crate::pktline;
18
19const CAPABILITIES: &[&str] = &[
20 "ls-refs=unborn",
21 "fetch=shallow wait-for-done",
22 "object-format=sha1",
23];
24
25pub enum Command {
26 LsRefs(LsRefsRequest),
27 Fetch(FetchRequest),
28}
29
30#[derive(Default)]
31pub struct LsRefsRequest {
32 pub peel: bool,
33 pub symrefs: bool,
34 pub unborn: bool,
35 pub ref_prefixes: Vec<String>,
36}
37
38pub struct FetchRequest {
39 pub upload_request: UploadPackRequest,
40}
41
42pub struct ShallowUpdate {
43 pub shallow: Vec<gix::ObjectId>,
44 pub unshallow: Vec<gix::ObjectId>,
45}
46
47pub fn advertise_capabilities() -> Vec<u8> {
48 let mut out = Vec::new();
49 out.extend_from_slice(&pktline::encode_comment("service=git-upload-pack"));
50 out.extend_from_slice(pktline::flush());
51 out.extend_from_slice(&pktline::encode(b"version 2\n"));
52 for capability in CAPABILITIES {
53 out.extend_from_slice(&pktline::encode(format!("{capability}\n").as_bytes()));
54 }
55 out.extend_from_slice(pktline::flush());
56 out
57}
58
59pub fn parse_command_request(body: &[u8]) -> Result<Command> {
60 let lines = decode_pkt_lines(body)?;
61 let mut iter = lines.into_iter();
62
63 let command = iter
64 .next()
65 .ok_or_else(|| Error::Protocol("missing protocol v2 command".into()))?;
66 let command = command
67 .strip_prefix("command=")
68 .ok_or_else(|| Error::Protocol("invalid protocol v2 command line".into()))?;
69
70 let mut args = Vec::new();
71 let mut saw_delim = false;
72 for line in iter {
73 if line.is_empty() {
74 continue;
75 }
76 if line == "0001" {
77 saw_delim = true;
78 continue;
79 }
80 if saw_delim {
81 args.push(line);
82 }
83 }
84
85 match command {
86 "ls-refs" => parse_ls_refs(args),
87 "fetch" => parse_fetch(args),
88 _ => Err(Error::Protocol(format!(
89 "unsupported protocol v2 command: {command}"
90 ))),
91 }
92}
93
94pub fn ls_refs(repo_path: &Path, request: &LsRefsRequest) -> Result<Vec<u8>> {
95 let repo = gix::open(repo_path)?;
96 let mut refs = BTreeSet::new();
97
98 if let Ok(mut head) = repo.head() {
99 if let Some(id) = head
100 .try_peel_to_id()
101 .map_err(|e| Error::Protocol(e.to_string()))?
102 {
103 let mut line = format!("{} HEAD", id.detach());
104 if request.symrefs
105 && let Some(target) = head.referent_name()
106 {
107 line.push_str(&format!(" symref-target:{}", target.as_bstr()));
108 }
109 refs.insert(line);
110 } else if request.unborn
111 && let Some(target) = head.referent_name()
112 {
113 refs.insert(format!("unborn HEAD symref-target:{}", target.as_bstr()));
114 }
115 }
116
117 if let Ok(platform) = repo.references()
118 && let Ok(iter) = platform.all()
119 {
120 for mut reference in iter.flatten() {
121 let name = reference.name().as_bstr().to_string();
122 if !request.ref_prefixes.is_empty()
123 && !request
124 .ref_prefixes
125 .iter()
126 .any(|prefix| name.starts_with(prefix))
127 {
128 continue;
129 }
130
131 let mut line = match reference.try_id() {
132 Some(id) => format!("{} {name}", id.detach()),
133 None => match reference.peel_to_id() {
134 Ok(id) => format!("{} {name}", id.detach()),
135 Err(_) => continue,
136 },
137 };
138
139 if request.symrefs
140 && let Some(target) = reference.target().try_name()
141 {
142 line.push_str(&format!(" symref-target:{}", target.as_bstr()));
143 }
144
145 if request.peel
146 && let Ok(peeled) = reference.peel_to_id()
147 {
148 line.push_str(&format!(" peeled:{}", peeled.detach()));
149 }
150
151 refs.insert(line);
152 }
153 }
154
155 let mut out = Vec::new();
156 for line in refs {
157 out.extend_from_slice(&pktline::encode(format!("{line}\n").as_bytes()));
158 }
159 out.extend_from_slice(pktline::flush());
160 Ok(out)
161}
162
163pub fn encode_fetch_pack_response(pack_bytes: &[u8]) -> Vec<u8> {
164 let mut out = Vec::new();
165 out.extend_from_slice(&pktline::encode(b"packfile\n"));
166
167 let mut pos = 0;
168 while pos + 4 <= pack_bytes.len() {
169 let len_str = match std::str::from_utf8(&pack_bytes[pos..pos + 4]) {
170 Ok(v) => v,
171 Err(_) => break,
172 };
173 pos += 4;
174
175 if len_str == "0000" {
176 out.extend_from_slice(b"0000");
177 break;
178 }
179
180 let len = match usize::from_str_radix(len_str, 16) {
181 Ok(v) if v >= 4 && pos + (v - 4) <= pack_bytes.len() => v,
182 _ => break,
183 };
184
185 let frame = &pack_bytes[pos - 4..pos + (len - 4)];
186 let payload = &pack_bytes[pos..pos + (len - 4)];
187 pos += len - 4;
188
189 if payload.starts_with(&[0x01])
190 || payload.starts_with(&[0x02])
191 || payload.starts_with(&[0x03])
192 {
193 out.extend_from_slice(frame);
194 }
195 }
196
197 out
198}
199
200pub struct PrefixThenReader<R> {
201 prefix: Cursor<Vec<u8>>,
202 reader: R,
203}
204
205impl<R> PrefixThenReader<R> {
206 pub fn new(prefix: Vec<u8>, reader: R) -> Self {
207 Self {
208 prefix: Cursor::new(prefix),
209 reader,
210 }
211 }
212}
213
214pub struct PackSectionReader<R> {
215 reader: R,
216 buf: Vec<u8>,
217 out: Cursor<Vec<u8>>,
218 finished: bool,
219}
220
221impl<R> PackSectionReader<R> {
222 pub fn new(reader: R) -> Self {
223 Self {
224 reader,
225 buf: Vec::new(),
226 out: Cursor::new(Vec::new()),
227 finished: false,
228 }
229 }
230}
231
232impl<R: tokio::io::AsyncRead + Unpin> tokio::io::AsyncRead for PackSectionReader<R> {
233 fn poll_read(
234 mut self: std::pin::Pin<&mut Self>,
235 cx: &mut std::task::Context<'_>,
236 buf: &mut tokio::io::ReadBuf<'_>,
237 ) -> std::task::Poll<std::io::Result<()>> {
238 loop {
239 if (self.out.position() as usize) < self.out.get_ref().len() {
240 let remaining = &self.out.get_ref()[self.out.position() as usize..];
241 let to_copy = remaining.len().min(buf.remaining());
242 buf.put_slice(&remaining[..to_copy]);
243 let next = self.out.position() + to_copy as u64;
244 self.out.set_position(next);
245 return std::task::Poll::Ready(Ok(()));
246 }
247
248 if self.finished {
249 return std::task::Poll::Ready(Ok(()));
250 }
251
252 let mut frame_buf = [0u8; 8192];
253 let mut read_buf = tokio::io::ReadBuf::new(&mut frame_buf);
254 match std::pin::Pin::new(&mut self.reader).poll_read(cx, &mut read_buf) {
255 std::task::Poll::Pending => return std::task::Poll::Pending,
256 std::task::Poll::Ready(Err(err)) => return std::task::Poll::Ready(Err(err)),
257 std::task::Poll::Ready(Ok(())) => {
258 let filled = read_buf.filled();
259 if filled.is_empty() {
260 self.finished = true;
261 return std::task::Poll::Ready(Ok(()));
262 }
263 self.buf.extend_from_slice(filled);
264 }
265 }
266
267 let mut emitted = Vec::new();
268 loop {
269 if self.buf.len() < 4 {
270 break;
271 }
272 let len_str = match std::str::from_utf8(&self.buf[..4]) {
273 Ok(v) => v,
274 Err(_) => {
275 self.finished = true;
276 return std::task::Poll::Ready(Err(std::io::Error::other(
277 "invalid pkt-line prefix in pack response",
278 )));
279 }
280 };
281
282 if len_str == "0000" {
283 emitted.extend_from_slice(b"0000");
284 self.buf.drain(..4);
285 self.finished = true;
286 break;
287 }
288
289 let len = match usize::from_str_radix(len_str, 16) {
290 Ok(v) if v >= 4 => v,
291 _ => {
292 self.finished = true;
293 return std::task::Poll::Ready(Err(std::io::Error::other(
294 "invalid pkt-line length in pack response",
295 )));
296 }
297 };
298
299 if self.buf.len() < len {
300 break;
301 }
302
303 let frame = self.buf[..len].to_vec();
304 let payload = &frame[4..];
305 if payload.starts_with(&[0x01])
306 || payload.starts_with(&[0x02])
307 || payload.starts_with(&[0x03])
308 {
309 emitted.extend_from_slice(&frame);
310 }
311 self.buf.drain(..len);
312 }
313
314 if !emitted.is_empty() {
315 self.out = Cursor::new(emitted);
316 self.out.set_position(0);
317 }
318 }
319 }
320}
321
322impl<R: tokio::io::AsyncRead + Unpin> tokio::io::AsyncRead for PrefixThenReader<R> {
323 fn poll_read(
324 mut self: std::pin::Pin<&mut Self>,
325 cx: &mut std::task::Context<'_>,
326 buf: &mut tokio::io::ReadBuf<'_>,
327 ) -> std::task::Poll<std::io::Result<()>> {
328 if (self.prefix.position() as usize) < self.prefix.get_ref().len() {
329 let remaining = &self.prefix.get_ref()[self.prefix.position() as usize..];
330 let to_copy = remaining.len().min(buf.remaining());
331 buf.put_slice(&remaining[..to_copy]);
332 let next = self.prefix.position() + to_copy as u64;
333 self.prefix.set_position(next);
334 return std::task::Poll::Ready(Ok(()));
335 }
336
337 std::pin::Pin::new(&mut self.reader).poll_read(cx, buf)
338 }
339}
340
341pub fn encode_fetch_ready_and_acknowledgments(common: &[gix::ObjectId]) -> Vec<u8> {
342 let mut out = encode_fetch_acknowledgments(common);
343 if !common.is_empty() {
344 out.truncate(out.len() - 4);
345 out.extend_from_slice(&pktline::encode(b"ready\n"));
346 out.extend_from_slice(b"0001");
347 }
348 out
349}
350
351pub fn encode_fetch_acknowledgments(common: &[gix::ObjectId]) -> Vec<u8> {
352 let mut out = Vec::new();
353 out.extend_from_slice(&pktline::encode(b"acknowledgments\n"));
354
355 if common.is_empty() {
356 out.extend_from_slice(&pktline::encode(b"NAK\n"));
357 } else {
358 for oid in common {
359 out.extend_from_slice(&pktline::encode(format!("ACK {oid}\n").as_bytes()));
360 }
361 }
362
363 out.extend_from_slice(pktline::flush());
364 out
365}
366
367pub fn encode_shallow_info(update: &ShallowUpdate) -> Vec<u8> {
368 let mut out = Vec::new();
369 if update.shallow.is_empty() && update.unshallow.is_empty() {
370 return out;
371 }
372
373 out.extend_from_slice(&pktline::encode(b"shallow-info\n"));
374 for oid in &update.shallow {
375 out.extend_from_slice(&pktline::encode(format!("shallow {oid}\n").as_bytes()));
376 }
377 for oid in &update.unshallow {
378 out.extend_from_slice(&pktline::encode(format!("unshallow {oid}\n").as_bytes()));
379 }
380 out.extend_from_slice(b"0001");
381 out
382}
383
384pub fn common_haves(repo_path: &Path, request: &FetchRequest) -> Result<Vec<gix::ObjectId>> {
385 let repo = gix::open(repo_path)?;
386 let want_set: HashSet<gix::ObjectId> =
387 collect_want_closure(&repo, &request.upload_request.wants)?
388 .into_iter()
389 .collect();
390
391 Ok(request
392 .upload_request
393 .haves
394 .iter()
395 .copied()
396 .filter(|oid| want_set.contains(oid))
397 .collect())
398}
399
400pub fn apply_shallow_boundaries(
401 repo_path: &Path,
402 request: &mut FetchRequest,
403) -> Result<ShallowUpdate> {
404 let Some(depth) = request.upload_request.shallow.depth else {
405 return Ok(ShallowUpdate {
406 shallow: Vec::new(),
407 unshallow: Vec::new(),
408 });
409 };
410
411 let repo = gix::open(repo_path)?;
412 let previous_shallows = request.upload_request.shallow.client_shallows.clone();
413 let state = collect_depth_limited_commits(&repo, &request.upload_request, depth)?;
414
415 request.upload_request.object_ids = Some(state.included_objects.clone());
416 request
417 .upload_request
418 .haves
419 .extend(previous_shallows.iter().copied());
420
421 let next_shallows: HashSet<_> = state.shallow_boundary.iter().copied().collect();
422 let prev_shallows: HashSet<_> = previous_shallows.iter().copied().collect();
423
424 Ok(ShallowUpdate {
425 shallow: state
426 .shallow_boundary
427 .iter()
428 .copied()
429 .filter(|oid| !prev_shallows.contains(oid))
430 .collect(),
431 unshallow: previous_shallows
432 .into_iter()
433 .filter(|oid| !next_shallows.contains(oid))
434 .collect(),
435 })
436}
437
438struct DepthState {
439 included_objects: Vec<gix::ObjectId>,
440 shallow_boundary: Vec<gix::ObjectId>,
441}
442
443fn parse_ls_refs(args: Vec<String>) -> Result<Command> {
444 let mut request = LsRefsRequest::default();
445
446 for arg in args {
447 match arg.as_str() {
448 "peel" => request.peel = true,
449 "symrefs" => request.symrefs = true,
450 "unborn" => request.unborn = true,
451 _ => {
452 if let Some(prefix) = arg.strip_prefix("ref-prefix ") {
453 request.ref_prefixes.push(prefix.to_owned());
454 } else {
455 return Err(Error::Protocol(format!(
456 "unsupported ls-refs argument: {arg}"
457 )));
458 }
459 }
460 }
461 }
462
463 Ok(Command::LsRefs(request))
464}
465
466fn parse_fetch(args: Vec<String>) -> Result<Command> {
467 let mut wants = Vec::new();
468 let mut haves = Vec::new();
469 let mut done = false;
470 let mut capabilities = crate::pack::UploadPackCapabilities::default();
471 let mut shallow = crate::pack::ShallowRequest::default();
472
473 for arg in args {
474 if arg == "done" {
475 done = true;
476 } else if arg == "ofs-delta" {
477 capabilities.ofs_delta = true;
478 } else if arg == "deepen-relative" {
479 shallow.deepen_relative = true;
480 } else if arg == "thin-pack"
481 || arg == "no-progress"
482 || arg == "include-tag"
483 || arg == "wait-for-done"
484 {
485 continue;
486 } else if let Some(depth) = arg.strip_prefix("deepen ") {
487 shallow.depth = Some(
488 depth
489 .parse::<usize>()
490 .map_err(|_| Error::Protocol(format!("invalid deepen value: {depth}")))?,
491 );
492 } else if let Some(oid_hex) = arg.strip_prefix("shallow ") {
493 let oid = gix::ObjectId::from_hex(oid_hex.as_bytes())
494 .map_err(|_| Error::Protocol(format!("invalid OID in shallow: {oid_hex}")))?;
495 shallow.client_shallows.push(oid);
496 } else if let Some(oid_hex) = arg.strip_prefix("want ") {
497 let oid = gix::ObjectId::from_hex(oid_hex.as_bytes())
498 .map_err(|_| Error::Protocol(format!("invalid OID in want: {oid_hex}")))?;
499 wants.push(oid);
500 } else if let Some(oid_hex) = arg.strip_prefix("have ") {
501 let oid = gix::ObjectId::from_hex(oid_hex.as_bytes())
502 .map_err(|_| Error::Protocol(format!("invalid OID in have: {oid_hex}")))?;
503 haves.push(oid);
504 } else {
505 return Err(Error::Protocol(format!(
506 "unsupported fetch argument: {arg}"
507 )));
508 }
509 }
510
511 Ok(Command::Fetch(FetchRequest {
512 upload_request: UploadPackRequest {
513 wants,
514 haves,
515 done,
516 capabilities,
517 shallow,
518 object_ids: None,
519 },
520 }))
521}
522
523fn decode_pkt_lines(body: &[u8]) -> Result<Vec<String>> {
524 let mut pos = 0;
525 let mut out = Vec::new();
526
527 while pos < body.len() {
528 if pos + 4 > body.len() {
529 return Err(Error::Protocol("truncated pkt-line prefix".into()));
530 }
531
532 let len_str = std::str::from_utf8(&body[pos..pos + 4])
533 .map_err(|_| Error::Protocol("invalid pkt-line length prefix".into()))?;
534 pos += 4;
535
536 if len_str == "0000" {
537 break;
538 }
539 if len_str == "0001" {
540 out.push("0001".to_string());
541 continue;
542 }
543
544 let len = usize::from_str_radix(len_str, 16)
545 .map_err(|_| Error::Protocol("invalid pkt-line length".into()))?;
546 if len < 4 || pos + (len - 4) > body.len() {
547 return Err(Error::Protocol("invalid pkt-line frame length".into()));
548 }
549
550 let payload = &body[pos..pos + (len - 4)];
551 pos += len - 4;
552 let line = std::str::from_utf8(payload)
553 .map_err(|_| Error::Protocol("invalid UTF-8 in pkt-line".into()))?;
554 out.push(line.trim_end_matches('\n').to_owned());
555 }
556
557 Ok(out)
558}
559
560fn collect_want_closure(
561 repo: &gix::Repository,
562 wants: &[gix::ObjectId],
563) -> Result<Vec<gix::ObjectId>> {
564 let mut seen = HashSet::new();
565 let mut out = Vec::new();
566
567 let walk = repo
568 .rev_walk(wants.iter().copied())
569 .all()
570 .map_err(|e| Error::Protocol(e.to_string()))?;
571 for info_result in walk {
572 let info = info_result.map_err(|e| Error::Protocol(e.to_string()))?;
573 let commit_oid = info.id;
574 if !seen.insert(commit_oid) {
575 continue;
576 }
577 out.push(commit_oid);
578 }
579
580 Ok(out)
581}
582
583fn collect_depth_limited_commits(
584 repo: &gix::Repository,
585 request: &crate::pack::UploadPackRequest,
586 depth: usize,
587) -> Result<DepthState> {
588 use std::collections::{
589 HashSet,
590 VecDeque,
591 };
592
593 let mut queue = VecDeque::new();
594 let mut seen = HashSet::new();
595 let mut included_commits = Vec::new();
596 let mut included_objects = Vec::new();
597 let mut shallow_boundary = Vec::new();
598
599 let base_depth = if request.shallow.deepen_relative {
600 1usize
601 } else {
602 0
603 };
604 let limit = base_depth + depth;
605
606 for want in &request.wants {
607 queue.push_back((*want, 1usize));
608 }
609
610 while let Some((commit_oid, current_depth)) = queue.pop_front() {
611 if !seen.insert(commit_oid) {
612 continue;
613 }
614 included_commits.push(commit_oid);
615 included_objects.push(commit_oid);
616
617 let commit_obj = repo
618 .find_object(commit_oid)
619 .map_err(|e| Error::Protocol(e.to_string()))?;
620 let tree_oid =
621 gix::objs::CommitRefIter::from_bytes(&commit_obj.data, gix::hash::Kind::Sha1)
622 .tree_id()
623 .map_err(|e| Error::Protocol(e.to_string()))?;
624 collect_tree_oids(repo, tree_oid, &mut seen, &mut included_objects)?;
625 let parents: Vec<_> =
626 gix::objs::CommitRefIter::from_bytes(&commit_obj.data, gix::hash::Kind::Sha1)
627 .parent_ids()
628 .collect();
629
630 if current_depth >= limit || parents.is_empty() {
631 shallow_boundary.push(commit_oid);
632 continue;
633 }
634
635 for parent in parents {
636 queue.push_back((parent, current_depth + 1));
637 }
638 }
639
640 Ok(DepthState {
641 included_objects,
642 shallow_boundary,
643 })
644}
645
646fn collect_tree_oids(
647 repo: &gix::Repository,
648 root_tree_oid: gix::ObjectId,
649 seen: &mut HashSet<gix::ObjectId>,
650 oids: &mut Vec<gix::ObjectId>,
651) -> Result<()> {
652 let mut stack = vec![root_tree_oid];
653
654 while let Some(tree_oid) = stack.pop() {
655 if !seen.insert(tree_oid) {
656 continue;
657 }
658
659 let tree_obj = repo
660 .find_object(tree_oid)
661 .map_err(|e| Error::Protocol(e.to_string()))?;
662 oids.push(tree_oid);
663
664 for entry_result in
665 gix::objs::TreeRefIter::from_bytes(&tree_obj.data, gix::hash::Kind::Sha1)
666 {
667 let entry = entry_result.map_err(|e| Error::Protocol(e.to_string()))?;
668 let entry_oid = entry.oid.to_owned();
669 let entry_mode = entry.mode;
670
671 if entry_mode.is_tree() {
672 stack.push(entry_oid);
673 } else if seen.insert(entry_oid) && !entry_mode.is_commit() {
674 oids.push(entry_oid);
675 }
676 }
677 }
678
679 Ok(())
680}
681
682#[cfg(test)]
683mod tests {
684 use super::*;
685
686 fn pkt(data: &str) -> Vec<u8> {
687 pktline::encode(data.as_bytes())
688 }
689
690 #[test]
691 fn parse_ls_refs_command() {
692 let mut body = Vec::new();
693 body.extend_from_slice(&pkt("command=ls-refs\n"));
694 body.extend_from_slice(b"0001");
695 body.extend_from_slice(&pkt("peel\n"));
696 body.extend_from_slice(&pkt("symrefs\n"));
697 body.extend_from_slice(&pkt("ref-prefix refs/heads/\n"));
698 body.extend_from_slice(b"0000");
699
700 let Command::LsRefs(req) = parse_command_request(&body).unwrap() else {
701 panic!("expected ls-refs command");
702 };
703 assert!(req.peel);
704 assert!(req.symrefs);
705 assert_eq!(req.ref_prefixes, vec!["refs/heads/"]);
706 }
707
708 #[test]
709 fn parse_fetch_command() {
710 let mut body = Vec::new();
711 body.extend_from_slice(&pkt("command=fetch\n"));
712 body.extend_from_slice(b"0001");
713 body.extend_from_slice(&pkt("ofs-delta\n"));
714 body.extend_from_slice(&pkt("want 0000000000000000000000000000000000000001\n"));
715 body.extend_from_slice(&pkt("done\n"));
716 body.extend_from_slice(b"0000");
717
718 let Command::Fetch(req) = parse_command_request(&body).unwrap() else {
719 panic!("expected fetch command");
720 };
721 assert_eq!(req.upload_request.wants.len(), 1);
722 assert!(req.upload_request.done);
723 assert!(req.upload_request.capabilities.ofs_delta);
724 }
725
726 #[test]
727 fn ls_refs_returns_unborn_head() {
728 let root = tempfile::TempDir::new().unwrap();
729 let repo_path = root.path().join("repo.git");
730 std::process::Command::new("git")
731 .args(["init", "--bare", repo_path.to_str().unwrap()])
732 .output()
733 .unwrap();
734 std::process::Command::new("git")
735 .args(["symbolic-ref", "HEAD", "refs/heads/main"])
736 .current_dir(&repo_path)
737 .output()
738 .unwrap();
739
740 let out = ls_refs(
741 &repo_path,
742 &LsRefsRequest {
743 unborn: true,
744 symrefs: true,
745 ..Default::default()
746 },
747 )
748 .unwrap();
749 let text = String::from_utf8(out).unwrap();
750 assert!(text.contains("unborn HEAD symref-target:refs/heads/main"));
751 }
752}