#![forbid(unsafe_code)]
use tempfile::TempDir;
use crate::store::directory::{self};
use crate::store::inode::InodeData;
use crate::store::transaction::CrashHooks;
use crate::store::{NewEntry, Store, StoreConfig, StoreError};
fn create_store(dir: &TempDir) -> Store {
let cfg = StoreConfig {
segment_size: 1024 * 1024,
..Default::default()
};
Store::create(dir.path(), &cfg, [0x22; 16]).unwrap()
}
fn new_file_entry(mode: u32) -> NewEntry {
NewEntry::file(mode, 1000, 1000)
}
fn new_dir_entry(mode: u32) -> NewEntry {
NewEntry::dir(mode, 1000, 1000)
}
fn names(store: &Store, ino: u64) -> Vec<Vec<u8>> {
let (entries, _) = store.dir_scan(ino, None, usize::MAX).unwrap();
entries.into_iter().map(|(n, _)| n).collect()
}
#[test]
fn same_parent_rename_moves_entry_exactly_once() {
let dir = TempDir::new().unwrap();
let mut store = create_store(&dir);
store
.create_entry(1, b"a.lock", new_file_entry(0o644), &CrashHooks::none())
.unwrap();
let outcome = store
.rename(1, b"a.lock", 1, b"a", &CrashHooks::none())
.unwrap();
assert_eq!(outcome.replaced_dst_ino, None);
assert!(store.dir_lookup(1, b"a.lock").unwrap().is_none());
let dst = store.dir_lookup(1, b"a").unwrap().expect("dst exists");
assert_eq!(dst.ino, outcome.src_ino);
assert_eq!(names(&store, 1), vec![b"a".to_vec()]);
let inode = store.get_inode(dst.ino).unwrap().expect("inode alive");
assert_eq!(inode.nlink, 1);
assert!(inode.is_file());
}
#[test]
fn same_parent_rename_over_existing_replaces_and_drops_old_inode() {
let dir = TempDir::new().unwrap();
let mut store = create_store(&dir);
let src = store
.create_entry(1, b"new", new_file_entry(0o644), &CrashHooks::none())
.unwrap();
let old = store
.create_entry(1, b"old", new_file_entry(0o644), &CrashHooks::none())
.unwrap();
let outcome = store
.rename(1, b"new", 1, b"old", &CrashHooks::none())
.unwrap();
assert_eq!(outcome.replaced_dst_ino, Some(old));
assert_eq!(outcome.src_ino, src);
let entry = store.dir_lookup(1, b"old").unwrap().expect("dst");
assert_eq!(entry.ino, src);
assert_eq!(names(&store, 1), vec![b"old".to_vec()]);
assert!(store.get_inode(old).unwrap().is_none());
}
#[test]
fn same_parent_rename_onto_hardlink_preserves_nlink() {
let dir = TempDir::new().unwrap();
let mut store = create_store(&dir);
let ino = store
.create_entry(1, b"a", new_file_entry(0o644), &CrashHooks::none())
.unwrap();
store.link(1, b"b", ino, &CrashHooks::none()).unwrap();
assert_eq!(store.get_inode(ino).unwrap().unwrap().nlink, 2);
let outcome = store.rename(1, b"b", 1, b"a", &CrashHooks::none()).unwrap();
assert_eq!(outcome.replaced_dst_ino, None);
assert_eq!(names(&store, 1), vec![b"a".to_vec()]);
assert_eq!(store.get_inode(ino).unwrap().unwrap().nlink, 2);
}
#[test]
fn cross_parent_rename_moves_entry() {
let dir = TempDir::new().unwrap();
let mut store = create_store(&dir);
store
.create_entry(1, b"d1", new_dir_entry(0o755), &CrashHooks::none())
.unwrap();
store
.create_entry(1, b"d2", new_dir_entry(0o755), &CrashHooks::none())
.unwrap();
let ino = store
.create_entry(2, b"f", new_file_entry(0o644), &CrashHooks::none())
.unwrap();
let outcome = store.rename(2, b"f", 3, b"g", &CrashHooks::none()).unwrap();
assert_eq!(outcome.src_ino, ino);
assert!(store.dir_lookup(2, b"f").unwrap().is_none());
assert_eq!(store.dir_lookup(3, b"g").unwrap().unwrap().ino, ino);
}
#[test]
fn cross_parent_dir_rename_adjusts_parent_nlinks() {
let dir = TempDir::new().unwrap();
let mut store = create_store(&dir);
store
.create_entry(1, b"d1", new_dir_entry(0o755), &CrashHooks::none())
.unwrap();
store
.create_entry(1, b"d2", new_dir_entry(0o755), &CrashHooks::none())
.unwrap();
store
.create_entry(2, b"sub", new_dir_entry(0o755), &CrashHooks::none())
.unwrap();
let nlink_d1_before = store.get_inode(2).unwrap().unwrap().nlink;
let nlink_d2_before = store.get_inode(3).unwrap().unwrap().nlink;
store
.rename(2, b"sub", 3, b"sub", &CrashHooks::none())
.unwrap();
let nlink_d1_after = store.get_inode(2).unwrap().unwrap().nlink;
let nlink_d2_after = store.get_inode(3).unwrap().unwrap().nlink;
assert_eq!(nlink_d1_after, nlink_d1_before - 1);
assert_eq!(nlink_d2_after, nlink_d2_before + 1);
let sub = store.dir_lookup(3, b"sub").unwrap().unwrap();
assert_eq!(store.get_inode(sub.ino).unwrap().unwrap().nlink, 2);
}
#[test]
fn rmdir_removes_directory_inode() {
let dir = TempDir::new().unwrap();
let mut store = create_store(&dir);
let ino = store
.create_entry(1, b"d", new_dir_entry(0o755), &CrashHooks::none())
.unwrap();
assert!(store.get_inode(ino).unwrap().is_some());
store.unlink(1, b"d", true, &CrashHooks::none()).unwrap();
assert!(store.dir_lookup(1, b"d").unwrap().is_none());
assert!(store.get_inode(ino).unwrap().is_none());
assert_eq!(store.get_inode(1).unwrap().unwrap().nlink, 2);
}
#[test]
fn unlink_file_drops_inode_at_zero_links() {
let dir = TempDir::new().unwrap();
let mut store = create_store(&dir);
let ino = store
.create_entry(1, b"f", new_file_entry(0o644), &CrashHooks::none())
.unwrap();
store.unlink(1, b"f", false, &CrashHooks::none()).unwrap();
assert!(store.get_inode(ino).unwrap().is_none());
}
#[test]
fn unlink_one_hardlink_keeps_inode() {
let dir = TempDir::new().unwrap();
let mut store = create_store(&dir);
let ino = store
.create_entry(1, b"a", new_file_entry(0o644), &CrashHooks::none())
.unwrap();
store.link(1, b"b", ino, &CrashHooks::none()).unwrap();
store.unlink(1, b"a", false, &CrashHooks::none()).unwrap();
let inode = store.get_inode(ino).unwrap().expect("still linked");
assert_eq!(inode.nlink, 1);
assert_eq!(store.dir_lookup(1, b"b").unwrap().unwrap().ino, ino);
}
#[test]
fn rename_rejects_nonempty_dir_over_dir() {
let dir = TempDir::new().unwrap();
let mut store = create_store(&dir);
store
.create_entry(1, b"src", new_dir_entry(0o755), &CrashHooks::none())
.unwrap();
store
.create_entry(1, b"dst", new_dir_entry(0o755), &CrashHooks::none())
.unwrap();
store
.create_entry(3, b"kid", new_file_entry(0o644), &CrashHooks::none())
.unwrap();
let err = store
.rename(1, b"src", 1, b"dst", &CrashHooks::none())
.unwrap_err();
assert!(matches!(err, StoreError::Invariant(m) if m == "directory not empty"));
}
#[test]
fn rename_rejects_type_mismatches() {
let dir = TempDir::new().unwrap();
let mut store = create_store(&dir);
store
.create_entry(1, b"f", new_file_entry(0o644), &CrashHooks::none())
.unwrap();
store
.create_entry(1, b"d", new_dir_entry(0o755), &CrashHooks::none())
.unwrap();
let err = store
.rename(1, b"f", 1, b"d", &CrashHooks::none())
.unwrap_err();
assert!(matches!(err, StoreError::Invariant(m) if m == "cannot rename file over dir"));
let err = store
.rename(1, b"d", 1, b"f", &CrashHooks::none())
.unwrap_err();
assert!(matches!(err, StoreError::Invariant(m) if m == "cannot rename dir over file"));
}
#[test]
fn rename_missing_source_errors() {
let dir = TempDir::new().unwrap();
let mut store = create_store(&dir);
let err = store
.rename(1, b"nope", 1, b"x", &CrashHooks::none())
.unwrap_err();
assert!(matches!(err, StoreError::Invariant(m) if m == "no such entry"));
}
#[test]
fn rename_noop_is_successful_and_preserves_state() {
let dir = TempDir::new().unwrap();
let mut store = create_store(&dir);
store
.create_entry(1, b"a", new_file_entry(0o644), &CrashHooks::none())
.unwrap();
let outcome = store.rename(1, b"a", 1, b"a", &CrashHooks::none()).unwrap();
assert_eq!(outcome.replaced_dst_ino, None);
assert_eq!(names(&store, 1), vec![b"a".to_vec()]);
assert!(store.dir_lookup(1, b"a").unwrap().is_some());
}
#[test]
fn rename_over_empty_dir_succeeds() {
let dir = TempDir::new().unwrap();
let mut store = create_store(&dir);
store
.create_entry(1, b"src", new_dir_entry(0o755), &CrashHooks::none())
.unwrap();
store
.create_entry(1, b"dst", new_dir_entry(0o755), &CrashHooks::none())
.unwrap();
let outcome = store
.rename(1, b"src", 1, b"dst", &CrashHooks::none())
.unwrap();
assert_eq!(outcome.src_ino, 2);
assert_eq!(outcome.replaced_dst_ino, Some(3));
let entry = store.dir_lookup(1, b"dst").unwrap().unwrap();
assert_eq!(entry.ino, 2);
assert!(store.get_inode(3).unwrap().is_none());
}
#[test]
fn git_lock_dance_reproduces_cleanly() {
let dir = TempDir::new().unwrap();
let mut store = create_store(&dir);
store
.create_entry(1, b".git", new_dir_entry(0o755), &CrashHooks::none())
.unwrap();
store
.create_entry(2, b"config", new_file_entry(0o644), &CrashHooks::none())
.unwrap();
for _ in 0..2 {
let lock = store
.create_entry(
2,
b"config.lock",
new_file_entry(0o644),
&CrashHooks::none(),
)
.unwrap();
let outcome = store
.rename(2, b"config.lock", 2, b"config", &CrashHooks::none())
.unwrap();
assert_eq!(outcome.replaced_dst_ino, Some(lock - 1));
assert!(store.dir_lookup(2, b"config.lock").unwrap().is_none());
let names = names(&store, 2);
assert_eq!(names, vec![b"config".to_vec()]);
let cfg = store.dir_lookup(2, b"config").unwrap().unwrap();
assert!(store.get_inode(cfg.ino).unwrap().is_some());
}
store
.unlink(2, b"config", false, &CrashHooks::none())
.unwrap();
assert!(store.get_inode(3).unwrap().is_none());
store.unlink(1, b".git", true, &CrashHooks::none()).unwrap();
assert!(store.get_inode(2).unwrap().is_none());
assert_eq!(names(&store, 1), Vec::<Vec<u8>>::new());
}
#[test]
fn directory_tree_invariants_after_rename_stress() {
let dir = TempDir::new().unwrap();
let mut store = create_store(&dir);
let a = store
.create_entry(1, b"a", new_file_entry(0o644), &CrashHooks::none())
.unwrap();
let b = store
.create_entry(1, b"b", new_file_entry(0o644), &CrashHooks::none())
.unwrap();
let d = store
.create_entry(1, b"d", new_dir_entry(0o755), &CrashHooks::none())
.unwrap();
let _ = b;
store.rename(1, b"a", 1, b"c", &CrashHooks::none()).unwrap();
store.rename(1, b"b", 1, b"c", &CrashHooks::none()).unwrap();
assert!(store.get_inode(a).unwrap().is_none());
store
.rename(1, b"c", d, b"moved", &CrashHooks::none())
.unwrap();
assert!(store.dir_lookup(1, b"c").unwrap().is_none());
store
.rename(d, b"moved", 1, b"c", &CrashHooks::none())
.unwrap();
let names = names(&store, 1);
assert_eq!(names, vec![b"c".to_vec(), b"d".to_vec()]);
let c = store.dir_lookup(1, b"c").unwrap().unwrap();
assert!(store.get_inode(c.ino).unwrap().unwrap().is_file());
let d2 = store.dir_lookup(1, b"d").unwrap().unwrap();
assert!(store.get_inode(d2.ino).unwrap().unwrap().is_dir());
let root = match store.get_inode(1).unwrap().unwrap().data {
InodeData::Directory { dir_root } => dir_root,
_ => unreachable!(),
};
let limit = store.limits();
let (scan, _) = directory::scan(root, None, 100, 64, limit.max_fanout, &store).unwrap();
assert_eq!(scan.len(), 2);
assert_eq!(scan[0].0, b"c");
assert_eq!(scan[1].0, b"d");
}