use std::ffi::{CString, OsStr, OsString};
use std::fs;
use std::io::Read;
use std::mem::MaybeUninit;
use std::os::unix::ffi::{OsStrExt, OsStringExt};
use std::os::unix::fs::{symlink, PermissionsExt};
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Output, Stdio};
use std::thread;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
#[test]
fn revision_resolvers_match_git_and_unmount_cleanly() {
let fixture = FixtureRepository::new();
let mountpoint = create_temp_dir("timefs-task4-mount");
let mut mount = spawn_mount(fixture.path(), &mountpoint);
wait_for_mount_ready(&mut mount, &mountpoint, b"now");
assert_eq!(
sorted_dir_entries(&mountpoint),
vec![
b"at".to_vec(),
b"commits".to_vec(),
b"history".to_vec(),
b"now".to_vec(),
b"refs".to_vec(),
]
);
assert!(
fs::read_dir(mountpoint.join("at"))
.expect("at/ should be readable")
.next()
.is_none(),
"at/ should not enumerate revisions"
);
assert_revision_tree_matches(&fixture, &mountpoint.join("now"), "HEAD");
assert_revision_tree_matches(&fixture, &mountpoint.join("at").join("HEAD~1"), "HEAD~1");
assert_revision_tree_matches(&fixture, &mountpoint.join("at").join("topic"), "topic");
assert_revision_tree_matches(&fixture, &mountpoint.join("at").join("v1.0"), "v1.0");
let raw_sha = fixture.git_rev_parse("HEAD~1");
assert_revision_tree_matches(
&fixture,
&mountpoint.join("at").join(raw_sha.as_str()),
"HEAD~1",
);
let short_sha = fixture.git_rev_parse_short("HEAD~1");
assert_revision_tree_matches(
&fixture,
&mountpoint.join("commits").join(short_sha.as_str()),
"HEAD~1",
);
let exec_mode = fs::symlink_metadata(mountpoint.join("now").join("run.sh"))
.expect("executable file metadata should be readable")
.permissions()
.mode();
let regular_mode = fs::symlink_metadata(mountpoint.join("now").join("README.md"))
.expect("regular file metadata should be readable")
.permissions()
.mode();
assert_eq!(exec_mode & 0o111, 0o111);
assert_eq!(regular_mode & 0o111, 0);
assert_eq!(
sorted_dir_entries(&mountpoint.join("refs").join("heads")),
vec![b"feature".to_vec(), b"main".to_vec(), b"topic".to_vec()]
);
assert_revision_tree_matches(
&fixture,
&mountpoint
.join("refs")
.join("heads")
.join("feature")
.join("x"),
"feature/x",
);
assert_eq!(
fs::read(
mountpoint
.join("refs")
.join("heads")
.join("feature")
.join("x")
.join("topic.txt"),
)
.expect("feature/x topic.txt should be readable"),
fixture.git_show_path("feature/x", b"topic.txt"),
);
let missing_revision = fs::metadata(mountpoint.join("at").join("does-not-exist"))
.expect_err("missing revisions should return ENOENT");
assert_eq!(missing_revision.raw_os_error(), Some(libc::ENOENT));
run_timefs(["unmount", mountpoint.to_string_lossy().as_ref()]);
wait_for_process_exit(
&mut mount,
"mount process should exit after explicit unmount",
);
wait_for_mount_cleared(&mountpoint);
let sigint_mountpoint = create_temp_dir("timefs-task4-sigint");
let mut sigint_mount = spawn_mount(fixture.path(), &sigint_mountpoint);
wait_for_mount_ready(&mut sigint_mount, &sigint_mountpoint, b"now");
send_sigint(sigint_mount.id());
wait_for_process_exit(
&mut sigint_mount,
"mount process should exit cleanly on SIGINT",
);
wait_for_mount_cleared(&sigint_mountpoint);
}
#[test]
fn metadata_and_corrupt_objects_match_spec() {
let fixture = MetadataFixtureRepository::new();
let mountpoint = create_temp_dir("timefs-task5-mount");
let mut mount = spawn_mount(fixture.path(), &mountpoint);
wait_for_mount_ready(&mut mount, &mountpoint, b"now");
let now_root = mountpoint.join("now");
let head_root = mountpoint.join("at").join("HEAD");
let readme = head_root.join("README.md");
let run_script = now_root.join("run.sh");
let link = now_root.join("link-to-script");
let gitlink_dir = now_root.join("vendor").join("submodule");
let gitlink_file = gitlink_dir.join(".gitlink");
assert_eq!(statvfs_free_blocks(&mountpoint), (0, 0));
let exec_mode = fs::symlink_metadata(&run_script)
.expect("executable file metadata should be readable")
.permissions()
.mode();
let regular_mode = fs::symlink_metadata(&readme)
.expect("regular file metadata should be readable")
.permissions()
.mode();
let symlink_mode = fs::symlink_metadata(&link)
.expect("symlink metadata should be readable")
.permissions()
.mode();
let gitlink_dir_mode = fs::symlink_metadata(&gitlink_dir)
.expect("gitlink directory metadata should be readable")
.permissions()
.mode();
let gitlink_file_mode = fs::symlink_metadata(&gitlink_file)
.expect("gitlink placeholder metadata should be readable")
.permissions()
.mode();
assert_eq!(exec_mode & 0o777, 0o555);
assert_eq!(regular_mode & 0o777, 0o444);
assert_eq!(symlink_mode & 0o777, 0o777);
assert_eq!(gitlink_dir_mode & 0o777, 0o555);
assert_eq!(gitlink_file_mode & 0o777, 0o444);
assert_eq!(
list_xattrs(&readme),
vec![
b"user.timefs.commit".to_vec(),
b"user.timefs.mode".to_vec(),
b"user.timefs.oid".to_vec(),
b"user.timefs.type".to_vec(),
]
);
assert_eq!(
get_xattr(&readme, "user.timefs.oid"),
fixture.git_rev_parse("HEAD:README.md").into_bytes()
);
assert_eq!(get_xattr(&readme, "user.timefs.mode"), b"100644");
assert_eq!(get_xattr(&readme, "user.timefs.type"), b"blob");
assert_eq!(
get_xattr(&readme, "user.timefs.commit"),
fixture.git_rev_parse("HEAD^{commit}").into_bytes()
);
assert_eq!(
get_xattr_error(&readme, "user.timefs.unknown"),
libc::ENODATA
);
assert_eq!(
get_xattr_small_buffer_error(&readme, "user.timefs.oid", 8),
libc::ERANGE
);
assert_eq!(get_xattr(&gitlink_dir, "user.timefs.type"), b"gitlink");
assert_eq!(
get_xattr(&gitlink_dir, "user.timefs.oid"),
fixture.submodule_head().as_bytes()
);
assert_eq!(
fs::read(&gitlink_file).expect(".gitlink file should be readable"),
format!("{}\n", fixture.submodule_head()).into_bytes()
);
let broken_error = fs::read(head_root.join("broken.txt"))
.expect_err("corrupt blob reads should return an error");
assert_eq!(broken_error.raw_os_error(), Some(libc::EIO));
assert_eq!(
fs::read(&readme).expect("healthy nodes should stay readable after EIO"),
b"timefs metadata fixture\n"
);
run_timefs(["unmount", mountpoint.to_string_lossy().as_ref()]);
wait_for_process_exit(
&mut mount,
"mount process should exit after explicit unmount",
);
wait_for_mount_cleared(&mountpoint);
}
#[test]
fn large_fixture_mount_walks_with_small_cache() {
let fixture = LargeFixtureRepository::new();
let mountpoint = create_temp_dir("timefs-task6-mount");
let mut mount =
spawn_mount_with_args(fixture.path(), &mountpoint, &["-f", "--cache-size", "1"]);
wait_for_mount_ready(&mut mount, &mountpoint, b"now");
let mounted_now = mountpoint.join("now");
let first_walk = collect_mount_leaf_paths(&mounted_now);
let second_walk = collect_mount_leaf_paths(&mounted_now);
assert_eq!(first_walk.len(), fixture.file_count());
assert_eq!(first_walk, second_walk);
for path in fixture.sample_paths() {
let mounted_path = mounted_now.join(OsString::from_vec(path.clone()));
assert_eq!(
fs::read(&mounted_path).expect("sample file should be readable"),
fixture.git_show_path("HEAD", &path),
);
}
run_timefs(["unmount", mountpoint.to_string_lossy().as_ref()]);
wait_for_process_exit(
&mut mount,
"mount process should exit after explicit unmount",
);
wait_for_mount_cleared(&mountpoint);
}
#[test]
fn shallow_clone_missing_revisions_log_the_cause() {
let fixture = ShallowCloneFixtureRepository::new();
let mountpoint = create_temp_dir("timefs-task7-shallow");
let mut mount =
spawn_mount_with_cli_and_mount_args(fixture.path(), &mountpoint, &["-v"], &["-f"]);
wait_for_mount_ready(&mut mount, &mountpoint, b"now");
let error = fs::metadata(mountpoint.join("at").join("HEAD~1"))
.expect_err("missing shallow revisions should return ENOENT");
assert_eq!(error.raw_os_error(), Some(libc::ENOENT));
run_timefs(["unmount", mountpoint.to_string_lossy().as_ref()]);
let status = mount
.wait()
.expect("shallow-clone mount child status should be readable");
assert!(status.success(), "shallow-clone mount should exit cleanly");
wait_for_mount_cleared(&mountpoint);
let stderr = child_stderr(&mut mount);
assert!(
stderr.contains("repository is shallow"),
"expected shallow-clone log line in stderr, got: {stderr}"
);
}
#[test]
fn lfs_mount_serves_pointer_or_local_content() {
let fixture = LfsFixtureRepository::new();
let mountpoint = create_temp_dir("timefs-task7-lfs");
let mut pointer_mount = spawn_mount_with_args(fixture.path(), &mountpoint, &["-f"]);
wait_for_mount_ready(&mut pointer_mount, &mountpoint, b"now");
let pointer_asset = mountpoint.join("now").join("asset.bin");
let missing_asset = mountpoint.join("now").join("missing.bin");
assert_eq!(
fs::read(&pointer_asset).expect("pointer asset should be readable"),
fixture.pointer_contents("asset.bin")
);
assert_eq!(get_xattr(&pointer_asset, "user.timefs.lfs"), b"pointer");
assert_eq!(
fs::read(&missing_asset).expect("missing LFS object should still expose the pointer"),
fixture.pointer_contents("missing.bin")
);
assert_eq!(get_xattr(&missing_asset, "user.timefs.lfs"), b"pointer");
run_timefs(["unmount", mountpoint.to_string_lossy().as_ref()]);
wait_for_process_exit(
&mut pointer_mount,
"pointer-mode mount should exit after unmount",
);
wait_for_mount_cleared(&mountpoint);
let mut resolved_mount = spawn_mount_with_args(fixture.path(), &mountpoint, &["-f", "--lfs"]);
wait_for_mount_ready(&mut resolved_mount, &mountpoint, b"now");
let resolved_asset = mountpoint.join("now").join("asset.bin");
assert_eq!(
fs::read(&resolved_asset).expect("resolved LFS asset should be readable"),
fixture.resolved_contents()
);
assert_eq!(get_xattr(&resolved_asset, "user.timefs.lfs"), b"resolved");
assert_eq!(
fs::read(&missing_asset).expect("missing LFS object should still expose the pointer"),
fixture.pointer_contents("missing.bin")
);
assert_eq!(get_xattr(&missing_asset, "user.timefs.lfs"), b"pointer");
run_timefs(["unmount", mountpoint.to_string_lossy().as_ref()]);
wait_for_process_exit(
&mut resolved_mount,
"resolved-mode mount should exit after unmount",
);
wait_for_mount_cleared(&mountpoint);
}
#[test]
fn submodules_recurse_mounts_local_checkouts() {
let fixture = MetadataFixtureRepository::new();
let mountpoint = create_temp_dir("timefs-task7-submodules");
let mut mount = spawn_mount_with_args(
fixture.path(),
&mountpoint,
&["-f", "--submodules", "recurse"],
);
wait_for_mount_ready(&mut mount, &mountpoint, b"now");
let submodule_root = mountpoint.join("now").join("vendor").join("submodule");
let submodule_file = submodule_root.join("submodule.txt");
assert_eq!(
fs::read(&submodule_file).expect("recursed submodule file should be readable"),
b"pinned submodule contents\n"
);
assert_eq!(get_xattr(&submodule_root, "user.timefs.type"), b"tree");
assert_eq!(
get_xattr(&submodule_file, "user.timefs.commit"),
fixture.submodule_head().as_bytes()
);
let placeholder_error = fs::metadata(submodule_root.join(".gitlink"))
.expect_err("recursed submodules should not expose the placeholder file");
assert_eq!(placeholder_error.raw_os_error(), Some(libc::ENOENT));
run_timefs(["unmount", mountpoint.to_string_lossy().as_ref()]);
wait_for_process_exit(
&mut mount,
"submodule recurse mount should exit after unmount",
);
wait_for_mount_cleared(&mountpoint);
}
fn assert_revision_tree_matches(fixture: &FixtureRepository, mount_root: &Path, rev: &str) {
let git_entries = fixture.git_ls_tree_recursive(rev);
let mount_paths = collect_mount_leaf_paths(mount_root);
assert_eq!(
mount_paths,
git_entries
.iter()
.map(|entry| entry.path.clone())
.collect::<Vec<_>>()
);
for entry in &git_entries {
let mounted_path = mount_root.join(OsString::from_vec(entry.path.clone()));
match entry.kind.as_str() {
"blob" if entry.mode == 0o120000 => {
let target = fs::read_link(&mounted_path).expect("symlink should resolve");
assert_eq!(
target.as_os_str().as_bytes(),
fixture.git_show_path(rev, &entry.path)
);
}
"blob" => {
assert_eq!(
fs::read(&mounted_path).expect("file should be readable"),
fixture.git_show_path(rev, &entry.path)
);
}
other => panic!("unexpected git entry kind in fixture: {other}"),
}
}
}
struct FixtureRepository {
root: PathBuf,
}
impl FixtureRepository {
fn new() -> Self {
let root = unique_temp_path("timefs-task4-fixture");
fs::create_dir_all(root.join("nested")).expect("fixture directory creation should succeed");
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 fixture\n")
.expect("README write should succeed");
fs::write(
fixture.root.join("nested/data.bin"),
[0_u8, 1, 2, 3, 0, 255],
)
.expect("binary write should succeed");
fs::write(fixture.root.join("run.sh"), b"#!/bin/sh\necho timefs\n")
.expect("script write should succeed");
symlink("run.sh", fixture.root.join("link-to-script"))
.expect("symlink creation should succeed");
let mut permissions = fs::metadata(fixture.root.join("run.sh"))
.expect("script metadata should be readable")
.permissions();
permissions.set_mode(0o755);
fs::set_permissions(fixture.root.join("run.sh"), permissions)
.expect("script permissions should be writable");
fixture.git(["add", "."]);
fixture.git(["commit", "-m", "Create fixture repository"]);
fixture.git(["tag", "v1.0"]);
fs::write(fixture.root.join("README.md"), b"timefs fixture v2\n")
.expect("updated README write should succeed");
fs::write(fixture.root.join("topic.txt"), b"branch point data\n")
.expect("topic marker write should succeed");
fixture.git(["add", "."]);
fixture.git(["commit", "-m", "Prepare revision resolvers"]);
fixture.git(["branch", "topic"]);
fixture.git(["branch", "feature/x"]);
fs::write(fixture.root.join("head-only.txt"), b"current HEAD\n")
.expect("head marker write should succeed");
fixture.git(["add", "."]);
fixture.git(["commit", "-m", "Advance HEAD"]);
fixture
}
fn path(&self) -> &Path {
&self.root
}
fn git<I, S>(&self, args: I) -> Output
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let output = Command::new("git")
.args(args)
.current_dir(&self.root)
.output()
.expect("git should be executable in tests");
if output.status.success() {
output
} else {
panic!(
"git command failed with status {}: {}",
output.status,
String::from_utf8_lossy(&output.stderr)
);
}
}
fn git_ls_tree_recursive(&self, rev: &str) -> Vec<GitEntry> {
parse_ls_tree_entries(&self.git(["ls-tree", "-r", "-z", rev]).stdout)
}
fn git_show_path(&self, rev: &str, path: &[u8]) -> Vec<u8> {
let spec = format!("{rev}:{}", String::from_utf8_lossy(path));
self.git(["show", spec.as_str()]).stdout
}
fn git_rev_parse(&self, spec: &str) -> String {
String::from_utf8(self.git(["rev-parse", spec]).stdout)
.expect("rev-parse output should be valid UTF-8")
.trim()
.to_owned()
}
fn git_rev_parse_short(&self, spec: &str) -> String {
String::from_utf8(self.git(["rev-parse", "--short", spec]).stdout)
.expect("short rev-parse output should be valid UTF-8")
.trim()
.to_owned()
}
}
impl Drop for FixtureRepository {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.root);
}
}
struct MetadataFixtureRepository {
root: PathBuf,
submodule_root: PathBuf,
submodule_head: String,
}
impl MetadataFixtureRepository {
fn new() -> Self {
let root = unique_temp_path("timefs-task5-fixture");
fs::create_dir_all(&root).expect("fixture directory creation should succeed");
let mut fixture = Self {
root,
submodule_root: unique_temp_path("timefs-task5-submodule"),
submodule_head: String::new(),
};
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 metadata fixture\n")
.expect("README write should succeed");
fs::write(fixture.root.join("broken.txt"), b"broken blob contents\n")
.expect("broken blob write should succeed");
fs::write(fixture.root.join("run.sh"), b"#!/bin/sh\necho metadata\n")
.expect("script write should succeed");
symlink("run.sh", fixture.root.join("link-to-script"))
.expect("symlink creation should succeed");
let mut permissions = fs::metadata(fixture.root.join("run.sh"))
.expect("script metadata should be readable")
.permissions();
permissions.set_mode(0o755);
fs::set_permissions(fixture.root.join("run.sh"), permissions)
.expect("script permissions should be writable");
fixture.git(["add", "."]);
fixture.git(["commit", "-m", "Create metadata fixture"]);
let submodule_head = initialize_submodule_source(&fixture.submodule_root);
fixture.git_with_options(
["-c", "protocol.file.allow=always"],
[
"submodule",
"add",
fixture.submodule_root.to_string_lossy().as_ref(),
"vendor/submodule",
],
);
fixture.git(["add", ".gitmodules", "vendor/submodule"]);
fixture.git(["commit", "-m", "Add submodule placeholder"]);
fixture.submodule_head = submodule_head;
fixture.remove_loose_blob("HEAD:broken.txt");
fixture
}
fn path(&self) -> &Path {
&self.root
}
fn submodule_head(&self) -> &str {
&self.submodule_head
}
fn git<I, S>(&self, args: I) -> Output
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let output = Command::new("git")
.args(args)
.current_dir(&self.root)
.output()
.expect("git should be executable in tests");
if output.status.success() {
output
} else {
panic!(
"git command failed with status {}: {}",
output.status,
String::from_utf8_lossy(&output.stderr)
);
}
}
fn git_with_options<A, B, S1, S2>(&self, options: A, args: B) -> Output
where
A: IntoIterator<Item = S1>,
B: IntoIterator<Item = S2>,
S1: AsRef<OsStr>,
S2: AsRef<OsStr>,
{
let output = Command::new("git")
.args(options)
.args(args)
.current_dir(&self.root)
.output()
.expect("git should be executable in tests");
if output.status.success() {
output
} else {
panic!(
"git command failed with status {}: {}",
output.status,
String::from_utf8_lossy(&output.stderr)
);
}
}
fn git_rev_parse(&self, spec: &str) -> String {
String::from_utf8(self.git(["rev-parse", spec]).stdout)
.expect("rev-parse output should be valid UTF-8")
.trim()
.to_owned()
}
fn remove_loose_blob(&self, spec: &str) {
let oid = self.git_rev_parse(spec);
let (prefix, suffix) = oid.split_at(2);
let object_path = self
.root
.join(".git")
.join("objects")
.join(prefix)
.join(suffix);
assert!(
object_path.exists(),
"expected loose object at {}",
object_path.display()
);
fs::remove_file(&object_path).expect("loose blob removal should succeed");
}
}
impl Drop for MetadataFixtureRepository {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.root);
let _ = fs::remove_dir_all(&self.submodule_root);
}
}
struct LargeFixtureRepository {
root: PathBuf,
file_count: usize,
sample_paths: Vec<Vec<u8>>,
}
impl LargeFixtureRepository {
fn new() -> Self {
let root = unique_temp_path("timefs-task6-fixture");
fs::create_dir_all(&root).expect("fixture directory creation should succeed");
let fixture = Self {
root,
file_count: 64 * 24,
sample_paths: vec![
b"tree-00/file-00.txt".to_vec(),
b"tree-17/file-09.txt".to_vec(),
b"tree-63/file-23.txt".to_vec(),
],
};
fixture.git(["init", "-b", "main"]);
fixture.git(["config", "user.name", "Timefs Tests"]);
fixture.git(["config", "user.email", "timefs-tests@example.com"]);
for dir_index in 0..64_usize {
let dir = fixture.root.join(format!("tree-{dir_index:02}"));
fs::create_dir_all(&dir).expect("fixture subtree creation should succeed");
for file_index in 0..24_usize {
let contents = format!(
"tree={dir_index:02} file={file_index:02} {}\n",
"payload".repeat(32)
);
fs::write(dir.join(format!("file-{file_index:02}.txt")), contents)
.expect("fixture file write should succeed");
}
}
fixture.git(["add", "."]);
fixture.git(["commit", "-m", "Create large cache fixture"]);
fixture
}
fn path(&self) -> &Path {
&self.root
}
fn file_count(&self) -> usize {
self.file_count
}
fn sample_paths(&self) -> Vec<Vec<u8>> {
self.sample_paths.clone()
}
fn git<I, S>(&self, args: I) -> Output
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let output = Command::new("git")
.args(args)
.current_dir(&self.root)
.output()
.expect("git should be executable in tests");
if output.status.success() {
output
} else {
panic!(
"git command failed with status {}: {}",
output.status,
String::from_utf8_lossy(&output.stderr)
);
}
}
fn git_show_path(&self, rev: &str, path: &[u8]) -> Vec<u8> {
let spec = format!("{rev}:{}", String::from_utf8_lossy(path));
self.git(["show", spec.as_str()]).stdout
}
}
impl Drop for LargeFixtureRepository {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.root);
}
}
struct ShallowCloneFixtureRepository {
source: PathBuf,
clone: PathBuf,
}
impl ShallowCloneFixtureRepository {
fn new() -> Self {
let source = unique_temp_path("timefs-task7-shallow-source");
fs::create_dir_all(&source).expect("shallow source creation should succeed");
let clone = unique_temp_path("timefs-task7-shallow-clone");
let fixture = Self { source, clone };
fixture.git_source(["init", "-b", "main"]);
fixture.git_source(["config", "user.name", "Timefs Tests"]);
fixture.git_source(["config", "user.email", "timefs-tests@example.com"]);
fs::write(fixture.source.join("README.md"), b"shallow fixture v1\n")
.expect("initial shallow fixture write should succeed");
fixture.git_source(["add", "."]);
fixture.git_source(["commit", "-m", "Create shallow fixture"]);
fs::write(fixture.source.join("README.md"), b"shallow fixture v2\n")
.expect("second shallow fixture write should succeed");
fixture.git_source(["add", "."]);
fixture.git_source(["commit", "-m", "Advance shallow fixture"]);
fs::write(fixture.source.join("README.md"), b"shallow fixture v3\n")
.expect("third shallow fixture write should succeed");
fixture.git_source(["add", "."]);
fixture.git_source(["commit", "-m", "Advance shallow fixture again"]);
let source_url = format!("file://{}", fixture.source.display());
let output = Command::new("git")
.args([
"-c",
"protocol.file.allow=always",
"clone",
"--no-local",
"--depth",
"1",
source_url.as_str(),
fixture.clone.to_string_lossy().as_ref(),
])
.output()
.expect("git clone should be executable in tests");
if !output.status.success() {
panic!(
"git clone failed with status {}: {}",
output.status,
String::from_utf8_lossy(&output.stderr)
);
}
assert!(
fixture.clone.join(".git").join("shallow").exists(),
"expected a real shallow clone at {}",
fixture.clone.display()
);
fixture
}
fn path(&self) -> &Path {
&self.clone
}
fn git_source<I, S>(&self, args: I) -> Output
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
run_git_in(&self.source, args)
}
}
impl Drop for ShallowCloneFixtureRepository {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.source);
let _ = fs::remove_dir_all(&self.clone);
}
}
struct LfsFixtureRepository {
root: PathBuf,
resolved_contents: Vec<u8>,
asset_pointer: Vec<u8>,
missing_pointer: Vec<u8>,
}
impl LfsFixtureRepository {
fn new() -> Self {
let root = unique_temp_path("timefs-task7-lfs-fixture");
fs::create_dir_all(&root).expect("LFS fixture directory creation should succeed");
let resolved_contents = b"real lfs payload\n".to_vec();
let resolved_oid =
String::from("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef");
let missing_oid =
String::from("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
let asset_pointer = lfs_pointer_text(&resolved_oid, resolved_contents.len());
let missing_pointer = lfs_pointer_text(&missing_oid, 27);
let fixture = Self {
root,
resolved_contents,
asset_pointer: asset_pointer.clone(),
missing_pointer: missing_pointer.clone(),
};
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("asset.bin"), asset_pointer)
.expect("LFS pointer write should succeed");
fs::write(fixture.root.join("missing.bin"), missing_pointer)
.expect("missing LFS pointer write should succeed");
fixture.git(["add", "."]);
fixture.git(["commit", "-m", "Create LFS pointer fixture"]);
let lfs_object_dir = fixture
.root
.join(".git")
.join("lfs")
.join("objects")
.join(&resolved_oid[..2])
.join(&resolved_oid[2..4]);
fs::create_dir_all(&lfs_object_dir).expect("LFS object directory creation should succeed");
fs::write(
lfs_object_dir.join(&resolved_oid),
&fixture.resolved_contents,
)
.expect("LFS object write should succeed");
fixture
}
fn path(&self) -> &Path {
&self.root
}
fn git<I, S>(&self, args: I) -> Output
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
run_git_in(&self.root, args)
}
fn pointer_contents(&self, name: &str) -> Vec<u8> {
match name {
"asset.bin" => self.asset_pointer.clone(),
"missing.bin" => self.missing_pointer.clone(),
other => panic!("unexpected pointer file requested: {other}"),
}
}
fn resolved_contents(&self) -> Vec<u8> {
self.resolved_contents.clone()
}
}
impl Drop for LfsFixtureRepository {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.root);
}
}
#[derive(Debug)]
struct GitEntry {
mode: u32,
kind: String,
path: Vec<u8>,
}
fn parse_ls_tree_entries(output: &[u8]) -> Vec<GitEntry> {
output
.split(|byte| *byte == 0)
.filter(|record| !record.is_empty())
.map(parse_ls_tree_entry)
.collect()
}
fn parse_ls_tree_entry(record: &[u8]) -> GitEntry {
let tab = record
.iter()
.position(|byte| *byte == b'\t')
.expect("ls-tree records should contain a tab");
let (header, path) = record.split_at(tab);
let header = std::str::from_utf8(header).expect("ls-tree header should be UTF-8");
let mut parts = header.split_whitespace();
let mode = u32::from_str_radix(parts.next().expect("mode should exist"), 8)
.expect("mode should be valid octal");
let kind = parts.next().expect("kind should exist").to_owned();
let _oid = parts.next().expect("oid should exist");
GitEntry {
mode,
kind,
path: path.get(1..).unwrap_or_default().to_vec(),
}
}
fn spawn_mount(repo: &Path, mountpoint: &Path) -> Child {
spawn_mount_with_args(repo, mountpoint, &["-f"])
}
fn spawn_mount_with_args(repo: &Path, mountpoint: &Path, extra_args: &[&str]) -> Child {
spawn_mount_with_cli_and_mount_args(repo, mountpoint, &[], extra_args)
}
fn spawn_mount_with_cli_and_mount_args(
repo: &Path,
mountpoint: &Path,
cli_args: &[&str],
extra_args: &[&str],
) -> Child {
Command::new(timefs_binary())
.args(cli_args)
.arg("mount")
.arg(repo)
.arg(mountpoint)
.args(extra_args)
.stdout(Stdio::null())
.stderr(Stdio::piped())
.spawn()
.expect("timefs mount should spawn")
}
fn run_timefs<I, S>(args: I)
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let output = Command::new(timefs_binary())
.args(args)
.output()
.expect("timefs command should execute");
if !output.status.success() {
panic!(
"timefs command failed with status {}: {}",
output.status,
String::from_utf8_lossy(&output.stderr)
);
}
}
fn wait_for_mount_ready(child: &mut Child, mountpoint: &Path, expected_name: &[u8]) {
for _ in 0..120 {
if let Ok(entries) = fs::read_dir(mountpoint) {
let names: Vec<Vec<u8>> = entries
.filter_map(Result::ok)
.map(|entry| entry.file_name().into_vec())
.collect();
if names.iter().any(|name| name == expected_name) {
return;
}
}
if let Some(status) = child
.try_wait()
.expect("mount child status should be readable")
{
panic!(
"mount process exited early with status {}: {}",
status,
child_stderr(child)
);
}
thread::sleep(Duration::from_millis(50));
}
panic!(
"mountpoint did not become ready at {}",
mountpoint.display()
);
}
fn wait_for_mount_cleared(mountpoint: &Path) {
for _ in 0..120 {
if let Ok(entries) = fs::read_dir(mountpoint) {
if entries.count() == 0 {
return;
}
}
thread::sleep(Duration::from_millis(50));
}
panic!(
"mountpoint did not clear after unmount: {}",
mountpoint.display()
);
}
fn wait_for_process_exit(child: &mut Child, message: &str) {
let status = child.wait().expect("mount child status should be readable");
assert!(status.success(), "{message}: {}", child_stderr(child));
}
fn child_stderr(child: &mut Child) -> String {
let mut stderr = String::new();
if let Some(mut pipe) = child.stderr.take() {
let _ = pipe.read_to_string(&mut stderr);
}
stderr
}
fn run_git_in<I, S>(root: &Path, args: I) -> Output
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let output = Command::new("git")
.args(args)
.current_dir(root)
.output()
.expect("git should be executable in tests");
if output.status.success() {
output
} else {
panic!(
"git command failed with status {}: {}",
output.status,
String::from_utf8_lossy(&output.stderr)
);
}
}
fn lfs_pointer_text(oid: &str, size: usize) -> Vec<u8> {
format!("version https://git-lfs.github.com/spec/v1\noid sha256:{oid}\nsize {size}\n")
.into_bytes()
}
fn collect_mount_leaf_paths(root: &Path) -> Vec<Vec<u8>> {
let mut paths = Vec::new();
collect_mount_leaf_paths_recursive(root, root, &mut paths);
paths.sort();
paths
}
fn collect_mount_leaf_paths_recursive(root: &Path, dir: &Path, out: &mut Vec<Vec<u8>>) {
let mut entries: Vec<_> = fs::read_dir(dir)
.expect("mounted directory should be readable")
.filter_map(Result::ok)
.collect();
entries.sort_by(|left, right| left.file_name().cmp(&right.file_name()));
for entry in entries {
let path = entry.path();
let file_type = entry.file_type().expect("entry type should be readable");
if file_type.is_dir() {
collect_mount_leaf_paths_recursive(root, &path, out);
} else {
let relative = path
.strip_prefix(root)
.expect("path should remain under root")
.as_os_str()
.as_bytes()
.to_vec();
out.push(relative);
}
}
}
fn sorted_dir_entries(path: &Path) -> Vec<Vec<u8>> {
let mut names: Vec<Vec<u8>> = fs::read_dir(path)
.expect("directory should be readable")
.filter_map(Result::ok)
.map(|entry| entry.file_name().into_vec())
.collect();
names.sort();
names
}
fn send_sigint(pid: u32) {
let status = Command::new("kill")
.arg("-INT")
.arg(pid.to_string())
.status()
.expect("kill should be executable");
assert!(
status.success(),
"failed to deliver SIGINT to mount process"
);
}
fn initialize_submodule_source(root: &Path) -> String {
fs::create_dir_all(root).expect("submodule fixture directory creation should succeed");
let run_git = |args: &[&str]| -> Output {
let output = Command::new("git")
.args(args)
.current_dir(root)
.output()
.expect("git should be executable in tests");
if output.status.success() {
output
} else {
panic!(
"git command failed with status {}: {}",
output.status,
String::from_utf8_lossy(&output.stderr)
);
}
};
run_git(&["init", "-b", "main"]);
run_git(&["config", "user.name", "Timefs Tests"]);
run_git(&["config", "user.email", "timefs-tests@example.com"]);
fs::write(root.join("submodule.txt"), b"pinned submodule contents\n")
.expect("submodule contents write should succeed");
run_git(&["add", "."]);
run_git(&["commit", "-m", "Create submodule fixture"]);
String::from_utf8(run_git(&["rev-parse", "HEAD"]).stdout)
.expect("submodule rev-parse output should be valid UTF-8")
.trim()
.to_owned()
}
fn get_xattr(path: &Path, name: &str) -> Vec<u8> {
let path = path_cstring(path);
let name = CString::new(name).expect("xattr names should not contain NUL bytes");
let size = unsafe { libc::getxattr(path.as_ptr(), name.as_ptr(), std::ptr::null_mut(), 0) };
assert!(size >= 0, "getxattr size probe should succeed");
let size = usize::try_from(size).expect("xattr length should fit in memory");
let mut value = vec![0_u8; size];
let written = unsafe {
libc::getxattr(
path.as_ptr(),
name.as_ptr(),
value.as_mut_ptr().cast(),
value.len(),
)
};
assert_eq!(
usize::try_from(written).expect("written xattr size should be non-negative"),
value.len()
);
value
}
fn get_xattr_error(path: &Path, name: &str) -> i32 {
let path = path_cstring(path);
let name = CString::new(name).expect("xattr names should not contain NUL bytes");
let result = unsafe { libc::getxattr(path.as_ptr(), name.as_ptr(), std::ptr::null_mut(), 0) };
assert_eq!(result, -1, "missing xattrs should report an error");
last_errno()
}
fn get_xattr_small_buffer_error(path: &Path, name: &str, size: usize) -> i32 {
let path = path_cstring(path);
let name = CString::new(name).expect("xattr names should not contain NUL bytes");
let mut value = vec![0_u8; size];
let result = unsafe {
libc::getxattr(
path.as_ptr(),
name.as_ptr(),
value.as_mut_ptr().cast(),
value.len(),
)
};
assert_eq!(
result, -1,
"undersized xattr buffers should report an error"
);
last_errno()
}
fn list_xattrs(path: &Path) -> Vec<Vec<u8>> {
let path = path_cstring(path);
let size = unsafe { libc::listxattr(path.as_ptr(), std::ptr::null_mut(), 0) };
assert!(size >= 0, "listxattr size probe should succeed");
let size = usize::try_from(size).expect("xattr list length should fit in memory");
let mut value = vec![0_u8; size];
let written = unsafe { libc::listxattr(path.as_ptr(), value.as_mut_ptr().cast(), value.len()) };
assert_eq!(
usize::try_from(written).expect("written xattr list should be non-negative"),
value.len()
);
let mut names: Vec<Vec<u8>> = value
.split(|byte| *byte == 0)
.filter(|name| !name.is_empty())
.map(<[u8]>::to_vec)
.collect();
names.sort();
names
}
fn statvfs_free_blocks(path: &Path) -> (u64, u64) {
let path = path_cstring(path);
let mut stat = MaybeUninit::<libc::statvfs>::uninit();
let result = unsafe { libc::statvfs(path.as_ptr(), stat.as_mut_ptr()) };
assert_eq!(result, 0, "statvfs should succeed");
let stat = unsafe { stat.assume_init() };
(
u64::try_from(stat.f_bfree).expect("free block count should fit in u64"),
u64::try_from(stat.f_bavail).expect("available block count should fit in u64"),
)
}
fn path_cstring(path: &Path) -> CString {
CString::new(path.as_os_str().as_bytes()).expect("test paths should not contain NUL bytes")
}
fn last_errno() -> i32 {
std::io::Error::last_os_error()
.raw_os_error()
.unwrap_or_default()
}
fn create_temp_dir(prefix: &str) -> PathBuf {
let path = unique_temp_path(prefix);
fs::create_dir_all(&path).expect("temporary directory creation should succeed");
path
}
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();
let mut path = std::env::temp_dir();
path.push(OsString::from_vec(format!("{prefix}-{nanos}").into_bytes()));
path
}
fn timefs_binary() -> OsString {
OsString::from(env!("CARGO_BIN_EXE_timefs"))
}