pub(super) use super::cmd::{upload_pack, upload_pack_into};
pub(super) use crate::reply_pack::Checksum;
use std::io::Write as _;
mod clone;
mod consolidation;
mod depth_one;
mod exactness;
mod full_growth;
mod negotiation;
mod wire;
use super::*;
use std::collections::HashSet;
use std::io::Read as _;
use std::net::{TcpListener, TcpStream};
use std::path::{Path, PathBuf};
use std::thread;
use crate::snapshot::RepoSnapshot;
use crate::store::RepoStore;
pub(super) fn git_stdin(dir: &Path, args: &[&str], stdin: &str) -> String {
let mut cmd = git_command(dir);
cmd.args(args).stdin(stdin_bytes(stdin.as_bytes()).unwrap());
let output = run_cmd(cmd, &format!("git {}", args.join(" "))).expect("git");
String::from_utf8(output.stdout).unwrap().trim().to_string()
}
pub(super) fn git(dir: &Path, args: &[&str]) -> String {
let mut cmd = git_command(dir);
cmd.args(args)
.env("GIT_AUTHOR_NAME", "t")
.env("GIT_AUTHOR_EMAIL", "t@t")
.env("GIT_COMMITTER_NAME", "t")
.env("GIT_COMMITTER_EMAIL", "t@t");
let output = run_cmd(cmd, &format!("git {}", args.join(" "))).expect("git");
String::from_utf8_lossy(&output.stdout).trim().to_string()
}
pub(super) fn commit(dir: &Path, path: &str, body: impl AsRef<[u8]>, message: &str) {
commit_dated(dir, path, body, message, None);
}
/// a fixed date keeps the commit's object id, and its packed size, the same each run.
pub(super) fn commit_dated(
dir: &Path,
path: &str,
body: impl AsRef<[u8]>,
message: &str,
date: Option<&str>,
) {
if let Some(parent) = std::path::Path::new(path).parent() {
std::fs::create_dir_all(dir.join(parent)).unwrap();
}
std::fs::write(dir.join(path), body.as_ref()).unwrap();
git(dir, &["add", "."]);
let mut cmd = git_command(dir);
cmd.args(["commit", "-q", "-m", message]);
cmd.env("GIT_AUTHOR_NAME", "t")
.env("GIT_AUTHOR_EMAIL", "t@t");
cmd.env("GIT_COMMITTER_NAME", "t")
.env("GIT_COMMITTER_EMAIL", "t@t");
if let Some(date) = date {
cmd.env("GIT_AUTHOR_DATE", date)
.env("GIT_COMMITTER_DATE", date);
}
run_cmd(cmd, "git commit").unwrap();
}
pub(super) fn snapshot(source: &Path, refs: Vec<(String, String)>) -> RepoSnapshot {
RepoSnapshot {
repo_did: knot_types::RepoDid::new("did:plc:smarttest").unwrap(),
owner: knot_types::OwnerDid::new("did:plc:owner").unwrap(),
rkey: knot_types::RepoRkey::new("smart").unwrap(),
name: None,
head_capture: crate::snapshot::HeadCapture {
resolved: Some(git(source, &["rev-parse", "HEAD"])),
symbolic: Some("refs/heads/main".to_string()),
unborn: false,
},
public_refs: refs
.into_iter()
.map(|(name, raw)| crate::snapshot::PublicRef {
name,
raw,
peeled: None,
})
.collect(),
cob_refs: Vec::new(),
object_format: knot_types::ObjectFormat::SHA1,
}
}
pub(super) fn init_test_source(root: &Path) -> PathBuf {
let source = root.join("plc").join("sm").join("arttest");
std::fs::create_dir_all(&source).unwrap();
git(&source, &["init", "-q", "-b", "main"]);
commit(&source, "hello.txt", "hi\n", "one");
source
}
pub(super) fn clone_over_http(
root: &Path,
export_dir: &Path,
clone_name: &str,
) -> anyhow::Result<PathBuf> {
let advertisement = std::fs::read(export_dir.join("smart/advertisement"))?;
let pack = std::fs::read(export_dir.join("smart/upload-pack"))?;
let listener = TcpListener::bind("127.0.0.1:0")?;
let addr = listener.local_addr()?;
let server = thread::spawn(move || {
for stream in listener.incoming() {
let Ok(mut stream) = stream else { break };
let mut buf = [0u8; 8192];
let n = stream.read(&mut buf).unwrap_or(0);
let req = String::from_utf8_lossy(&buf[..n]);
if req.starts_with("GET /__done ") {
break;
}
if req.starts_with("GET /x/info/refs?service=git-upload-pack ") {
let _ = stream.write_all(
format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/x-git-upload-pack-advertisement\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
advertisement.len()
)
.as_bytes(),
);
let _ = stream.write_all(&advertisement);
} else if req.starts_with("POST /x/git-upload-pack ") {
let head_end = req.find("\r\n\r\n").map(|i| i + 4).unwrap_or(n);
let cl: usize = req
.lines()
.find_map(|l| {
l.to_ascii_lowercase()
.strip_prefix("content-length: ")
.and_then(|v| v.trim().parse().ok())
})
.unwrap_or(0);
let mut body = buf[head_end..n].to_vec();
while body.len() < cl {
match stream.read(&mut buf) {
Ok(0) | Err(_) => break,
Ok(m) => body.extend_from_slice(&buf[..m]),
}
}
let _ = stream.write_all(
format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/x-git-upload-pack-result\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
pack.len()
)
.as_bytes(),
);
let _ = stream.write_all(&pack);
} else {
let _ = stream.write_all(
b"HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\nConnection: close\r\n\r\n",
);
}
}
});
let clone_dir = root.join(clone_name);
let mut clone_cmd = git_command(root);
clone_cmd.args([
"clone",
"-q",
&format!("http://{addr}/x"),
clone_dir.to_str().unwrap(),
]);
let clone_result = run_cmd(clone_cmd, "git clone over smart http");
if let Ok(mut done_stream) = TcpStream::connect(addr) {
let _ = done_stream.write_all(b"GET /__done HTTP/1.1\r\nHost: x\r\n\r\n");
}
let _ = server.join();
clone_result?;
Ok(clone_dir)
}
pub(super) fn export_repo_test(scan_root: &Path, snap: &RepoSnapshot, export_dir: &Path) {
let config = test_config(scan_root);
let fingerprint = super::export_fingerprint(snap, config.default_branch.as_str(), None);
super::materialize_export(&config, snap, export_dir, &fingerprint, None, None).unwrap();
}
pub(super) fn heads(source: &Path) -> Vec<(String, String)> {
git(
source,
&[
"for-each-ref",
"--format=%(refname) %(objectname)",
"refs/heads",
],
)
.lines()
.map(|line| {
let (name, oid) = line.split_once(' ').unwrap();
(name.to_string(), oid.to_string())
})
.collect()
}
/// letters that don't compress, because pack sizes should follow content size.
pub(super) fn noise(seed: u64, len: usize) -> String {
let mut state = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
(0..len)
.map(|_| {
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
char::from(b'a' + (state >> 59) as u8 % 26)
})
.collect()
}
pub(super) fn export_against(
root: &Path,
source: &Path,
name: &str,
prior: Option<&ExportPrior>,
store: &RepoStore,
) -> (PathBuf, ExportPrior) {
export_refs_against(root, source, name, prior, store, heads(source))
}
pub(super) fn export_refs_against(
root: &Path,
source: &Path,
name: &str,
prior: Option<&ExportPrior>,
store: &RepoStore,
refs: Vec<(String, String)>,
) -> (PathBuf, ExportPrior) {
let snap = snapshot(source, refs);
let config = test_config(root);
let fingerprint = super::export_fingerprint(&snap, "main", prior);
let export = root.join(name);
super::materialize_export(&config, &snap, &export, &fingerprint, Some(store), prior).unwrap();
assert_partitions_exact(root, source, &export, &snap);
let bases = super::fetch_replies(&export).unwrap().bases;
let refs = snap
.public_refs
.iter()
.map(|r| (r.name.clone(), r.raw.clone()))
.collect();
(
export,
ExportPrior {
fingerprint,
refs,
bases,
},
)
}
pub(super) fn assert_have_replies_whole(root: &Path, export: &Path) -> anyhow::Result<()> {
let replies = super::fetch_replies(export)?;
let refs = git(export, &["for-each-ref", "--format=%(objectname)"]);
let every: Vec<&str> = refs.lines().collect();
let moved: Vec<&str> = replies.moved_wants.iter().map(String::as_str).collect();
for (n, base) in replies.bases.iter().enumerate() {
for (name, wants) in [
(format!("have-{n}-done"), &every),
(format!("moved-{n}-done"), &moved),
] {
let Ok(reply) = std::fs::read(export.join("smart").join(&name)) else {
continue;
};
let client = tempfile::tempdir_in(root)?;
git(client.path(), &["init", "-q", "--bare"]);
let mut pack = git_command(export);
pack.args(["pack-objects", "--revs", "--stdout", "-q"]);
pack.stdin(stdin_bytes(base.join("\n").as_bytes())?);
let had = run_cmd(pack, "git pack-objects")?.stdout;
let mut index = git_command(client.path());
index
.args(["index-pack", "--stdin"])
.stdin(stdin_bytes(&had)?);
run_cmd(index, "git index-pack")?;
// strict refuses a missing delta base or repeated object, plain index-pack doesn't
let mut index = git_command(client.path());
index.args(["index-pack", "--strict", "--stdin"]);
index.stdin(stdin_bytes(&demux(&reply).1)?);
run_cmd(index, &format!("git index-pack --strict of {name}"))?;
let mut missing = git_command(client.path());
missing.args(["cat-file", "--batch-check"]);
missing.stdin(stdin_bytes(reached(export, wants).join("\n").as_bytes())?);
let checked = String::from_utf8(run_cmd(missing, "git cat-file")?.stdout)?;
assert!(!checked.contains("missing"), "{name}: {checked}");
}
}
Ok(())
}
pub(super) fn cut_after_change(tree: &[(String, usize)], changed: &[usize]) -> (bool, f64) {
let root = tempfile::tempdir().unwrap();
let store = RepoStore::open(&root.path().join("repo-store"));
let source = init_test_source(root.path());
for (n, (name, len)) in tree.iter().enumerate() {
commit(&source, name, &noise(n as u64, *len), "f");
}
let (_, prior) = export_against(root.path(), &source, "export-1", None, &store);
let before = depth_one_objects(&source);
for &n in changed {
let (name, len) = &tree[n];
commit(&source, name, &noise(1000 + n as u64, *len), "changed");
}
let after = depth_one_objects(&source);
let stayed = after.iter().filter(|oid| before.contains(*oid)).count();
let (export, _) = export_against(root.path(), &source, "export-2", Some(&prior), &store);
let reply = export.join("smart/shallow-head");
let tip = git(&source, &["rev-parse", "main"]);
assert_depth_one_pack(root.path(), &source, &std::fs::read(&reply).unwrap(), &tip);
let (entries, _) =
crate::depth_one::tests::map_of(&reply, knot_types::ObjectFormat::SHA1).expect("a map");
(entries[0] == tip, stayed as f64 / after.len() as f64)
}
pub(super) fn depth_one_objects(source: &Path) -> HashSet<String> {
let listed = git(source, &["rev-list", "--objects", "--no-walk", "main"]);
listed
.lines()
.map(|line| listed_oid(line).to_string())
.collect()
}
pub(super) fn listed_oid(line: &str) -> &str {
line.split(' ').next().unwrap_or(line)
}
pub(super) fn assert_depth_one_pack(
root: &Path,
source: &Path,
reply: &[u8],
tip: &str,
) -> Vec<String> {
let (preamble, pack) = demux(reply);
let shallow = format!("shallow {tip}").into_bytes();
assert_eq!(
preamble,
[
b"shallow-info\n".to_vec(),
shallow,
Vec::new(),
b"packfile\n".to_vec()
]
);
let check = tempfile::tempdir_in(root).unwrap();
git(check.path(), &["init", "-q", "--bare"]);
std::fs::write(check.path().join("shallow"), format!("{tip}\n")).unwrap();
let mut index = git_command(check.path());
index.args(["index-pack", "--strict", "--stdin"]);
index.stdin(stdin_bytes(&pack).unwrap());
run_cmd(index, "git index-pack --strict").unwrap();
let all = [
"cat-file",
"--batch-all-objects",
"--batch-check=%(objectname)",
];
let packed: Vec<String> = git(check.path(), &all)
.lines()
.map(str::to_string)
.collect();
let walk = git(source, &["rev-list", "--objects", "--no-walk", tip]);
let mut reached: Vec<String> = walk
.lines()
.map(|line| listed_oid(line).to_string())
.collect();
reached.sort();
assert_eq!(packed, reached);
packed
}
pub(super) fn orphan_commit(source: &Path, message: &str) -> String {
let tree = git(source, &["rev-parse", "main^{tree}"]);
git(
source,
&[
"-c",
"user.name=t",
"-c",
"user.email=t@t",
"commit-tree",
&tree,
"-m",
message,
],
)
}
pub(super) fn pack_inodes(repo: &Path) -> HashSet<u64> {
use std::os::unix::fs::MetadataExt as _;
std::fs::read_dir(repo.join("objects/pack"))
.unwrap()
.map(|entry| entry.unwrap().metadata().unwrap().ino())
.collect()
}
pub(super) fn demux(reply: &[u8]) -> (Vec<Vec<u8>>, Vec<u8>) {
let (mut preamble, mut pack, mut at) = (Vec::new(), Vec::new(), 0);
loop {
let len =
usize::from_str_radix(std::str::from_utf8(&reply[at..at + 4]).unwrap(), 16).unwrap();
if len == 0 {
assert_eq!(at + 4, reply.len(), "bytes after the final flush");
return (preamble, pack);
}
let payload = &reply[at + 4..at + len.max(4)];
match payload.first() {
Some(1) => pack.extend_from_slice(&payload[1..]),
Some(2) if payload.len() == 1 => {}
_ => {
assert!(pack.is_empty(), "a packet outside the side band");
preamble.push(payload.to_vec());
}
}
at += len.max(4);
}
}
pub(super) fn assert_whole_pack(
root: &Path,
source: &Path,
reply: &[u8],
tips: &[String],
format: &str,
) {
let (_, pack) = demux(reply);
let check = tempfile::tempdir_in(root).unwrap();
git(
check.path(),
&["init", "-q", "--bare", &format!("--object-format={format}")],
);
let mut index = git_command(check.path());
index.args(["index-pack", "--strict", "--stdin"]);
index.stdin(stdin_bytes(&pack).unwrap());
run_cmd(index, "git index-pack --strict").unwrap();
let count = u32::from_be_bytes(pack[8..12].try_into().unwrap()) as usize;
let mut walk = vec!["rev-list", "--objects"];
walk.extend(tips.iter().map(String::as_str));
assert_eq!(
count,
git(source, &walk).lines().count(),
"objects in the pack"
);
for (n, tip) in tips.iter().enumerate() {
git(
check.path(),
&["update-ref", &format!("refs/heads/t{n}"), tip],
);
}
git(check.path(), &["fsck", "--strict", "--no-dangling"]);
}
pub(super) fn reply_objects(
root: &Path,
reply: &[u8],
format: knot_types::ObjectFormat,
) -> Vec<String> {
let (_, pack) = demux(reply);
let check = tempfile::tempdir_in(root).unwrap();
let object_format = format!("--object-format={}", format.capability());
git(check.path(), &["init", "-q", "--bare", &object_format]);
let mut index = git_command(check.path());
index.args(["index-pack", "--strict", "--stdin"]);
index.stdin(stdin_bytes(&pack).unwrap());
run_cmd(index, "git index-pack --strict").unwrap();
let all = [
"cat-file",
"--batch-all-objects",
"--batch-check=%(objectname)",
];
git(check.path(), &all)
.lines()
.map(str::to_string)
.collect()
}
pub(super) fn packed_in(export: &Path, packs: &[super::partitions::PartitionPack]) -> Vec<String> {
let mut oids = Vec::new();
for pack in packs {
let idx = std::fs::read(pack.path(export, "idx")).unwrap();
let mut show = git_command(export);
show.args(["show-index"]).stdin(stdin_bytes(&idx).unwrap());
let listing = run_cmd(show, "git show-index").unwrap().stdout;
oids.extend(
String::from_utf8(listing)
.unwrap()
.lines()
.map(|line| line.split(' ').nth(1).unwrap().to_string()),
);
}
oids.sort();
oids
}
pub(super) fn added_since(prior: &Path, source: &Path, tips: &[&str]) -> Option<Vec<u8>> {
let partitions = super::Partitions::read(prior).unwrap().unwrap();
let held = super::partitions::PackIndexes::open(
prior,
partitions.public().iter().chain(partitions.cob()),
knot_types::ObjectFormat::SHA1,
)
.unwrap();
let repo = knot_git::Repo::open(source).unwrap();
let tips = super::partitions::object_ids(tips.iter().copied()).unwrap();
let segment = super::partitions::unknown_reached(repo.git(), &tips, &held).unwrap();
if segment.is_empty() {
return None;
}
let mut pack = git_command(source);
pack.args(["pack-objects", "--stdout", "-q", "--delta-base-offset"]);
pack.stdin(stdin_bytes(segment.object_list()).unwrap());
Some(run_cmd(pack, "git pack-objects").unwrap().stdout)
}
pub(super) fn reached(source: &Path, tips: &[&str]) -> Vec<String> {
if tips.is_empty() {
return Vec::new();
}
let mut walk = vec!["rev-list", "--objects", "--no-object-names"];
walk.extend(tips);
let mut oids: Vec<String> = git(source, &walk).lines().map(str::to_string).collect();
oids.sort();
oids
}
pub(super) fn assert_partitions_exact(
root: &Path,
source: &Path,
export: &Path,
snap: &RepoSnapshot,
) {
let partitions = super::Partitions::read(export)
.unwrap()
.expect("no partitions");
let public: Vec<&str> = snap.public_refs.iter().map(|r| r.raw.as_str()).collect();
let exported: Vec<&str> = super::exported_refs(snap).map(|(_, raw)| raw).collect();
let public_reached = reached(source, &public);
assert_eq!(
packed_in(export, partitions.public()),
public_reached,
"the public partition"
);
let mut both = packed_in(export, partitions.public());
both.extend(packed_in(export, partitions.cob()));
both.sort();
both.dedup();
assert_eq!(both, reached(source, &exported), "the partitions together");
let format = snap.object_format;
let spares = crate::spare::read(&export.join("smart"));
let with_fillers = |mut objects: Vec<String>, at: usize| {
let ids = spares[at].iter().flat_map(|spare| spare.ids(format));
objects.extend(ids.map(|id| knot_types::lowercase_hex(&id)));
objects.sort();
objects
};
let mut v0 = reply_objects(
root,
&std::fs::read(export.join("smart/upload-pack")).unwrap(),
format,
);
v0.sort();
assert_eq!(v0, with_fillers(public_reached, 0), "the v0 reply");
let mut v2 = reply_objects(
root,
&std::fs::read(export.join("smart/fetch-done")).unwrap(),
format,
);
v2.sort();
assert_eq!(
v2,
with_fillers(reached(source, &exported), 1),
"the v2 reply"
);
}
#[cfg(target_os = "linux")]
pub(super) fn shares_extent(path: &Path, offset: u64) -> bool {
const FIEMAP_EXTENT_SHARED: u32 = 0x2000;
extent_at(path, offset).is_some_and(|(_, flags)| flags & FIEMAP_EXTENT_SHARED != 0)
}
#[cfg(target_os = "linux")]
pub(super) fn same_on_disk(path: &Path, offset: u64, other: &Path, at: u64) -> bool {
let (here, there) = (extent_at(path, offset), extent_at(other, at));
here.is_some() && here.map(|(disk, _)| disk) == there.map(|(disk, _)| disk)
}
#[cfg(target_os = "linux")]
pub(super) fn extent_at(path: &Path, offset: u64) -> Option<(u64, u32)> {
use std::os::fd::AsRawFd as _;
#[repr(C)]
#[derive(Default)]
struct Extent {
logical: u64,
physical: u64,
length: u64,
reserved64: [u64; 2],
flags: u32,
reserved: [u32; 3],
}
#[repr(C)]
#[derive(Default)]
struct Map {
start: u64,
length: u64,
flags: u32,
mapped: u32,
count: u32,
reserved: u32,
extents: [Extent; 1],
}
const FS_IOC_FIEMAP: libc::Ioctl = 0xC020_660B as libc::Ioctl;
const FIEMAP_FLAG_SYNC: u32 = 1;
let file = std::fs::File::open(path).unwrap();
let mut map = Map {
start: offset,
length: 1,
flags: FIEMAP_FLAG_SYNC,
count: 1,
..Default::default()
};
// SAFETY: map is a fiemap with room for the one extent it asks for.
let status = unsafe { libc::ioctl(file.as_raw_fd(), FS_IOC_FIEMAP, &mut map) };
assert_eq!(status, 0, "fiemap: {}", std::io::Error::last_os_error());
let extent = &map.extents[0];
(map.mapped == 1).then(|| (extent.physical + (offset - extent.logical), extent.flags))
}
#[cfg(target_os = "linux")]
pub(super) fn reflinks_here(root: &Path) -> bool {
let source = tempfile::NamedTempFile::new_in(root).unwrap();
std::fs::write(source.path(), noise(7, 8192)).unwrap();
let target = tempfile::NamedTempFile::new_in(root).unwrap();
crate::reply_pack::reflink(source.as_file(), target.as_file()).is_ok()
}
pub(super) fn commit_packs(dir: &Path, sizes: &[usize]) -> (PathBuf, Vec<PathBuf>) {
let repo = dir.join("repo");
git(dir, &["init", "-q", "repo"]);
let mut packs = Vec::new();
let mut prior: Option<String> = None;
for (n, size) in sizes.iter().enumerate() {
commit(&repo, &format!("f{n}"), &noise(n as u64, *size), "c");
let tip = git(&repo, &["rev-parse", "HEAD"]);
let revs = [Some(tip.clone()), prior.map(|prior| format!("^{prior}"))];
let base = dir.join(format!("p{n}"));
let hash = git_stdin(
&repo,
&["pack-objects", "--revs", "-q", base.to_str().unwrap()],
&revs.into_iter().flatten().collect::<Vec<_>>().join("\n"),
);
packs.push(PathBuf::from(format!("{}-{hash}.pack", base.display())));
prior = Some(tip);
}
(repo, packs)
}
pub(super) fn read_write(path: &Path) -> anyhow::Result<std::fs::File> {
Ok(std::fs::File::options()
.read(true)
.write(true)
.create_new(true)
.open(path)?)
}
/// a reply framed from packs an earlier reply was framed from shares that reply's bytes
/// below the packet where they part, where the filesystem can.
pub(super) fn export_spared(
root: &Path,
source: &Path,
name: &str,
prior: Option<&ExportPrior>,
store: &RepoStore,
format: knot_types::ObjectFormat,
cob: Option<&str>,
) -> (PathBuf, ExportPrior, [Option<crate::spare::Spare>; 2]) {
let mut snap = snapshot(source, heads(source));
snap.object_format = format;
let ticket = "refs/cobs/org.tangled.track.ticket/1";
snap.cob_refs = cob
.iter()
.map(|cob| (ticket.to_string(), cob.to_string()))
.collect();
let fingerprint = super::export_fingerprint(&snap, "main", prior);
let dir = root.join(name);
let config = test_config(root);
super::materialize_export(&config, &snap, &dir, &fingerprint, Some(store), prior).unwrap();
let main = git(source, &["rev-parse", "main"]);
let tips = [
vec![main.as_str()],
[main.as_str()].into_iter().chain(cob).collect(),
];
let spares = crate::spare::read(&dir.join("smart"));
for (at, name) in super::FULL_REPLIES.iter().enumerate() {
let reply = std::fs::read(dir.join("smart").join(name)).unwrap();
let mut held = reply_objects(root, &reply, format);
held.sort();
let mut expected = reached(source, &tips[at]);
let ids = spares[at].iter().flat_map(|spare| spare.ids(format));
expected.extend(ids.map(|id| knot_types::lowercase_hex(&id)));
expected.sort();
assert_eq!(held, expected, "{name} of {name:?}");
}
let refs = exported_refs(&snap)
.map(|(name, raw)| (name.to_string(), raw.to_string()))
.collect();
let bases = super::fetch_replies(&dir).unwrap().bases;
let next = ExportPrior {
fingerprint,
refs,
bases,
};
(dir, next, spares)
}
pub(super) fn reply_count(path: &Path) -> u32 {
let (_, pack) = demux(&std::fs::read(path).unwrap());
u32::from_be_bytes(pack[8..12].try_into().unwrap())
}
pub(super) fn pack_entry_types(reply: &[u8]) -> Vec<u8> {
let (_, pack) = demux(reply);
assert_eq!(&pack[..4], b"PACK");
let count = u32::from_be_bytes(pack[8..12].try_into().unwrap());
let mut at = 12;
(0..count)
.map(|_| {
let kind = (pack[at] >> 4) & 7;
while pack[at] & 0x80 != 0 {
at += 1;
}
at += 1;
match kind {
6 => {
while pack[at] & 0x80 != 0 {
at += 1;
}
at += 1;
}
7 => at += 20,
_ => {}
}
let mut inflate = flate2::Decompress::new(true);
let mut sink = vec![0; 1 << 16];
loop {
let before = inflate.total_in();
let status = inflate
.decompress(&pack[at..], &mut sink, flate2::FlushDecompress::None)
.unwrap();
at += (inflate.total_in() - before) as usize;
if status == flate2::Status::StreamEnd {
break;
}
}
kind
})
.collect()
}
pub(super) fn bytes_contains(haystack: &[u8], needle: &[u8]) -> bool {
bytes_position(haystack, needle).is_some()
}
pub(super) fn bytes_position(haystack: &[u8], needle: &[u8]) -> Option<usize> {
haystack
.windows(needle.len())
.position(|window| window == needle)
}
pub(super) fn test_config(scan_root: &Path) -> crate::config::ValidatedConfig {
crate::config::ValidatedConfig {
config_path: scan_root.join("knot-static.toml"),
hostname: knot_types::KnotHostname::new("knot.example").unwrap(),
knot_did: knot_types::KnotId::new("did:web:knot.example").unwrap(),
owner: knot_types::AccountDid::new("did:plc:owner").unwrap(),
scan_path: scan_root.to_path_buf(),
default_branch: knot_types::BranchName::new("main").unwrap(),
object_format: knot_types::ObjectFormat::SHA1,
sealed_key_file: scan_root.join("nope.sealed"),
master_key_env: "X".to_string(),
plc_directory: url::Url::parse("http://127.0.0.1:9").unwrap(),
output_root: scan_root.to_path_buf(),
crawlers: Vec::new(),
allow_private_addresses: false,
writes: Default::default(),
activate: crate::config::ActivateConfig::default(),
serve: Default::default(),
firehose_socket: None,
index: Default::default(),
}
}