liteboxfs 0.2.0

A modern POSIX filesystem in a SQLite database
Documentation
mod testing;

use std::{io::prelude::*, path::Path};

use liteboxfs::{Connection, CreateOptions, Error, FileBy, FileKind, FileOrigin, Owner};
use testing::{Case, random_string};
use xpct::{be_err, be_ok, equal, expect, match_pattern, pattern};

#[test]
fn link_target_not_found() -> liteboxfs::Result<()> {
    let mut conn = Connection::open_in_memory(&CreateOptions::new())?;

    conn.exec(|fs| {
        expect!(fs.open("original.txt")).to(be_err()).to(match_pattern(
            pattern!(Error::FileNotFound { file: FileOrigin::Litebox { locator, .. }, .. } if locator == &FileBy::from("/original.txt")),
        ));

        let mut file = fs.create("original.txt", FileKind::Regular, Owner::ROOT)?;
        expect!(file.link("link.txt")).to(be_ok());

        liteboxfs::Result::Ok(())
    })?;

    Ok(())
}

#[test]
fn file_already_exists_at_link() -> liteboxfs::Result<()> {
    let mut conn = Connection::open_in_memory(&CreateOptions::new())?;

    conn.exec(|fs| {
        fs.create("original.txt", FileKind::Regular, Owner::ROOT)?;
        fs.create("link.txt", FileKind::Regular, Owner::ROOT)?;

        let mut file = fs.open("original.txt")?;
        expect!(file.link("link.txt"))
            .to(be_err())
            .to(match_pattern(
                pattern!(Error::FileAlreadyExists { path: FileOrigin::Litebox { locator, .. }, .. } if locator == Path::new("/link.txt")),
            ));

        liteboxfs::Result::Ok(())
    })?;

    Ok(())
}

#[test]
fn link_has_no_parent_directory() -> liteboxfs::Result<()> {
    let mut conn = Connection::open_in_memory(&CreateOptions::new())?;

    conn.exec(|fs| {
        let mut file = fs.create("original.txt", FileKind::Regular, Owner::ROOT)?;

        expect!(file.link("/nonexistent/link.txt"))
            .to(be_err())
            .to(match_pattern(
                pattern!(Error::NoParentDirectory { file: FileOrigin::Litebox { locator, .. }, .. } if locator == &FileBy::from("/nonexistent/link.txt")),
            ));

        liteboxfs::Result::Ok(())
    })?;

    Ok(())
}

#[test]
fn link_parent_is_not_a_directory() -> liteboxfs::Result<()> {
    let mut conn = Connection::open_in_memory(&CreateOptions::new())?;

    conn.exec(|fs| {
        fs.create("original.txt", FileKind::Regular, Owner::ROOT)?;
        fs.create("regular.txt", FileKind::Regular, Owner::ROOT)?;

        let mut file = fs.open("original.txt")?;
        expect!(file.link("regular.txt/link.txt"))
            .to(be_err())
            .to(match_pattern(
                pattern!(Error::NotADirectory { file: FileOrigin::Litebox { locator, .. }, .. } if locator == &FileBy::from("/regular.txt")),
            ));

        liteboxfs::Result::Ok(())
    })?;

    Ok(())
}

#[test]
fn links_have_same_file_id() -> liteboxfs::Result<()> {
    let mut conn = Connection::open_in_memory(&CreateOptions::new())?;

    conn.exec(|fs| {
        fs.create("original.txt", FileKind::Regular, Owner::ROOT)?;
        fs.open("original.txt")?.link("link.txt")?;

        let original_id = fs.open("original.txt")?.file_id();
        let link_id = fs.open("link.txt")?.file_id();

        expect!(original_id).to(equal(link_id));

        liteboxfs::Result::Ok(())
    })?;

    Ok(())
}

#[test]
fn links_have_same_content() -> liteboxfs::Result<()> {
    let mut conn = Connection::open_in_memory(&CreateOptions::new())?;

    conn.exec(|fs| {
        let expected = random_string(32, Case::Lower);
        fs.create("original.txt", FileKind::Regular, Owner::ROOT)?;

        fs.open("original.txt")?.link("link.txt")?;

        let mut original_file = fs.open("original.txt")?;
        original_file.write_all(expected.as_bytes())?;
        drop(original_file);

        let mut link_file = fs.open("link.txt")?;

        let mut actual = String::new();
        link_file.read_to_string(&mut actual)?;

        expect!(actual).to(equal(expected));

        liteboxfs::Result::Ok(())
    })?;

    Ok(())
}

