sqlarfs 0.1.1

A file archive format and virtual filesystem backed by a SQLite database
Documentation
#![cfg(feature = "reference-conformance-tests")]

mod common;

use std::env;
use std::fs;
use std::io::prelude::*;
use std::path::Path;
use std::process;

use common::dump_table;
use common::have_same_contents;
use common::have_same_mtime;
use common::have_same_permissions;
use common::have_same_symlink_target;
use serial_test::serial;
use sqlarfs::Connection;
use sqlarfs::FileMode;
use xpct::{consist_of, expect};

fn sqlar_command(db: &Path, args: &[&str]) -> sqlarfs::Result<()> {
    let output = process::Command::new("sqlite3")
        .arg("-A")
        .arg("--file")
        .arg(db.to_string_lossy().as_ref())
        .args(args)
        .output()?;

    if !output.status.success() {
        panic!(
            "Failed executing sqlite3 command:\n{}",
            String::from_utf8_lossy(&output.stderr)
        );
    }

    Ok(())
}

//
// These tests need to change the process's working directory to ensure the paths in the SQLite
// archive are all relative paths. To prevent race conditions, that means they must be run
// serially.
//

#[test]
#[serial(change_directory)]
fn archive_empty_regular_file() -> sqlarfs::Result<()> {
    let db_dir = tempfile::tempdir()?;
    let reference_db = db_dir.path().join("reference.sqlar");
    let crate_db = db_dir.path().join("crate.sqlar");

    let temp_dir = tempfile::tempdir()?;
    fs::File::create(temp_dir.path().join("file"))?;

    env::set_current_dir(temp_dir.path())?;

    sqlar_command(&reference_db, &["--create", "file"])?;

    Connection::create_new(&crate_db)?.exec(|archive| {
        let opts = sqlarfs::ArchiveOptions::new().children(true);
        archive.archive_with(temp_dir.path(), "", &opts)
    })?;

    expect!(dump_table(&crate_db)?).to(consist_of(dump_table(&reference_db)?));

    Ok(())
}

#[test]
#[serial(change_directory)]
fn archive_regular_file_with_data() -> sqlarfs::Result<()> {
    let db_dir = tempfile::tempdir()?;
    let reference_db = db_dir.path().join("reference.sqlar");
    let crate_db = db_dir.path().join("crate.sqlar");

    let temp_dir = tempfile::tempdir()?;
    let mut file = fs::File::create(temp_dir.path().join("file"))?;

    write!(&mut file, "file contents")?;
    file.sync_all()?;

    env::set_current_dir(temp_dir.path())?;

    sqlar_command(&reference_db, &["--create", "file"])?;

    Connection::create_new(&crate_db)?.exec(|archive| {
        let opts = sqlarfs::ArchiveOptions::new().children(true);
        archive.archive_with(temp_dir.path(), "", &opts)
    })?;

    expect!(dump_table(&crate_db)?).to(consist_of(dump_table(&reference_db)?));

    Ok(())
}

#[test]
#[serial(change_directory)]
#[cfg(unix)]
fn archive_symlink() -> sqlarfs::Result<()> {
    use std::os::unix::fs::symlink;

    let db_dir = tempfile::tempdir()?;
    let reference_db = db_dir.path().join("reference.sqlar");
    let crate_db = db_dir.path().join("crate.sqlar");

    let temp_dir = tempfile::tempdir()?;
    let symlink_target = tempfile::NamedTempFile::new()?;
    symlink(symlink_target.path(), temp_dir.path().join("symlink"))?;

    env::set_current_dir(temp_dir.path())?;

    sqlar_command(&reference_db, &["--create", "symlink"])?;

    Connection::create_new(&crate_db)?.exec(|archive| {
        let opts = sqlarfs::ArchiveOptions::new().children(true);
        archive.archive_with(temp_dir.path(), "", &opts)
    })?;

    expect!(dump_table(&crate_db)?).to(consist_of(dump_table(&reference_db)?));

    Ok(())
}

#[test]
#[serial(change_directory)]
fn archive_empty_directory() -> sqlarfs::Result<()> {
    let db_dir = tempfile::tempdir()?;
    let reference_db = db_dir.path().join("reference.sqlar");
    let crate_db = db_dir.path().join("crate.sqlar");

    let temp_dir = tempfile::tempdir()?;
    fs::create_dir_all(temp_dir.path().join("source/dir"))?;

    env::set_current_dir(temp_dir.path())?;

    sqlar_command(&reference_db, &["--create", "source"])?;

    Connection::create_new(&crate_db)?.exec(|archive| {
        let opts = sqlarfs::ArchiveOptions::new().children(true);
        archive.archive_with(temp_dir.path(), "", &opts)
    })?;

    expect!(dump_table(&crate_db)?).to(consist_of(dump_table(&reference_db)?));

    Ok(())
}

