This repository has no description
1use std::io::{self, Read, Seek, SeekFrom};
2
3use knot_git::{ArchiveFormat, ArchivePrefix, Repo};
4use knot_types::Oid;
5
6use crate::error::PackError;
7use crate::pkt;
8
9struct Request {
10 treeish: String,
11 format: ArchiveFormat,
12 prefix: Option<ArchivePrefix>,
13}
14
15pub fn stream(
16 repo: &Repo,
17 request: &[u8],
18 sink: &mut dyn FnMut(&[u8]) -> io::Result<()>,
19) -> Result<(), PackError> {
20 let args = parse_arguments(request)?;
21 match build(repo, &args) {
22 Ok(mut spool) => {
23 let mut head = Vec::new();
24 pkt::write_data(&mut head, b"ACK\n")?;
25 pkt::write_flush(&mut head)?;
26 emit(sink, &head)?;
27 std::iter::from_fn(|| {
28 let mut chunk = vec![0u8; pkt::MAX_BAND];
29 match spool.read(&mut chunk) {
30 Ok(0) => None,
31 Ok(read) => {
32 chunk.truncate(read);
33 Some(Ok(chunk))
34 }
35 Err(error) => Some(Err(error)),
36 }
37 })
38 .try_for_each(|chunk| -> Result<(), PackError> {
39 let chunk = chunk.map_err(|error| PackError::Pack(error.to_string()))?;
40 let mut framed = Vec::new();
41 pkt::write_band(&mut framed, &chunk)?;
42 emit(sink, &framed)
43 })?;
44 let mut tail = Vec::new();
45 pkt::write_flush(&mut tail)?;
46 emit(sink, &tail)
47 }
48 Err(error) => {
49 let mut buf = Vec::new();
50 pkt::write_data(
51 &mut buf,
52 format!("NACK {}\n", error.to_string().replace('\n', " ")).as_bytes(),
53 )?;
54 pkt::write_flush(&mut buf)?;
55 emit(sink, &buf)
56 }
57 }
58}
59
60fn emit(sink: &mut dyn FnMut(&[u8]) -> io::Result<()>, bytes: &[u8]) -> Result<(), PackError> {
61 sink(bytes).map_err(|error| PackError::Pack(error.to_string()))
62}
63
64fn parse_arguments(request: &[u8]) -> Result<Vec<String>, PackError> {
65 Ok(pkt::data_payloads(request)?
66 .iter()
67 .filter_map(|line| {
68 std::str::from_utf8(line)
69 .ok()?
70 .trim_end_matches('\n')
71 .strip_prefix("argument ")
72 .map(str::to_string)
73 })
74 .collect())
75}
76
77fn interpret(args: &[String]) -> Result<Request, PackError> {
78 let format = args
79 .iter()
80 .find_map(|arg| arg.strip_prefix("--format="))
81 .map(format_from)
82 .unwrap_or(ArchiveFormat::Tar);
83 let prefix = args
84 .iter()
85 .find_map(|arg| arg.strip_prefix("--prefix="))
86 .map(|raw| {
87 ArchivePrefix::new(raw).map_err(|_| {
88 PackError::Protocol("archive prefix must not escape archive root".to_string())
89 })
90 })
91 .transpose()?;
92 let treeish = args
93 .iter()
94 .find(|arg| !arg.starts_with('-'))
95 .cloned()
96 .ok_or_else(|| PackError::Protocol("archive request has no tree-ish".to_string()))?;
97 Ok(Request {
98 treeish,
99 format,
100 prefix,
101 })
102}
103
104fn format_from(value: &str) -> ArchiveFormat {
105 match value {
106 "zip" => ArchiveFormat::Zip,
107 "tar.gz" | "tgz" => ArchiveFormat::TarGz,
108 _ => ArchiveFormat::Tar,
109 }
110}
111
112fn build(repo: &Repo, args: &[String]) -> Result<std::fs::File, PackError> {
113 let request = interpret(args)?;
114 let id = repo
115 .resolve_revision(&request.treeish)
116 .ok_or_else(|| PackError::Protocol(format!("cannot resolve {}", request.treeish)))?;
117 let commit = ensure_public_commit(repo, id)?;
118 let tree = repo
119 .peel_to_tree(commit)
120 .map_err(|error| PackError::Pack(error.to_string()))?;
121 let mut spool = tempfile::tempfile().map_err(|error| PackError::Pack(error.to_string()))?;
122 repo.write_archive(tree, request.format, request.prefix.as_ref(), &mut spool)
123 .map_err(|error| PackError::Pack(error.to_string()))?;
124 spool
125 .seek(SeekFrom::Start(0))
126 .map_err(|error| PackError::Pack(error.to_string()))?;
127 Ok(spool)
128}
129
130fn ensure_public_commit(repo: &Repo, id: Oid) -> Result<Oid, PackError> {
131 let unreachable =
132 || PackError::Protocol("tree-ish is not reachable from public ref".to_string());
133 let commit = repo.peel_to_commit(id).map_err(|_| unreachable())?;
134 match repo.reachable_from_public(commit) {
135 Ok(true) => Ok(commit),
136 Ok(false) => Err(unreachable()),
137 Err(error) => Err(PackError::Pack(error.to_string())),
138 }
139}