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