fsys 1.1.0

Filesystem IO for Rust storage engines: journal substrate, io_uring, NVMe passthrough, atomic writes, cross-platform durability.
Documentation
//! Edge-case battery — every documented edge case has a test.
//!
//! Coverage:
//! - 0-byte files
//! - files at exactly the page-size boundary
//! - files at exactly one sector
//! - paths with Unicode + emoji
//! - paths with OS-special characters that aren't separators
//! - very long filenames (within filesystem limits)
//! - deeply nested directory trees
//! - back-to-back rename + write race (sequential)

use fsys::builder;
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};

static C: AtomicU64 = AtomicU64::new(0);

fn tmp_path(tag: &str) -> PathBuf {
    let n = C.fetch_add(1, Ordering::Relaxed);
    std::env::temp_dir().join(format!("fsys_edge_{}_{}_{}", std::process::id(), n, tag))
}

fn tmp_dir(tag: &str) -> PathBuf {
    let p = tmp_path(tag);
    std::fs::create_dir_all(&p).unwrap();
    p
}

struct CleanupFile(PathBuf);
impl Drop for CleanupFile {
    fn drop(&mut self) {
        let _ = std::fs::remove_file(&self.0);
    }
}

struct CleanupDir(PathBuf);
impl Drop for CleanupDir {
    fn drop(&mut self) {
        let _ = std::fs::remove_dir_all(&self.0);
    }
}

#[test]
fn zero_byte_write_then_read_round_trips() {
    let path = tmp_path("zero_byte");
    let _g = CleanupFile(path.clone());
    let fs = builder().build().expect("handle");
    fs.write(&path, b"").expect("write zero bytes");
    let read = fs.read(&path).expect("read zero bytes");
    assert!(read.is_empty());
    assert_eq!(fs.size(&path).expect("size"), 0);
}

#[test]
fn page_size_payload_round_trips() {
    let page = fsys::os::info().page_size.max(4096);
    let path = tmp_path("page_size");
    let _g = CleanupFile(path.clone());
    let fs = builder().build().expect("handle");

    let payload = vec![0xA5u8; page];
    fs.write(&path, &payload).expect("write");
    let read = fs.read(&path).expect("read");
    assert_eq!(read, payload);
}

#[test]
fn single_sector_payload_round_trips() {
    // 512 is the universal min sector; some drives report 4096 but
    // they always also accept 512-aligned smaller writes through
    // the buffered fallback path.
    let path = tmp_path("one_sector");
    let _g = CleanupFile(path.clone());
    let fs = builder().build().expect("handle");

    let payload = vec![0x5Au8; 512];
    fs.write(&path, &payload).expect("write");
    let read = fs.read(&path).expect("read");
    assert_eq!(read, payload);
}

#[test]
fn one_byte_payload_round_trips() {
    let path = tmp_path("one_byte");
    let _g = CleanupFile(path.clone());
    let fs = builder().build().expect("handle");
    fs.write(&path, b"X").expect("write");
    let read = fs.read(&path).expect("read");
    assert_eq!(read, b"X");
}

#[test]
fn unicode_and_emoji_paths_round_trip() {
    let dir = tmp_dir("unicode");
    let _g = CleanupDir(dir.clone());
    let fs = builder().build().expect("handle");

    // Stick to filename characters that all major platforms accept.
    let cases = [
        "héllo.txt",
        "日本語ファイル.txt",
        "café résumé.txt",
        // A couple of emoji that have stable rendering in tooling:
        "smile_😀.txt",
        "rocket_🚀.txt",
    ];

    for name in cases {
        let path = dir.join(name);
        fs.write(&path, name.as_bytes()).expect("write unicode");
        let read = fs.read(&path).expect("read unicode");
        assert_eq!(read, name.as_bytes());
    }
}

#[test]
fn deeply_nested_directory_tree_walks_clean() {
    let root = tmp_dir("deep");
    let _g = CleanupDir(root.clone());
    let fs = builder().build().expect("handle");

    let mut p = root.clone();
    for i in 0..20 {
        p = p.join(format!("d{i:02}"));
    }
    fs.mkdir_all(&p).expect("mkdir deep");
    fs.write(p.join("leaf.txt"), b"reached")
        .expect("write leaf");

    let scan = fs.scan_all(&root).expect("scan_all deep");
    // 20 directories + 1 file = 21 entries.
    assert!(scan.len() >= 21, "expected ≥21 entries, got {}", scan.len());

    let count = fs.count_all(&root).expect("count_all deep");
    assert_eq!(count, 1, "exactly one regular file in tree");
}

#[test]
fn long_filename_within_limits() {
    // Filesystems typically support 255-byte filenames, but
    // Windows's classic `MAX_PATH = 260` constrains the *full*
    // path including parent directories. We test 100 chars on the
    // filename — well within both Linux NAME_MAX and Windows
    // MAX_PATH given the temp-dir prefix overhead — to validate
    // the long-name path without tripping the path-length ceiling.
    let dir = tmp_dir("longname");
    let _g = CleanupDir(dir.clone());
    let fs = builder().build().expect("handle");

    let name: String = "a".repeat(100);
    let path = dir.join(format!("{name}.txt"));
    fs.write(&path, b"x").expect("write long-name");
    assert!(fs.exists(&path).expect("exists"));
}

#[test]
fn write_then_rename_then_write_again_atomic() {
    let dir = tmp_dir("rename_race");
    let _g = CleanupDir(dir.clone());
    let fs = builder().build().expect("handle");

    let a = dir.join("a.dat");
    let b = dir.join("b.dat");

    fs.write(&a, b"first").expect("write a");
    fs.rename(&a, &b).expect("rename a→b");
    assert!(!fs.exists(&a).expect("a gone"));
    assert!(fs.exists(&b).expect("b present"));

    fs.write(&a, b"second").expect("write a (post-rename)");
    let ra = fs.read(&a).expect("read a");
    let rb = fs.read(&b).expect("read b");
    assert_eq!(ra, b"second");
    assert_eq!(rb, b"first");
}

#[test]
fn write_copy_zero_byte_then_replace_round_trip() {
    let path = tmp_path("wcopy_zero");
    let _g = CleanupFile(path.clone());
    let fs = builder().build().expect("handle");

    fs.write(&path, b"").expect("create empty");
    fs.write_copy(&path, b"non-empty replacement")
        .expect("write_copy");
    assert_eq!(fs.read(&path).expect("read"), b"non-empty replacement");
}

#[test]
fn truncate_to_zero_then_extend() {
    let path = tmp_path("trunc_zero");
    let _g = CleanupFile(path.clone());
    let fs = builder().build().expect("handle");

    fs.write(&path, b"abcdefgh").expect("write");
    fs.truncate(&path, 0).expect("truncate to 0");
    assert_eq!(fs.size(&path).expect("size"), 0);

    fs.write(&path, b"new").expect("write again");
    assert_eq!(fs.read(&path).expect("read"), b"new");
}

#[test]
fn append_to_nonexistent_creates_file() {
    let path = tmp_path("append_create");
    let _g = CleanupFile(path.clone());
    let fs = builder().build().expect("handle");

    fs.append(&path, b"hello").expect("append (creates)");
    assert_eq!(fs.read(&path).expect("read"), b"hello");
}