#[test]
#[serial(change_directory)]
fn archive_directory_with_children() -> sqlarfs::Result<()> {
    let db_dir = tempfile::tempdir()?;
    let reference_db = db_dir.path().join("reference.sqlar");
    let crate_db = db_dir.path().join("crate.sqlar");

    let temp_dir = tempfile::tempdir()?;
    fs::create_dir_all(temp_dir.path().join("source/dir"))?;
    fs::File::create(temp_dir.path().join("source/file1"))?;
    fs::File::create(temp_dir.path().join("source/dir/file2"))?;

    env::set_current_dir(temp_dir.path())?;

    sqlar_command(&reference_db, &["--create", "source"])?;

    Connection::create_new(&crate_db)?.exec(|archive| {
        let opts = sqlarfs::ArchiveOptions::new().children(true);
        archive.archive_with(temp_dir.path(), "", &opts)
    })?;

    expect!(dump_table(&crate_db)?).to(consist_of(dump_table(&reference_db)?));

    Ok(())
}

#[test]
#[serial(change_directory)]
fn archive_regular_file_with_readonly_permissions() -> sqlarfs::Result<()> {
    let db_dir = tempfile::tempdir()?;
    let reference_db = db_dir.path().join("reference.sqlar");
    let crate_db = db_dir.path().join("crate.sqlar");

    let temp_dir = tempfile::tempdir()?;
    let file = fs::File::create(temp_dir.path().join("file"))?;

    let mut permissions = file.metadata()?.permissions();
    permissions.set_readonly(true);
    file.set_permissions(permissions)?;

    env::set_current_dir(temp_dir.path())?;

    sqlar_command(&reference_db, &["--create", "file"])?;

    Connection::create_new(&crate_db)?.exec(|archive| {
        let opts = sqlarfs::ArchiveOptions::new().children(true);
        archive.archive_with(temp_dir.path(), "", &opts)
    })?;

    expect!(dump_table(&crate_db)?).to(consist_of(dump_table(&reference_db)?));

    Ok(())
}

#[test]
#[serial(change_directory)]
fn extract_empty_regular_file() -> sqlarfs::Result<()> {
    let db_dir = tempfile::tempdir()?;
    let db = db_dir.path().join("test.sqlar");

    let crate_dest_dir = tempfile::tempdir()?;
    let reference_dest_dir = tempfile::tempdir()?;

    Connection::create_new(&db)?.exec(|archive| {
        archive.open("file")?.create_file()?;

        archive.extract("file", &crate_dest_dir.path().join("file"))
    })?;

    env::set_current_dir(reference_dest_dir.path())?;
    sqlar_command(&db, &["--extract", "file"])?;

    expect!(crate_dest_dir.path().join("file"))
        .to(have_same_contents(reference_dest_dir.path().join("file")));
    expect!(crate_dest_dir.path().join("file")).to(have_same_permissions(
        reference_dest_dir.path().join("file"),
    ));
    expect!(crate_dest_dir.path().join("file"))
        .to(have_same_mtime(reference_dest_dir.path().join("file")));

    Ok(())
}

#[test]
#[serial(change_directory)]
fn extract_regular_file_with_data() -> sqlarfs::Result<()> {
    let db_dir = tempfile::tempdir()?;
    let db = db_dir.path().join("test.sqlar");

    let crate_dest_dir = tempfile::tempdir()?;
    let reference_dest_dir = tempfile::tempdir()?;

    Connection::create_new(&db)?.exec(|archive| {
        let mut file = archive.open("file")?;
        file.create_file()?;
        file.write_str("file contents")?;

        archive.extract("file", &crate_dest_dir.path().join("file"))
    })?;

    env::set_current_dir(reference_dest_dir.path())?;
    sqlar_command(&db, &["--extract", "file"])?;

    expect!(crate_dest_dir.path().join("file"))
        .to(have_same_contents(reference_dest_dir.path().join("file")));
    expect!(crate_dest_dir.path().join("file")).to(have_same_permissions(
        reference_dest_dir.path().join("file"),
    ));
    expect!(crate_dest_dir.path().join("file"))
        .to(have_same_mtime(reference_dest_dir.path().join("file")));

    Ok(())
}

