This repository has no description
1#![allow(dead_code, unused_imports)]
2
3use std::collections::BTreeSet;
4use std::io::Write;
5use std::net::SocketAddr;
6use std::path::{Path, PathBuf};
7use std::process::Stdio;
8use std::sync::Arc;
9
10use axum::Router;
11use knot_git::{Layout, Repo};
12use knot_pack::{RepoLookup, RepoResolver, RepoTarget};
13use knot_types::{ObjectFormat, RepoDid};
14
15pub use knot_fixtures::{commit, contains, must, run as git};
16
17pub fn pkt(payload: &[u8]) -> Vec<u8> {
18 let mut out = format!("{:04x}", payload.len() + 4).into_bytes();
19 out.extend_from_slice(payload);
20 out
21}
22
23pub fn pack_objects(cwd: &Path, oids: &[String]) -> Vec<u8> {
24 let mut child = knot_fixtures::command(cwd)
25 .args(["pack-objects", "--stdout", "-q"])
26 .stdin(Stdio::piped())
27 .stdout(Stdio::piped())
28 .stderr(Stdio::piped())
29 .spawn()
30 .expect("spawn pack-objects");
31 child
32 .stdin
33 .take()
34 .unwrap()
35 .write_all(oids.join("\n").as_bytes())
36 .unwrap();
37 let out = child.wait_with_output().unwrap();
38 assert!(
39 out.status.success(),
40 "pack-objects failed ({}): {}",
41 out.status,
42 String::from_utf8_lossy(&out.stderr)
43 );
44 out.stdout
45}
46
47pub fn pack_objects_tuned(cwd: &Path, oids: &[String], ofs: bool) -> Vec<u8> {
48 let args: &[&str] = if ofs {
49 &[
50 "pack-objects",
51 "--stdout",
52 "-q",
53 "--delta-base-offset",
54 "--depth=50",
55 "--window=250",
56 ]
57 } else {
58 &[
59 "-c",
60 "pack.useDeltaBaseOffset=false",
61 "pack-objects",
62 "--stdout",
63 "-q",
64 "--depth=50",
65 "--window=250",
66 ]
67 };
68 let mut child = knot_fixtures::command(cwd)
69 .args(args)
70 .stdin(Stdio::piped())
71 .stdout(Stdio::piped())
72 .stderr(Stdio::piped())
73 .spawn()
74 .expect("spawn pack-objects");
75 child
76 .stdin
77 .take()
78 .unwrap()
79 .write_all(oids.join("\n").as_bytes())
80 .unwrap();
81 let out = child.wait_with_output().unwrap();
82 assert!(
83 out.status.success(),
84 "pack-objects failed ({}): {}",
85 out.status,
86 String::from_utf8_lossy(&out.stderr)
87 );
88 out.stdout
89}
90
91pub fn index_into_bare(extra: &[&str], pack: &[u8]) -> bool {
92 let bare = tempfile::tempdir().unwrap();
93 knot_fixtures::must(
94 bare.path(),
95 &["init", "--bare", "-q", bare.path().to_str().unwrap()],
96 );
97 let args: Vec<&str> = std::iter::once("index-pack")
98 .chain(extra.iter().copied())
99 .chain(std::iter::once("--stdin"))
100 .collect();
101 knot_fixtures::feed(bare.path(), &args, pack).0
102}
103
104fn zlib(data: &[u8]) -> Vec<u8> {
105 let mut encoder = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::fast());
106 encoder.write_all(data).unwrap();
107 encoder.finish().unwrap()
108}
109
110fn base128(value: u64) -> Vec<u8> {
111 let low = (value & 0x7f) as u8;
112 let rest = value >> 7;
113 if rest == 0 {
114 vec![low]
115 } else {
116 std::iter::once(low | 0x80).chain(base128(rest)).collect()
117 }
118}
119
120fn obj_header(obj_type: u8, size: usize) -> Vec<u8> {
121 fn tail(size: usize) -> Vec<u8> {
122 if size == 0 {
123 Vec::new()
124 } else {
125 let byte = (size & 0x7f) as u8;
126 let rest = size >> 7;
127 let cont = if rest > 0 { 0x80 } else { 0 };
128 std::iter::once(byte | cont).chain(tail(rest)).collect()
129 }
130 }
131 let rest = size >> 4;
132 let cont = if rest > 0 { 0x80 } else { 0 };
133 std::iter::once((obj_type << 4) | (size & 0x0f) as u8 | cont)
134 .chain(tail(rest))
135 .collect()
136}
137
138fn ofs_distance(distance: u64) -> Vec<u8> {
139 fn prefix(value: u64) -> Vec<u8> {
140 if value == 0 {
141 Vec::new()
142 } else {
143 let reduced = value - 1;
144 prefix(reduced >> 7)
145 .into_iter()
146 .chain(std::iter::once(0x80 | (reduced & 0x7f) as u8))
147 .collect()
148 }
149 }
150 prefix(distance >> 7)
151 .into_iter()
152 .chain(std::iter::once((distance & 0x7f) as u8))
153 .collect()
154}
155
156pub fn delta_bomb_pack(declared_result_bytes: u64) -> Vec<u8> {
157 let base = b"hi";
158 let mut entry0 = obj_header(3, base.len());
159 entry0.extend(zlib(base));
160
161 let delta_stream: Vec<u8> = base128(base.len() as u64)
162 .into_iter()
163 .chain(base128(declared_result_bytes))
164 .chain([0x90, 0x02])
165 .collect();
166 let mut entry1 = obj_header(6, delta_stream.len());
167 entry1.extend(ofs_distance(entry0.len() as u64));
168 entry1.extend(zlib(&delta_stream));
169
170 let mut pack = b"PACK".to_vec();
171 pack.extend_from_slice(&2u32.to_be_bytes());
172 pack.extend_from_slice(&2u32.to_be_bytes());
173 pack.extend_from_slice(&entry0);
174 pack.extend_from_slice(&entry1);
175
176 let mut hasher = gix_hash::hasher(gix_hash::Kind::Sha1);
177 hasher.update(&pack);
178 let checksum = hasher.try_finalize().unwrap();
179 pack.extend_from_slice(checksum.as_bytes());
180 pack
181}
182
183pub fn receive_request(refname: &str, old: &str, new: &str, pack: &[u8]) -> Vec<u8> {
184 let mut first = format!("{old} {new} {refname}").into_bytes();
185 first.push(0);
186 first.extend_from_slice(b"report-status\n");
187 let mut req = pkt(&first);
188 req.extend_from_slice(b"0000");
189 req.extend_from_slice(pack);
190 req
191}
192
193pub fn unsideband(resp: &[u8]) -> Vec<u8> {
194 let mut out = Vec::new();
195 let mut pos = 0usize;
196 let mut in_pack = false;
197 while pos + 4 <= resp.len() {
198 let len = std::str::from_utf8(&resp[pos..pos + 4])
199 .ok()
200 .and_then(|hex| usize::from_str_radix(hex, 16).ok())
201 .unwrap_or(0);
202 pos += 4;
203 if len < 4 {
204 continue;
205 }
206 let end = (pos + len - 4).min(resp.len());
207 let payload = &resp[pos..end];
208 pos = end;
209 if payload == b"packfile\n" {
210 in_pack = true;
211 } else if in_pack && payload.first() == Some(&1) {
212 out.extend_from_slice(&payload[1..]);
213 }
214 }
215 out
216}
217
218pub fn object_set(dir: &Path) -> BTreeSet<String> {
219 must(
220 dir,
221 &[
222 "cat-file",
223 "--batch-all-objects",
224 "--batch-check=%(objectname)",
225 ],
226 )
227 .lines()
228 .map(|line| line.trim().to_string())
229 .filter(|line| !line.is_empty())
230 .collect()
231}
232
233pub fn incompressible(seed: u64, len: usize) -> Vec<u8> {
234 let mut state = seed.wrapping_mul(0x9E37_79B9_7F4A_7C15).wrapping_add(1);
235 (0..len)
236 .map(|_| {
237 state ^= state << 13;
238 state ^= state >> 7;
239 state ^= state << 17;
240 (state & 0xff) as u8
241 })
242 .collect()
243}
244
245pub fn serve_dids() -> Arc<dyn RepoResolver> {
246 Arc::new(|target: &RepoTarget| match target {
247 RepoTarget::Did(did) => RepoLookup::Hosted(did.clone()),
248 RepoTarget::OwnerPath(_, _) => RepoLookup::Unhosted,
249 })
250}
251
252pub async fn spawn(router: Router, bind: &str) -> SocketAddr {
253 let listener = tokio::net::TcpListener::bind(bind).await.unwrap();
254 let addr = listener.local_addr().unwrap();
255 tokio::spawn(async move {
256 axum::serve(listener, router).await.unwrap();
257 });
258 addr
259}
260
261pub fn advance_via_receive(bare: &Path, work: &Path, old: &str, new: &str) {
262 let oids: Vec<String> = must(work, &["rev-list", "--objects", new, "--not", old])
263 .lines()
264 .filter_map(|line| line.split_whitespace().next())
265 .map(str::to_string)
266 .collect();
267 let request = receive_request("refs/heads/main", old, new, &pack_objects(work, &oids));
268 let repo = Repo::open(bare).expect("open knot bare");
269 let report = knot_pack::receive_pack(&repo, &request).expect("knot receive");
270 assert!(
271 String::from_utf8_lossy(&report).contains("ok refs/heads/main"),
272 "knot must accept a receive that advances main"
273 );
274}
275
276pub fn seed_branches_and_tag(work: &Path, bares: [&Path; 2], format: ObjectFormat) {
277 let fmt = format!("--object-format={}", format.capability());
278 std::fs::create_dir_all(work).unwrap();
279 must(work, &["init", &fmt, "-q", "-b", "main"]);
280 std::fs::write(work.join("README.md"), "seed\n").unwrap();
281 must(work, &["add", "-A"]);
282 must(work, &["commit", "-q", "-m", "c1"]);
283 let c1 = must(work, &["rev-parse", "HEAD"]);
284 std::fs::write(work.join("src.txt"), "more\n").unwrap();
285 must(work, &["add", "-A"]);
286 must(work, &["commit", "-q", "-m", "c2"]);
287 must(work, &["checkout", "-q", "-b", "dev", &c1]);
288 std::fs::write(work.join("dev.txt"), "branch\n").unwrap();
289 must(work, &["add", "-A"]);
290 must(work, &["commit", "-q", "-m", "c3"]);
291 must(work, &["checkout", "-q", "main"]);
292 must(work, &["tag", "-a", "v1", "-m", "release"]);
293 bares.into_iter().for_each(|bare| {
294 must(
295 work,
296 &["push", "-q", bare.to_str().unwrap(), "main", "dev", "v1"],
297 );
298 must(bare, &["symbolic-ref", "HEAD", "refs/heads/main"]);
299 });
300}
301
302pub fn seeded(layout: &Layout, did: &RepoDid) -> (Repo, tempfile::TempDir, String, Vec<u8>) {
303 let bare = layout.create(did).unwrap();
304 let work_dir = tempfile::tempdir().unwrap();
305 let work = work_dir.path();
306 must(work, &["init", "-q", "-b", "main"]);
307 commit(work, "a.txt", "x\n", "c1");
308 let c1 = must(work, &["rev-parse", "HEAD"]);
309 let oids: Vec<String> = must(work, &["rev-list", "--objects", &c1])
310 .lines()
311 .map(|line| line.split_whitespace().next().unwrap().to_string())
312 .collect();
313 let pack = pack_objects(work, &oids);
314 (bare, work_dir, c1, pack)
315}
316
317pub struct Stand {
318 pub scan: tempfile::TempDir,
319 pub scratch: tempfile::TempDir,
320 pub layout: Layout,
321 pub addr: SocketAddr,
322 pub bare: PathBuf,
323}
324
325pub async fn stand(did: &RepoDid) -> Stand {
326 let scan = tempfile::tempdir().unwrap();
327 let layout = Layout::new(scan.path());
328 layout.create(did).unwrap();
329 let bare = layout.repo_path(did).unwrap();
330 let addr = spawn(
331 knot_pack::router(
332 layout.clone(),
333 serve_dids(),
334 std::sync::Arc::new(knot_runtime::SystemClock),
335 ),
336 "[::1]:0",
337 )
338 .await;
339 let scratch = tempfile::tempdir().unwrap();
340 Stand {
341 scan,
342 scratch,
343 layout,
344 addr,
345 bare,
346 }
347}