timefs 0.1.0

Mount a Git repository as a read-only filesystem.
Documentation
//! A lightweight local fuzz runner for revision specs and slash resolution.

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}"))
}