This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knot2 / crates / knot-pack / tests / common / mod.rs
10 kB 337 lines
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}