liteboxfs 0.2.0

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

#[cfg(all(feature = "compression", feature = "chunking"))]
mod compression {
    use std::io::{Read, Seek, SeekFrom, Write};

    use xpct::{eq_diff, expect};

    use super::testing::{Case, random_string};
    use liteboxfs::{
        Connection, CreateOptions,
        metadata::{FileKind, Owner},
    };

    // Use a small block size so tests don't need huge allocations to span multiple blocks.
    const BLOCK_SIZE: usize = 16 * 1024;

    // Use content large enough to fill several blocks.
    const CONTENT_SIZE: usize = BLOCK_SIZE * 6;

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

        let data_a = random_string(CONTENT_SIZE, Case::Lower);
        let data_b = random_string(CONTENT_SIZE, Case::Lower);

        conn.exec(|fs| {
            // Write file A with compression enabled.
            fs.set_compression(true);

            let mut file_a = fs.create("a.txt", FileKind::Regular, Owner::ROOT)?;
            file_a.write_all(data_a.as_bytes())?;
            drop(file_a);

            // Disable compression and write file B.
            fs.set_compression(false);

            let mut file_b = fs.create("b.txt", FileKind::Regular, Owner::ROOT)?;
            file_b.write_all(data_b.as_bytes())?;
            drop(file_b);

            // Both files must read back correctly regardless of current compression setting.
            let actual_a = {
                let mut actual = String::new();
                let mut file = fs.open("a.txt")?;
                file.read_to_string(&mut actual)?;
                actual
            };
            expect!(actual_a).to(eq_diff(data_a));

            let actual_b = {
                let mut actual = String::new();
                let mut file = fs.open("b.txt")?;
                file.read_to_string(&mut actual)?;
                actual
            };
            expect!(actual_b).to(eq_diff(data_b));

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

        Ok(())
    }

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

        let data = random_string(CONTENT_SIZE, Case::Lower);

        conn.exec(|fs| {
            // Write file without compression.
            let mut file = fs.create("test.txt", FileKind::Regular, Owner::ROOT)?;
            file.write_all(data.as_bytes())?;
            drop(file);

            // Enable compression, then read the file written without compression.
            fs.set_compression(true);

            let mut actual = String::new();
            let mut file = fs.open("test.txt")?;
            file.read_to_string(&mut actual)?;
            expect!(actual).to(eq_diff(data));

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

        Ok(())
    }

    #[test]
    fn write_with_compression_enabled_reads_back_correctly() -> liteboxfs::Result<()> {
        // Verifies that writes made when compression is enabled round-trip correctly
        // even after re-opening with compression disabled.
        let mut conn = Connection::open_in_memory(&CreateOptions::new().block_size(BLOCK_SIZE))?;

        let data = random_string(CONTENT_SIZE, Case::Lower);

        conn.exec(|fs| {
            fs.set_compression(true);

            let mut file = fs.create("test.txt", FileKind::Regular, Owner::ROOT)?;
            file.write_all(data.as_bytes())?;
            drop(file);

            // Disable compression and re-read — compressed blocks must still be decoded.
            fs.set_compression(false);

            let mut actual = String::new();
            let mut file = fs.open("test.txt")?;
            file.read_to_string(&mut actual)?;
            expect!(actual).to(eq_diff(data));

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

        Ok(())
    }

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

        let data_v1 = random_string(CONTENT_SIZE, Case::Lower);
        let data_v2 = random_string(CONTENT_SIZE, Case::Lower);

        conn.exec(|fs| {
            // Write v1 without compression.
            let mut file = fs.create("test.txt", FileKind::Regular, Owner::ROOT)?;
            file.write_all(data_v1.as_bytes())?;
            drop(file);

            // Enable compression and overwrite the file.
            fs.set_compression(true);

            let mut file = fs.open("test.txt")?;
            file.seek(SeekFrom::Start(0))?;
            file.write_all(data_v2.as_bytes())?;
            drop(file);

            // Disable compression and read back.
            fs.set_compression(false);

            let mut actual = String::new();
            let mut file = fs.open("test.txt")?;
            file.read_to_string(&mut actual)?;
            expect!(actual).to(eq_diff(data_v2));

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

        Ok(())
    }

    #[test]
    fn switching_compression_mid_write_produces_correct_data() -> liteboxfs::Result<()> {
        // Write some blocks compressed, some uncompressed; the full file must read back correctly.
        //
        // The file handle borrows fs mutably, so we must drop it before changing settings and
        // reopen with a seek to end to append the second half.
        let mut conn = Connection::open_in_memory(&CreateOptions::new().block_size(BLOCK_SIZE))?;

        let first_half = random_string(CONTENT_SIZE / 2, Case::Lower);
        let second_half = random_string(CONTENT_SIZE / 2, Case::Lower);

        conn.exec(|fs| {
            fs.set_compression(true);

            let mut file = fs.create("test.txt", FileKind::Regular, Owner::ROOT)?;
            file.write_all(first_half.as_bytes())?;
            drop(file);

            fs.set_compression(false);

            let mut file = fs.open("test.txt")?;
            file.seek(SeekFrom::End(0))?;
            file.write_all(second_half.as_bytes())?;
            drop(file);

            let mut actual = String::new();
            let mut file = fs.open("test.txt")?;
            file.read_to_string(&mut actual)?;

            let expected = format!("{first_half}{second_half}");
            expect!(actual).to(eq_diff(expected));

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

        Ok(())
    }
}

#[cfg(all(feature = "compression", feature = "chunking"))]
mod chunking {
    use std::io::{Read, Seek, SeekFrom, Write};

    use xpct::{eq_diff, expect};

    use super::testing::{Case, random_string};
    use liteboxfs::{
        Connection, CreateOptions,
        metadata::{FileKind, Owner},
    };

    const BLOCK_SIZE: usize = 16 * 1024;
    const CONTENT_SIZE: usize = BLOCK_SIZE * 6;

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

        let data = random_string(CONTENT_SIZE, Case::Lower);

        conn.exec(|fs| {
            // Write with CDC enabled.
            fs.set_chunking(true);

            let mut file = fs.create("test.txt", FileKind::Regular, Owner::ROOT)?;
            file.write_all(data.as_bytes())?;
            drop(file);

            // Switch to fixed chunking and read — existing CDC blocks must still be read.
            fs.set_chunking(false);

            let mut actual = String::new();
            let mut file = fs.open("test.txt")?;
            file.read_to_string(&mut actual)?;
            expect!(actual).to(eq_diff(data));

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

        Ok(())
    }

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

        let data = random_string(CONTENT_SIZE, Case::Lower);

        conn.exec(|fs| {
            // Write with fixed chunking.
            let mut file = fs.create("test.txt", FileKind::Regular, Owner::ROOT)?;
            file.write_all(data.as_bytes())?;
            drop(file);

            // Switch to CDC and read — existing fixed blocks must still be read correctly.
            fs.set_chunking(true);

            let mut actual = String::new();
            let mut file = fs.open("test.txt")?;
            file.read_to_string(&mut actual)?;
            expect!(actual).to(eq_diff(data));

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

        Ok(())
    }

    #[test]
    fn switching_chunking_mid_write_produces_correct_data() -> liteboxfs::Result<()> {
        // Write the first half with CDC and the second half with fixed chunking.
        //
        // The file handle borrows fs mutably, so we must drop it before changing settings and
        // reopen with a seek to end to append the second half.
        let mut conn = Connection::open_in_memory(&CreateOptions::new().block_size(BLOCK_SIZE))?;

        let first_half = random_string(CONTENT_SIZE / 2, Case::Lower);
        let second_half = random_string(CONTENT_SIZE / 2, Case::Lower);

        conn.exec(|fs| {
            fs.set_chunking(true);

            let mut file = fs.create("test.txt", FileKind::Regular, Owner::ROOT)?;
            file.write_all(first_half.as_bytes())?;
            drop(file);

            fs.set_chunking(false);

            let mut file = fs.open("test.txt")?;
            file.seek(SeekFrom::End(0))?;
            file.write_all(second_half.as_bytes())?;
            drop(file);

            let mut actual = String::new();
            let mut file = fs.open("test.txt")?;
            file.read_to_string(&mut actual)?;

            let expected = format!("{first_half}{second_half}");
            expect!(actual).to(eq_diff(expected));

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

        Ok(())
    }
}