#[test]
fn links_have_same_content_id() -> liteboxfs::Result<()> {
    let mut conn = Connection::open_in_memory(&CreateOptions::new())?;

    conn.exec(|fs| {
        let expected = random_string(32, Case::Lower);
        fs.create("original.txt", FileKind::Regular, Owner::ROOT)?;

        fs.open("original.txt")?.link("link.txt")?;

        let mut original_file = fs.open("original.txt")?;
        original_file.write_all(expected.as_bytes())?;
        drop(original_file);

        let original_content_id = fs.open("original.txt")?.content_id()?;
        let link_content_id = fs.open("link.txt")?.content_id()?;

        expect!(original_content_id).to(equal(link_content_id));

        liteboxfs::Result::Ok(())
    })?;

    Ok(())
}

#[test]
fn link_count_of_file_with_no_links() -> liteboxfs::Result<()> {
    let mut conn = Connection::open_in_memory(&CreateOptions::new())?;

    conn.exec(|fs| {
        fs.create("file.txt", FileKind::Regular, Owner::ROOT)?;

        expect!(fs.open("file.txt")?.link_count())
            .to(be_ok())
            .to(equal(1));

        liteboxfs::Result::Ok(())
    })?;

    Ok(())
}

#[test]
fn link_count_increases_with_each_link() -> liteboxfs::Result<()> {
    let mut conn = Connection::open_in_memory(&CreateOptions::new())?;

    conn.exec(|fs| {
        fs.create("original.txt", FileKind::Regular, Owner::ROOT)?;

        expect!(fs.open("original.txt")?.link_count())
            .to(be_ok())
            .to(equal(1));

        fs.open("original.txt")?.link("link1.txt")?;
        expect!(fs.open("original.txt")?.link_count())
            .to(be_ok())
            .to(equal(2));

        fs.open("original.txt")?.link("link2.txt")?;
        expect!(fs.open("original.txt")?.link_count())
            .to(be_ok())
            .to(equal(3));

        liteboxfs::Result::Ok(())
    })?;

    Ok(())
}

#[test]
fn link_count_is_same_from_any_path() -> liteboxfs::Result<()> {
    let mut conn = Connection::open_in_memory(&CreateOptions::new())?;

    conn.exec(|fs| {
        fs.create("original.txt", FileKind::Regular, Owner::ROOT)?;
        fs.open("original.txt")?.link("link.txt")?;

        let original_count = fs.open("original.txt")?.link_count()?;
        let link_count = fs.open("link.txt")?.link_count()?;

        expect!(original_count).to(equal(link_count));

        liteboxfs::Result::Ok(())
    })?;

    Ok(())
}

#[test]
fn link_count_decreases_after_delete() -> liteboxfs::Result<()> {
    let mut conn = Connection::open_in_memory(&CreateOptions::new())?;

    conn.exec(|fs| {
        fs.create("original.txt", FileKind::Regular, Owner::ROOT)?;
        fs.open("original.txt")?.link("link.txt")?;

        expect!(fs.open("link.txt")?.link_count())
            .to(be_ok())
            .to(equal(2));

        fs.delete("original.txt")?;

        expect!(fs.open("link.txt")?.link_count())
            .to(be_ok())
            .to(equal(1));

        liteboxfs::Result::Ok(())
    })?;

    Ok(())
}

#[test]
fn link_count_by_file_id() -> liteboxfs::Result<()> {
    let mut conn = Connection::open_in_memory(&CreateOptions::new())?;

    conn.exec(|fs| {
        fs.create("original.txt", FileKind::Regular, Owner::ROOT)?;
        fs.open("original.txt")?.link("link.txt")?;

        let file_id = fs.open("original.txt")?.file_id();

        expect!(fs.open(file_id)?.link_count())
            .to(be_ok())
            .to(equal(2));

        liteboxfs::Result::Ok(())
    })?;

    Ok(())
}

#[test]
fn open_by_id_on_hard_linked_file() -> liteboxfs::Result<()> {
    // This is a regression test. Opening a hard-linked file by its file ID at one point failed
    // with a SQLite "Query returned more than one row" error.
    let mut conn = Connection::open_in_memory(&CreateOptions::new())?;

    conn.exec(|fs| {
        fs.create("original.txt", FileKind::Regular, Owner::ROOT)?;
        fs.open("original.txt")?.link("link.txt")?;

        let file_id = fs.open("original.txt")?.file_id();

        expect!(fs.open(file_id)).to(be_ok());

        liteboxfs::Result::Ok(())
    })?;

    Ok(())
}