use std::{
io::{Read, Seek, SeekFrom, Write},
os::unix::fs::{FileTypeExt, MetadataExt},
path::PathBuf,
};
use xpct::{be_err, be_false, be_ok, be_true, equal, expect, match_pattern, pattern};
use liteboxfs::{
ArchiveOptions, Connection, CreateOptions, Error, ExtractOptions,
metadata::{FileKind, FileMode, Owner},
};
mod archive {
use std::fs;
use super::*;
#[test]
fn archive_regular_file() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let source = dir.path().join("hello.txt");
fs::write(&source, b"hello world").unwrap();
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
fs.archive(&source, "hello.txt")?;
let mut file = fs.open("hello.txt")?;
expect!(file.kind()).to(equal(&FileKind::Regular));
let mut content = String::new();
file.read_to_string(&mut content)?;
expect!(content).to(equal("hello world"));
let meta = file.metadata()?;
let source_meta = fs::metadata(&source).unwrap();
let source_mode = FileMode::from_bits_truncate(source_meta.mode());
expect!(meta.mode()).to(equal(source_mode));
liteboxfs::Result::Ok(())
})?;
Ok(())
}
#[test]
fn archive_empty_directory() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let source = dir.path().join("subdir");
fs::create_dir(&source).unwrap();
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
fs.archive(&source, "subdir")?;
let file = fs.open("subdir")?;
expect!(file.kind()).to(equal(&FileKind::Dir));
liteboxfs::Result::Ok(())
})?;
Ok(())
}
#[test]
fn archive_symlink() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("target.txt");
fs::write(&target, b"target content").unwrap();
let link = dir.path().join("link");
std::os::unix::fs::symlink("target.txt", &link).unwrap();
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
fs.archive(&link, "link")?;
let file = fs.open("link")?;
expect!(file.kind()).to(equal(&FileKind::Symlink {
target: PathBuf::from("target.txt"),
}));
liteboxfs::Result::Ok(())
})?;
Ok(())
}
#[test]
fn archive_pipe() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let source = dir.path().join("fifo");
nix::unistd::mkfifo(&source, nix::sys::stat::Mode::S_IRWXU).unwrap();
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
fs.archive(&source, "fifo")?;
let file = fs.open("fifo")?;
expect!(file.kind()).to(equal(&FileKind::Pipe));
liteboxfs::Result::Ok(())
})?;
Ok(())
}
#[test]
fn archive_preserves_timestamps() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let source = dir.path().join("timed.txt");
fs::write(&source, b"data").unwrap();
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
fs.archive(&source, "timed.txt")?;
let mut file = fs.open("timed.txt")?;
let meta = file.metadata()?;
let host_meta = fs::metadata(&source).unwrap();
let host_mtime = host_meta.modified().unwrap();
expect!(meta.modified()).to(equal(host_mtime));
liteboxfs::Result::Ok(())
})?;
Ok(())
}
#[test]
fn archive_nonexistent_source() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let source = dir.path().join("nonexistent");
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
expect!(fs.archive(&source, "dest"))
.to(be_err())
.to(match_pattern(pattern!(Error::FileNotFound { .. })));
liteboxfs::Result::Ok(())
})?;
Ok(())
}
#[test]
fn archive_to_existing_dest() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let source = dir.path().join("file.txt");
fs::write(&source, b"data").unwrap();
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
fs.create("file.txt", FileKind::Regular, Owner::ROOT)?;
expect!(fs.archive(&source, "file.txt"))
.to(be_err())
.to(match_pattern(pattern!(Error::FileAlreadyExists { .. })));
liteboxfs::Result::Ok(())
})?;
Ok(())
}
#[test]
fn archive_to_missing_parent() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let source = dir.path().join("file.txt");
fs::write(&source, b"data").unwrap();
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
expect!(fs.archive(&source, "missing/file.txt"))
.to(be_err())
.to(match_pattern(pattern!(Error::NoParentDirectory { .. })));
liteboxfs::Result::Ok(())
})?;
Ok(())
}
#[test]
fn archive_socket_returns_unsupported_file_type() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let socket_path = dir.path().join("sock");
let _listener = std::os::unix::net::UnixListener::bind(&socket_path).unwrap();
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
expect!(fs.archive(&socket_path, "sock"))
.to(be_err())
.to(match_pattern(pattern!(Error::UnsupportedFileType { .. })));
liteboxfs::Result::Ok(())
})?;
Ok(())
}
}
mod archive_with {
use std::fs;
use super::*;
#[test]
fn archive_with_nested_dirs() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let root = dir.path().join("root");
fs::create_dir_all(root.join("a")).unwrap();
fs::write(root.join("a/b.txt"), b"nested content").unwrap();
fs::create_dir(root.join("c")).unwrap();
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
fs.archive(&root, "root")?;
let root_file = fs.open("root")?;
expect!(root_file.kind()).to(equal(&FileKind::Dir));
drop(root_file);
let a_file = fs.open("root/a")?;
expect!(a_file.kind()).to(equal(&FileKind::Dir));
drop(a_file);
let mut b_file = fs.open("root/a/b.txt")?;
let mut content = String::new();
b_file.read_to_string(&mut content)?;
expect!(content).to(equal("nested content"));
drop(b_file);
let c_file = fs.open("root/c")?;
expect!(c_file.kind()).to(equal(&FileKind::Dir));
liteboxfs::Result::Ok(())
})?;
Ok(())
}
#[test]
fn archive_preserves_hard_links() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let root = dir.path().join("root");
fs::create_dir(&root).unwrap();
fs::write(root.join("original.txt"), b"shared data").unwrap();
fs::hard_link(root.join("original.txt"), root.join("link.txt")).unwrap();
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
fs.archive(&root, "root")?;
let original = fs.open("root/original.txt")?;
let original_id = original.file_id();
drop(original);
let link = fs.open("root/link.txt")?;
let link_id = link.file_id();
drop(link);
expect!(original_id).to(equal(link_id));
liteboxfs::Result::Ok(())
})?;
Ok(())
}
#[test]
fn archive_single_file() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let source = dir.path().join("single.txt");
fs::write(&source, b"just a file").unwrap();
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
fs.archive(&source, "single.txt")?;
let mut file = fs.open("single.txt")?;
let mut content = String::new();
file.read_to_string(&mut content)?;
expect!(content).to(equal("just a file"));
liteboxfs::Result::Ok(())
})?;
Ok(())
}
#[test]
fn follow_archives_symlink_target() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("target.txt");
fs::write(&target, b"target content").unwrap();
let link = dir.path().join("link");
std::os::unix::fs::symlink("target.txt", &link).unwrap();
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
fs.archive_with(&link, "link", &ArchiveOptions::new().follow_symlinks(true))?;
let mut file = fs.open("link")?;
expect!(file.kind()).to(equal(&FileKind::Regular));
let mut content = String::new();
file.read_to_string(&mut content)?;
expect!(content).to(equal("target content"));
liteboxfs::Result::Ok(())
})?;
Ok(())
}
#[test]
fn children_archives_dir_contents_into_existing_dir() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let source = dir.path().join("src");
fs::create_dir(&source).unwrap();
fs::write(source.join("a.txt"), b"aaa").unwrap();
fs::write(source.join("b.txt"), b"bbb").unwrap();
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
fs.create("dest", FileKind::Dir, Owner::current())?;
fs.archive_with(&source, "dest", &ArchiveOptions::new().children(true))?;
let mut a = fs.open("dest/a.txt")?;
let mut content = String::new();
a.read_to_string(&mut content)?;
expect!(content).to(equal("aaa"));
drop(a);
let mut b = fs.open("dest/b.txt")?;
let mut content = String::new();
b.read_to_string(&mut content)?;
expect!(content).to(equal("bbb"));
liteboxfs::Result::Ok(())
})?;
Ok(())
}
#[test]
fn children_on_non_directory_source_returns_not_a_directory() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let source = dir.path().join("file.txt");
fs::write(&source, b"data").unwrap();
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
expect!(fs.archive_with(&source, "dest", &ArchiveOptions::new().children(true)))
.to(be_err())
.to(match_pattern(pattern!(Error::NotADirectory { .. })));
liteboxfs::Result::Ok(())
})?;
Ok(())
}
#[test]
fn children_on_non_directory_dest_returns_not_a_directory() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let source = dir.path().join("src");
fs::create_dir(&source).unwrap();
fs::write(source.join("a.txt"), b"aaa").unwrap();
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
fs.create("dest", FileKind::Regular, Owner::current())?;
expect!(fs.archive_with(&source, "dest", &ArchiveOptions::new().children(true)))
.to(be_err())
.to(match_pattern(pattern!(Error::NotADirectory { .. })));
liteboxfs::Result::Ok(())
})?;
Ok(())
}
#[test]
fn non_recursive_archives_only_immediate_entries() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let source = dir.path().join("src");
fs::create_dir(&source).unwrap();
fs::write(source.join("top.txt"), b"top").unwrap();
fs::create_dir(source.join("sub")).unwrap();
fs::write(source.join("sub/nested.txt"), b"nested").unwrap();
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
fs.archive_with(&source, "dest", &ArchiveOptions::new().recursive(false))?;
let dest = fs.open("dest")?;
expect!(dest.kind()).to(equal(&FileKind::Dir));
drop(dest);
expect!(fs.open("dest/top.txt"))
.to(be_err())
.to(match_pattern(pattern!(Error::FileNotFound { .. })));
liteboxfs::Result::Ok(())
})?;
Ok(())
}
#[test]
fn socket_descendant_skipped_in_recursive_archive() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let source = dir.path().join("src");
fs::create_dir(&source).unwrap();
fs::write(source.join("file.txt"), b"data").unwrap();
let _listener = std::os::unix::net::UnixListener::bind(source.join("sock")).unwrap();
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
fs.archive(&source, "dest")?;
let file = fs.open("dest/file.txt")?;
expect!(file.kind()).to(equal(&FileKind::Regular));
drop(file);
expect!(fs.open("dest/sock"))
.to(be_err())
.to(match_pattern(pattern!(Error::FileNotFound { .. })));
liteboxfs::Result::Ok(())
})?;
Ok(())
}
#[test]
fn socket_child_skipped_in_children_mode() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let source = dir.path().join("src");
fs::create_dir(&source).unwrap();
fs::write(source.join("file.txt"), b"data").unwrap();
let _listener = std::os::unix::net::UnixListener::bind(source.join("sock")).unwrap();
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
fs.create("dest", FileKind::Dir, Owner::current())?;
fs.archive_with(&source, "dest", &ArchiveOptions::new().children(true))?;
let file = fs.open("dest/file.txt")?;
expect!(file.kind()).to(equal(&FileKind::Regular));
drop(file);
expect!(fs.open("dest/sock"))
.to(be_err())
.to(match_pattern(pattern!(Error::FileNotFound { .. })));
liteboxfs::Result::Ok(())
})?;
Ok(())
}
#[test]
fn socket_child_skipped_in_non_recursive_children_mode() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let source = dir.path().join("src");
fs::create_dir(&source).unwrap();
fs::write(source.join("file.txt"), b"data").unwrap();
let _listener = std::os::unix::net::UnixListener::bind(source.join("sock")).unwrap();
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
fs.create("dest", FileKind::Dir, Owner::current())?;
fs.archive_with(
&source,
"dest",
&ArchiveOptions::new().children(true).recursive(false),
)?;
let file = fs.open("dest/file.txt")?;
expect!(file.kind()).to(equal(&FileKind::Regular));
drop(file);
expect!(fs.open("dest/sock"))
.to(be_err())
.to(match_pattern(pattern!(Error::FileNotFound { .. })));
liteboxfs::Result::Ok(())
})?;
Ok(())
}
#[test]
fn metadata_false_uses_default_metadata() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let source = dir.path().join("file.txt");
fs::write(&source, b"data").unwrap();
fs::set_permissions(&source, std::os::unix::fs::PermissionsExt::from_mode(0o755)).unwrap();
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
fs.archive_with(
&source,
"file.txt",
&ArchiveOptions::new().preserve_metadata(false),
)?;
let mut file = fs.open("file.txt")?;
let meta = file.metadata()?;
let source_mode = FileMode::from_bits_truncate(0o755);
expect!(meta.mode() != source_mode).to(be_true());
let mut content = String::new();
file.read_to_string(&mut content)?;
expect!(content).to(equal("data"));
liteboxfs::Result::Ok(())
})?;
Ok(())
}
}
mod extract {
use std::fs;
use super::*;
#[test]
fn extract_regular_file() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let dest = dir.path().join("extracted.txt");
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
let mut file = fs.create("test.txt", FileKind::Regular, Owner::current())?;
file.write_all(b"extract me")?;
let mode = FileMode::OWNER_R | FileMode::OWNER_W | FileMode::GROUP_R;
file.set_mode(mode)?;
drop(file);
fs.extract("test.txt", &dest)?;
let content = fs::read_to_string(&dest).unwrap();
expect!(content).to(equal("extract me"));
let host_meta = fs::metadata(&dest).unwrap();
let host_mode = FileMode::from_bits_truncate(host_meta.mode());
expect!(host_mode).to(equal(mode));
liteboxfs::Result::Ok(())
})?;
Ok(())
}
#[test]
fn extract_empty_directory() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let dest = dir.path().join("extracted_dir");
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
fs.create("dir", FileKind::Dir, Owner::current())?;
fs.extract("dir", &dest)?;
expect!(dest.is_dir()).to(be_true());
liteboxfs::Result::Ok(())
})?;
Ok(())
}
#[test]
fn extract_symlink() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let dest = dir.path().join("extracted_link");
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
fs.create(
"link",
FileKind::Symlink {
target: PathBuf::from("/some/target"),
},
Owner::current(),
)?;
fs.extract("link", &dest)?;
let target = fs::read_link(&dest).unwrap();
expect!(target).to(equal(PathBuf::from("/some/target")));
liteboxfs::Result::Ok(())
})?;
Ok(())
}
#[test]
fn extract_pipe() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let dest = dir.path().join("extracted_fifo");
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
fs.create("fifo", FileKind::Pipe, Owner::current())?;
fs.extract("fifo", &dest)?;
let meta = fs::symlink_metadata(&dest).unwrap();
expect!(meta.file_type().is_fifo()).to(be_true());
liteboxfs::Result::Ok(())
})?;
Ok(())
}
#[test]
fn extract_nonexistent_source() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let dest = dir.path().join("dest.txt");
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
expect!(fs.extract("nonexistent.txt", &dest))
.to(be_err())
.to(match_pattern(pattern!(Error::FileNotFound { .. })));
liteboxfs::Result::Ok(())
})?;
Ok(())
}
#[test]
fn extract_to_existing_dest() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let dest = dir.path().join("existing.txt");
fs::write(&dest, b"already here").unwrap();
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
fs.create("test.txt", FileKind::Regular, Owner::ROOT)?;
expect!(fs.extract("test.txt", &dest))
.to(be_err())
.to(match_pattern(pattern!(Error::FileAlreadyExists { .. })));
liteboxfs::Result::Ok(())
})?;
Ok(())
}
#[test]
fn extract_to_missing_parent() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let dest = dir.path().join("missing/dest.txt");
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
fs.create("test.txt", FileKind::Regular, Owner::ROOT)?;
expect!(fs.extract("test.txt", &dest))
.to(be_err())
.to(match_pattern(pattern!(Error::NoParentDirectory { .. })));
liteboxfs::Result::Ok(())
})?;
Ok(())
}
#[test]
fn extract_file_with_trailing_hole() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let dest = dir.path().join("sparse.bin");
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
let mut file = fs.create("sparse.bin", FileKind::Regular, Owner::current())?;
file.write_all(b"hello")?;
file.set_len(4096)?;
drop(file);
fs.extract("sparse.bin", &dest)?;
let meta = fs::metadata(&dest).unwrap();
expect!(meta.len()).to(equal(4096));
let content = fs::read(&dest).unwrap();
expect!(&content[..5]).to(equal(b"hello".as_slice()));
expect!(content[5..].iter().all(|&b| b == 0)).to(be_true());
liteboxfs::Result::Ok(())
})?;
Ok(())
}
#[test]
fn extract_file_with_leading_hole() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let dest = dir.path().join("sparse.bin");
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
let mut file = fs.create("sparse.bin", FileKind::Regular, Owner::current())?;
file.set_len(4096)?;
file.seek(SeekFrom::Start(4096))?;
file.write_all(b"world")?;
drop(file);
fs.extract("sparse.bin", &dest)?;
let meta = fs::metadata(&dest).unwrap();
expect!(meta.len()).to(equal(4101));
let content = fs::read(&dest).unwrap();
expect!(content[..4096].iter().all(|&b| b == 0)).to(be_true());
expect!(&content[4096..]).to(equal(b"world".as_slice()));
liteboxfs::Result::Ok(())
})?;
Ok(())
}
#[test]
fn extract_file_with_hole_between_data() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let dest = dir.path().join("sparse.bin");
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
let mut file = fs.create("sparse.bin", FileKind::Regular, Owner::current())?;
file.write_all(b"prefix")?;
file.set_len(4096)?;
file.seek(SeekFrom::Start(4096))?;
file.write_all(b"suffix")?;
drop(file);
fs.extract("sparse.bin", &dest)?;
let meta = fs::metadata(&dest).unwrap();
expect!(meta.len()).to(equal(4102));
let content = fs::read(&dest).unwrap();
expect!(&content[..6]).to(equal(b"prefix".as_slice()));
expect!(content[6..4096].iter().all(|&b| b == 0)).to(be_true());
expect!(&content[4096..]).to(equal(b"suffix".as_slice()));
liteboxfs::Result::Ok(())
})?;
Ok(())
}
#[test]
fn extract_file_that_is_entirely_a_hole() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let dest = dir.path().join("sparse.bin");
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
let mut file = fs.create("sparse.bin", FileKind::Regular, Owner::current())?;
file.set_len(8192)?;
drop(file);
fs.extract("sparse.bin", &dest)?;
let meta = fs::metadata(&dest).unwrap();
expect!(meta.len()).to(equal(8192));
let content = fs::read(&dest).unwrap();
expect!(content.iter().all(|&b| b == 0)).to(be_true());
expect!(meta.blocks()).to(equal(0));
liteboxfs::Result::Ok(())
})?;
Ok(())
}
#[test]
fn extract_file_with_no_holes_is_not_sparse() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let dest = dir.path().join("dense.bin");
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
let mut file = fs.create("dense.bin", FileKind::Regular, Owner::current())?;
let data = vec![0xABu8; 4096];
file.write_all(&data)?;
drop(file);
fs.extract("dense.bin", &dest)?;
let content = fs::read(&dest).unwrap();
expect!(content.len()).to(equal(4096));
expect!(content.iter().all(|&b| b == 0xAB)).to(be_true());
liteboxfs::Result::Ok(())
})?;
Ok(())
}
}
mod extract_with {
use std::fs;
use super::*;
#[test]
fn extract_with_nested_dirs() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let dest = dir.path().join("extracted");
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
fs.create("root", FileKind::Dir, Owner::current())?;
fs.create("root/a", FileKind::Dir, Owner::current())?;
let mut file = fs.create("root/a/b.txt", FileKind::Regular, Owner::current())?;
file.write_all(b"nested")?;
drop(file);
fs.create("root/c", FileKind::Dir, Owner::current())?;
fs.extract("root", &dest)?;
expect!(dest.is_dir()).to(be_true());
expect!(dest.join("a").is_dir()).to(be_true());
expect!(dest.join("c").is_dir()).to(be_true());
let content = fs::read_to_string(dest.join("a/b.txt")).unwrap();
expect!(content).to(equal("nested"));
liteboxfs::Result::Ok(())
})?;
Ok(())
}
#[test]
fn extract_preserves_hard_links() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let dest = dir.path().join("extracted");
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
fs.create("root", FileKind::Dir, Owner::current())?;
let mut file = fs.create("root/original.txt", FileKind::Regular, Owner::current())?;
file.write_all(b"shared data")?;
drop(file);
fs.open("root/original.txt")?.link("root/link.txt")?;
fs.extract("root", &dest)?;
let content1 = fs::read_to_string(dest.join("original.txt")).unwrap();
let content2 = fs::read_to_string(dest.join("link.txt")).unwrap();
expect!(content1).to(equal("shared data"));
expect!(content2).to(equal("shared data"));
let meta1 = fs::metadata(dest.join("original.txt")).unwrap();
let meta2 = fs::metadata(dest.join("link.txt")).unwrap();
expect!(meta1.ino()).to(equal(meta2.ino()));
liteboxfs::Result::Ok(())
})?;
Ok(())
}
#[test]
fn children_extracts_dir_contents_into_existing_dir() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let dest = dir.path().join("dest");
fs::create_dir(&dest).unwrap();
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
fs.create("src", FileKind::Dir, Owner::current())?;
let mut file = fs.create("src/a.txt", FileKind::Regular, Owner::current())?;
file.write_all(b"aaa")?;
drop(file);
let mut file = fs.create("src/b.txt", FileKind::Regular, Owner::current())?;
file.write_all(b"bbb")?;
drop(file);
fs.extract_with("src", &dest, &ExtractOptions::new().children(true))?;
let content_a = fs::read_to_string(dest.join("a.txt")).unwrap();
expect!(content_a).to(equal("aaa"));
let content_b = fs::read_to_string(dest.join("b.txt")).unwrap();
expect!(content_b).to(equal("bbb"));
liteboxfs::Result::Ok(())
})?;
Ok(())
}
#[test]
fn children_on_non_directory_source_returns_not_a_directory() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let dest = dir.path().join("dest");
fs::create_dir(&dest).unwrap();
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
fs.create("file.txt", FileKind::Regular, Owner::current())?;
expect!(fs.extract_with("file.txt", &dest, &ExtractOptions::new().children(true)))
.to(be_err())
.to(match_pattern(pattern!(Error::NotADirectory { .. })));
liteboxfs::Result::Ok(())
})?;
Ok(())
}
#[test]
fn children_on_non_directory_dest_returns_not_a_directory() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let dest = dir.path().join("dest.txt");
fs::write(&dest, b"existing").unwrap();
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
fs.create("src", FileKind::Dir, Owner::current())?;
expect!(fs.extract_with("src", &dest, &ExtractOptions::new().children(true)))
.to(be_err())
.to(match_pattern(pattern!(Error::NotADirectory { .. })));
liteboxfs::Result::Ok(())
})?;
Ok(())
}
#[test]
fn children_non_recursive_extracts_only_immediate_children() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let dest = dir.path().join("dest");
fs::create_dir(&dest).unwrap();
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
fs.create("src", FileKind::Dir, Owner::current())?;
let mut file = fs.create("src/top.txt", FileKind::Regular, Owner::current())?;
file.write_all(b"top")?;
drop(file);
fs.create("src/sub", FileKind::Dir, Owner::current())?;
let mut file = fs.create("src/sub/nested.txt", FileKind::Regular, Owner::current())?;
file.write_all(b"nested")?;
drop(file);
fs.extract_with(
"src",
&dest,
&ExtractOptions::new().children(true).recursive(false),
)?;
expect!(fs::read_to_string(dest.join("top.txt")))
.to(be_ok())
.to(equal("top"));
expect!(dest.join("sub").is_dir()).to(be_true());
expect!(dest.join("sub/nested.txt").exists()).to(be_false());
liteboxfs::Result::Ok(())
})?;
Ok(())
}
#[test]
fn non_recursive_extracts_only_immediate_entries() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let dest = dir.path().join("extracted");
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
fs.create("root", FileKind::Dir, Owner::current())?;
let mut file = fs.create("root/top.txt", FileKind::Regular, Owner::current())?;
file.write_all(b"top")?;
drop(file);
fs.create("root/sub", FileKind::Dir, Owner::current())?;
let mut file = fs.create("root/sub/nested.txt", FileKind::Regular, Owner::current())?;
file.write_all(b"nested")?;
drop(file);
fs.extract_with("root", &dest, &ExtractOptions::new().recursive(false))?;
expect!(dest.is_dir()).to(be_true());
expect!(dest.join("top.txt").exists()).to(equal(false));
expect!(dest.join("sub").exists()).to(equal(false));
liteboxfs::Result::Ok(())
})?;
Ok(())
}
}
mod roundtrip {
use std::{fs, os};
use super::*;
#[test]
fn archive_then_extract_regular_file() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let source = dir.path().join("source.txt");
let dest = dir.path().join("dest.txt");
fs::write(&source, b"roundtrip content").unwrap();
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
fs.archive(&source, "file.txt")?;
fs.extract("file.txt", &dest)?;
let content = fs::read_to_string(&dest).unwrap();
expect!(content).to(equal("roundtrip content"));
let source_meta = fs::metadata(&source).unwrap();
let dest_meta = fs::metadata(&dest).unwrap();
expect!(dest_meta.modified())
.to(be_ok())
.to(equal(source_meta.modified()?));
liteboxfs::Result::Ok(())
})?;
Ok(())
}
#[test]
fn archive_then_extract_tree() -> liteboxfs::Result<()> {
let dir = tempfile::tempdir().unwrap();
let source = dir.path().join("src_tree");
let dest = dir.path().join("dst_tree");
fs::create_dir_all(source.join("sub")).unwrap();
fs::write(source.join("file.txt"), b"top level").unwrap();
fs::write(source.join("sub/nested.txt"), b"nested").unwrap();
os::unix::fs::symlink("../file.txt", source.join("sub/link")).unwrap();
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
fs.archive(&source, "tree")?;
fs.extract("tree", &dest)?;
let top = fs::read_to_string(dest.join("file.txt")).unwrap();
expect!(top).to(equal("top level"));
let nested = fs::read_to_string(dest.join("sub/nested.txt")).unwrap();
expect!(nested).to(equal("nested"));
let link_target = fs::read_link(dest.join("sub/link")).unwrap();
expect!(link_target).to(equal(PathBuf::from("../file.txt")));
liteboxfs::Result::Ok(())
})?;
Ok(())
}
}