// TODO: We need to ignore this test until the following bug fixes in SQLite are released:
// - https://www.sqlite.org/src/info/4d90c3f179a3d735
// - https://www.sqlite.org/src/info/2bf8c3f99ad8b74f
#[test]
#[serial(change_directory)]
#[cfg(unix)]
#[ignore]
fn extract_symlink() -> sqlarfs::Result<()> {
    let db_dir = tempfile::tempdir()?;
    let db = db_dir.path().join("test.sqlar");

    let crate_dest_dir = tempfile::tempdir()?;
    let reference_dest_dir = tempfile::tempdir()?;

    let symlink_target = tempfile::NamedTempFile::new()?;

    Connection::create_new(&db)?.exec(|archive| {
        archive
            .open("symlink")?
            .create_symlink(symlink_target.path())?;

        archive.extract("symlink", &crate_dest_dir.path().join("symlink"))
    })?;

    env::set_current_dir(reference_dest_dir.path())?;
    sqlar_command(&db, &["--extract", "symlink"])?;

    dbg!(reference_dest_dir.path().join("symlink").exists());

    expect!(crate_dest_dir.path().join("symlink")).to(have_same_symlink_target(
        reference_dest_dir.path().join("symlink"),
    ));
    expect!(crate_dest_dir.path().join("symlink")).to(have_same_permissions(
        reference_dest_dir.path().join("symlink"),
    ));
    expect!(crate_dest_dir.path().join("symlink"))
        .to(have_same_mtime(reference_dest_dir.path().join("symlink")));

    Ok(())
}

#[test]
#[serial(change_directory)]
fn extract_empty_directory() -> sqlarfs::Result<()> {
    let db_dir = tempfile::tempdir()?;
    let db = db_dir.path().join("test.sqlar");

    let crate_dest_dir = tempfile::tempdir()?;
    let reference_dest_dir = tempfile::tempdir()?;

    Connection::create_new(&db)?.exec(|archive| {
        archive.open("dir")?.create_dir()?;

        archive.extract("dir", &crate_dest_dir.path().join("dir"))
    })?;

    env::set_current_dir(reference_dest_dir.path())?;
    sqlar_command(&db, &["--extract", "dir"])?;

    expect!(crate_dest_dir.path().join("dir"))
        .to(have_same_permissions(reference_dest_dir.path().join("dir")));
    expect!(crate_dest_dir.path().join("dir"))
        .to(have_same_mtime(reference_dest_dir.path().join("dir")));

    Ok(())
}

#[test]
#[serial(change_directory)]
fn extract_directory_with_children() -> sqlarfs::Result<()> {
    let db_dir = tempfile::tempdir()?;
    let db = db_dir.path().join("test.sqlar");

    let crate_dest_dir = tempfile::tempdir()?;
    let reference_dest_dir = tempfile::tempdir()?;

    Connection::create_new(&db)?.exec(|archive| {
        archive.open("dir")?.create_dir()?;
        archive.open("dir/file1")?.create_file()?;
        archive.open("dir/subdir")?.create_dir()?;
        archive.open("dir/subdir/file2")?.create_file()?;

        archive.extract("dir", &crate_dest_dir.path().join("dir"))
    })?;

    env::set_current_dir(reference_dest_dir.path())?;
    sqlar_command(&db, &["--extract", "dir"])?;

    expect!(crate_dest_dir.path().join("dir"))
        .to(have_same_permissions(reference_dest_dir.path().join("dir")));
    expect!(crate_dest_dir.path().join("dir"))
        .to(have_same_mtime(reference_dest_dir.path().join("dir")));

    Ok(())
}

#[test]
#[serial(change_directory)]
fn extract_regular_file_with_readonly_permissions() -> sqlarfs::Result<()> {
    let db_dir = tempfile::tempdir()?;
    let db = db_dir.path().join("test.sqlar");

    let crate_dest_dir = tempfile::tempdir()?;
    let reference_dest_dir = tempfile::tempdir()?;

    Connection::create_new(&db)?.exec(|archive| {
        let mut file = archive.open("file")?;
        file.create_file()?;
        file.set_mode(Some(
            FileMode::OWNER_R | FileMode::GROUP_R | FileMode::OTHER_R,
        ))?;

        archive.extract("file", &crate_dest_dir.path().join("file"))
    })?;

    env::set_current_dir(reference_dest_dir.path())?;
    sqlar_command(&db, &["--extract", "file"])?;

    expect!(crate_dest_dir.path().join("file")).to(have_same_permissions(
        reference_dest_dir.path().join("file"),
    ));

    Ok(())
}