use std::env;
use std::ffi::OsStr;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::time::{SystemTime, UNIX_EPOCH};
use timefs::fs::resolve::{
is_valid_tree_entry_name, join_path_components, reference_candidates, revision_candidates,
split_path_bytes, RefNamespace,
};
use timefs::git::{GixRepository, ObjectStore, RevResolver};
fn main() -> anyhow::Result<()> {
let mut iterations = 2_048_usize;
let mut seed = 0x5eed_cafe_u64;
let mut args = env::args().skip(1);
while let Some(arg) = args.next() {
match arg.as_str() {
"--iterations" => {
if let Some(value) = args.next() {
iterations = value.parse::<usize>()?;
}
}
"--seed" => {
if let Some(value) = args.next() {
seed = value.parse::<u64>()?;
}
}
other => {
anyhow::bail!("unrecognized argument: {other}");
}
}
}
let fixture = FuzzFixture::new()?;
let store = GixRepository::open(fixture.path())?;
let mut rng = SimpleRng::new(seed);
for _ in 0..iterations {
let len = rng.range_usize(0, 48);
let raw = random_bytes(&mut rng, len);
let components = split_path_bytes(&raw);
let _ = store.resolve_revision(&raw);
let _ = store.resolve_commit_prefix(&raw);
let _ = revision_candidates(&components);
let _ = is_valid_tree_entry_name(&raw);
let _ = join_path_components(&components);
for namespace in [
RefNamespace::Heads,
RefNamespace::Tags,
RefNamespace::Remotes,
] {
for candidate in reference_candidates(namespace, &components) {
let _ = store.resolve_revision(&candidate.spec);
}
}
}
println!("completed {iterations} resolver fuzz iterations with seed {seed}");
Ok(())
}
struct FuzzFixture {
root: PathBuf,
}
impl FuzzFixture {
fn new() -> anyhow::Result<Self> {
let root = unique_temp_path("timefs-fuzz-fixture");
fs::create_dir_all(root.join("nested"))?;
let fixture = Self { root };
fixture.git(["init", "-b", "main"])?;
fixture.git(["config", "user.name", "Timefs Tests"])?;
fixture.git(["config", "user.email", "timefs-tests@example.com"])?;
fs::write(fixture.root.join("README.md"), b"timefs fuzz fixture\n")?;
fs::write(
fixture.root.join("nested/data.bin"),
[0_u8, 1, 2, 3, 0, 255],
)?;
fixture.git(["add", "."])?;
fixture.git(["commit", "-m", "Create fuzz fixture"])?;
fixture.git(["tag", "v1.0"])?;
fs::write(fixture.root.join("topic.txt"), b"topic branch data\n")?;
fixture.git(["add", "."])?;
fixture.git(["commit", "-m", "Advance fuzz fixture"])?;
fixture.git(["branch", "topic"])?;
fixture.git(["branch", "feature/x"])?;
Ok(fixture)
}
fn path(&self) -> &Path {
&self.root
}
fn git<I, S>(&self, args: I) -> anyhow::Result<Output>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let output = Command::new("git")
.args(args)
.current_dir(&self.root)
.output()?;
if output.status.success() {
Ok(output)
} else {
anyhow::bail!(
"git command failed with status {}: {}",
output.status,
String::from_utf8_lossy(&output.stderr)
);
}
}
}
impl Drop for FuzzFixture {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.root);
}
}
struct SimpleRng(u64);
impl SimpleRng {
fn new(seed: u64) -> Self {
Self(seed)
}
fn next_u64(&mut self) -> u64 {
self.0 = self
.0
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
self.0
}
fn range_usize(&mut self, start: usize, end: usize) -> usize {
if start == end {
return start;
}
start + (self.next_u64() as usize % (end - start))
}
}
fn random_bytes(rng: &mut SimpleRng, len: usize) -> Vec<u8> {
(0..len)
.map(|_| u8::try_from(rng.range_usize(0, 256)).expect("byte range fits in u8"))
.collect()
}
fn unique_temp_path(prefix: &str) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time should be after the Unix epoch")
.as_nanos();
std::env::temp_dir().join(format!("{prefix}-{nanos}"))
}