mod common; use std::ffi::OsStr; use std::fs; use std::io::prelude::*; use std::time::{Duration, SystemTime}; use common::{ connection, have_file_metadata, have_symlink_metadata, into_sqlarfs_error, truncate_mtime, with_timeout, }; use sqlarfs::{ArchiveOptions, Error, FileMode, FileType}; use xpct::{ approx_eq_time, be_err, be_false, be_ok, be_some, be_true, equal, expect, match_pattern, pattern, }; // // `Archive::archive` // #[test] fn archiving_when_source_path_does_not_exist_errors() -> sqlarfs::Result<()> { connection()?.exec(|archive| { expect!(archive.archive("nonexistent", "dest")) .to(be_err()) .to(equal(Error::FileNotFound { path: "nonexistent".into(), })); Ok(()) }) } #[test] fn archiving_when_dest_path_has_no_parent_dir_errors() -> sqlarfs::Result<()> { let temp_file = tempfile::NamedTempFile::new()?; connection()?.exec(|archive| { expect!(archive.archive(temp_file.path(), "nonexistent/file")) .to(be_err()) .to(equal(Error::NoParentDirectory { path: "nonexistent/file".into(), })); Ok(()) }) } #[test] fn archiving_when_dest_path_already_exists_errors() -> sqlarfs::Result<()> { let temp_file = tempfile::NamedTempFile::new()?; connection()?.exec(|archive| { let mut target = archive.open("file")?; target.create_file()?; expect!(archive.archive(temp_file.path(), "file")) .to(be_err()) .to(equal(Error::FileAlreadyExists { path: "file".into(), })); Ok(()) }) } #[test] fn archiving_when_dest_path_is_absolute_errors() -> sqlarfs::Result<()> { let dest_path = if cfg!(windows) { r"C:\file" } else { "/file" }; let temp_file = tempfile::NamedTempFile::new()?; connection()?.exec(|archive| { expect!(archive.archive(temp_file.path(), dest_path)) .to(be_err()) .to(match_pattern(pattern!(Error::InvalidArgs { .. }))); Ok(()) }) } #[test] #[cfg(unix)] fn archiving_when_dest_path_is_not_valid_unicode_errors() -> sqlarfs::Result<()> { use std::os::unix::ffi::OsStrExt; let temp_file = tempfile::NamedTempFile::new()?; connection()?.exec(|archive| { expect!(archive.archive(temp_file.path(), OsStr::from_bytes(b"invalid-unicode-\xff"),)) .to(be_err()) .to(match_pattern(pattern!(Error::InvalidArgs { .. }))); Ok(()) }) } #[test] fn archive_regular_file() -> sqlarfs::Result<()> { let temp_file = tempfile::NamedTempFile::new()?; connection()?.exec(|archive| { expect!(archive.archive(temp_file.path(), "file")).to(be_ok()); let file = archive.open("file")?; expect!(file.exists()).to(be_ok()).to(be_true()); expect!(file.metadata()) .to(be_ok()) .into::<FileType>() .to(equal(FileType::File)); Ok(()) }) } #[test] fn archive_empty_directory() -> sqlarfs::Result<()> { let temp_dir = tempfile::tempdir()?; connection()?.exec(|archive| { expect!(archive.archive(temp_dir.path(), "dir")).to(be_ok()); let file = archive.open("dir")?; expect!(file.exists()).to(be_ok()).to(be_true()); expect!(file.metadata()) .to(be_ok()) .into::<FileType>() .to(equal(FileType::Dir)); Ok(()) }) } #[test] #[cfg(unix)] fn archiving_preserves_unix_file_mode() -> sqlarfs::Result<()> { use std::os::unix::fs::PermissionsExt; // These are unlikely to be the default permissions of a temporary file. let expected_mode = 0o444; let temp_file = tempfile::NamedTempFile::new()?; fs::set_permissions(temp_file.path(), fs::Permissions::from_mode(expected_mode))?; // A sanity check to guard against tests passing when they shouldn't. expect!(fs::metadata(temp_file.path())?) .map(|metadata| metadata.permissions().mode()) .to_not(equal(expected_mode)); connection()?.exec(|archive| { expect!(archive.archive(temp_file.path(), "file")).to(be_ok()); let file = archive.open("file")?; expect!(file.metadata()) .to(be_ok()) .to(have_file_metadata()) .map(|metadata| metadata.mode) .to(be_some()) .to(equal(FileMode::from_bits_truncate(expected_mode))); Ok(()) }) } #[test] fn archiving_preserves_file_mtime() -> sqlarfs::Result<()> { // Some time in the past. let expected_mtime = truncate_mtime(SystemTime::now() - Duration::from_secs(60)); let mut temp_file = tempfile::NamedTempFile::new()?; write!(temp_file.as_file_mut(), "file contents")?; temp_file.as_file().set_modified(expected_mtime)?; connection()?.exec(|archive| { expect!(archive.archive(temp_file.path(), "file")).to(be_ok()); let file = archive.open("file")?; expect!(file.metadata()) .to(be_ok()) .to(have_file_metadata()) .map(|metadata| metadata.mtime) .to(be_some()) .to(equal(expected_mtime)); Ok(()) }) } #[test] #[cfg(unix)] fn archiving_skips_special_files() -> sqlarfs::Result<()> { use nix::sys::stat::Mode as UnixMode; use nix::unistd::mkfifo; let temp_dir = tempfile::tempdir()?; mkfifo(&temp_dir.path().join("fifo"), UnixMode::S_IRWXU).map_err(into_sqlarfs_error)?; connection()?.exec(|archive| { expect!(archive.archive(temp_dir.path(), "dir")).to(be_ok()); let special_file = archive.open("fifo")?; expect!(special_file.exists()).to(be_ok()).to(be_false()); Ok(()) }) } #[test] fn archive_with_trailing_slash_in_dest_path() -> sqlarfs::Result<()> { let temp_file = tempfile::NamedTempFile::new()?; connection()?.exec(|archive| { let dest_path = if cfg!(windows) { r"file\" } else { "file/" }; expect!(archive.archive(temp_file.path(), dest_path)).to(be_ok()); let file = archive.open("file")?; expect!(file.metadata()) .to(be_ok()) .into::<FileType>() .to(equal(FileType::File)); Ok(()) }) } // // `ArchiveOptions::follow_symlinks` // #[test] #[cfg(unix)] fn archiving_follows_symlinks() -> sqlarfs::Result<()> { use std::os::unix::fs::symlink; let temp_dir = tempfile::tempdir()?; let symlink_target = tempfile::NamedTempFile::new()?; symlink(symlink_target.path(), temp_dir.path().join("symlink"))?; connection()?.exec(|archive| { let opts = ArchiveOptions::new().follow_symlinks(true); expect!(archive.archive_with(temp_dir.path(), "dir", &opts)).to(be_ok()); let symlink = archive.open("dir/symlink")?; expect!(symlink.exists()).to(be_ok()).to(be_true()); expect!(symlink.metadata()) .to(be_ok()) .into::<FileType>() .to(equal(FileType::File)); Ok(()) }) } #[test] #[cfg(unix)] fn archiving_follows_chained_symlinks() -> sqlarfs::Result<()> { use std::os::unix::fs::symlink; let temp_dir = tempfile::tempdir()?; let symlink_target = tempfile::NamedTempFile::new()?; symlink(symlink_target.path(), temp_dir.path().join("symlink1"))?; symlink( temp_dir.path().join("symlink1"), temp_dir.path().join("symlink2"), )?; connection()?.exec(|archive| { let opts = ArchiveOptions::new().follow_symlinks(true); expect!(archive.archive_with(temp_dir.path(), "dir", &opts)).to(be_ok()); let symlink = archive.open("dir/symlink2")?; expect!(symlink.exists()).to(be_ok()).to(be_true()); expect!(symlink.metadata()) .to(be_ok()) .into::<FileType>() .to(equal(FileType::File)); Ok(()) }) } #[test] #[cfg(unix)] fn archiving_doest_not_follow_symlinks() -> sqlarfs::Result<()> { use std::os::unix::fs::symlink; let temp_dir = tempfile::tempdir()?; let symlink_target = tempfile::NamedTempFile::new()?; symlink(symlink_target.path(), temp_dir.path().join("symlink"))?; connection()?.exec(|archive| { expect!(archive.archive(temp_dir.path().join("symlink"), "symlink")).to(be_ok()); let symlink = archive.open("symlink")?; expect!(symlink.exists()).to(be_ok()).to(be_true()); expect!(symlink.metadata()) .to(be_ok()) .to(have_symlink_metadata()) .map(|metadata| metadata.target) .to(equal(symlink_target.path())); Ok(()) }) } #[test] #[cfg(unix)] fn archiving_doest_not_follow_symlink_children_of_directory() -> sqlarfs::Result<()> { use std::os::unix::fs::symlink; let temp_dir = tempfile::tempdir()?; let symlink_target = tempfile::NamedTempFile::new()?; symlink(symlink_target.path(), temp_dir.path().join("symlink"))?; connection()?.exec(|archive| { expect!(archive.archive(temp_dir.path(), "dir")).to(be_ok()); let symlink = archive.open("dir/symlink")?; expect!(symlink.exists()).to(be_ok()).to(be_true()); expect!(symlink.metadata()) .to(be_ok()) .to(have_symlink_metadata()) .map(|metadata| metadata.target) .to(equal(symlink_target.path())); Ok(()) }) } // // `ArchiveOptions::children` // #[test] fn archiving_fails_when_source_is_root_and_children_is_false() -> sqlarfs::Result<()> { connection()?.exec(|archive| { let temp_file = tempfile::NamedTempFile::new()?; expect!(archive.archive(temp_file.path(), "")) .to(be_err()) .to(match_pattern(pattern!(Error::InvalidArgs { .. }))); Ok(()) }) } #[test] fn archive_directory_children_to_dir() -> sqlarfs::Result<()> { let temp_dir = tempfile::tempdir()?; fs::OpenOptions::new() .write(true) .create_new(true) .open(temp_dir.path().join("file"))?; connection()?.exec(|archive| { let mut target = archive.open("dir")?; target.create_dir()?; let opts = ArchiveOptions::new().children(true); expect!(archive.archive_with(temp_dir.path(), "dir", &opts)).to(be_ok()); let file = archive.open(if cfg!(windows) { r"dir\file" } else { "dir/file" })?; expect!(file.exists()).to(be_ok()).to(be_true()); expect!(file.metadata()) .to(be_ok()) .into::<FileType>() .to(equal(FileType::File)); Ok(()) }) } #[test] fn archive_directory_children_to_archive_root() -> sqlarfs::Result<()> { let temp_dir = tempfile::tempdir()?; fs::OpenOptions::new() .write(true) .create_new(true) .open(temp_dir.path().join("file"))?; let opts = ArchiveOptions::new().children(true); connection()?.exec(|archive| { expect!(archive.archive_with(temp_dir.path(), "", &opts)).to(be_ok()); let file = archive.open("file")?; expect!(file.exists()).to(be_ok()).to(be_true()); expect!(file.metadata()) .to(be_ok()) .into::<FileType>() .to(equal(FileType::File)); Ok(()) }) } #[test] fn archiving_directory_children_when_target_is_file_errors() -> sqlarfs::Result<()> { let temp_dir = tempfile::tempdir()?; fs::OpenOptions::new() .write(true) .create_new(true) .open(temp_dir.path().join("file"))?; connection()?.exec(|archive| { let mut target = archive.open("file")?; target.create_file()?; let opts = ArchiveOptions::new().children(true); expect!(archive.archive_with(temp_dir.path(), "file", &opts)) .to(be_err()) .to(equal(Error::NotADirectory { path: "file".into(), })); Ok(()) }) } #[test] fn archiving_directory_children_when_target_doest_not_exist_errors() -> sqlarfs::Result<()> { let temp_dir = tempfile::tempdir()?; fs::OpenOptions::new() .write(true) .create_new(true) .open(temp_dir.path().join("file"))?; connection()?.exec(|archive| { let opts = ArchiveOptions::new().children(true); expect!(archive.archive_with(temp_dir.path(), "dir", &opts)) .to(be_err()) .to(equal(Error::FileNotFound { path: "dir".into() })); Ok(()) }) } #[test] fn archive_directory_children_when_source_is_file_errors() -> sqlarfs::Result<()> { let temp_file = tempfile::NamedTempFile::new()?; connection()?.exec(|archive| { let opts = ArchiveOptions::new().children(true); archive.open("dir")?.create_dir()?; expect!(archive.archive_with(temp_file.path(), "dir", &opts)) .to(be_err()) .to(equal(Error::NotADirectory { path: temp_file.path().into(), })); Ok(()) }) } // // `ArchiveOptions::recursive` // #[test] fn archive_non_recursively() -> sqlarfs::Result<()> { let temp_dir = tempfile::tempdir()?; fs::OpenOptions::new() .write(true) .create_new(true) .open(temp_dir.path().join("file"))?; connection()?.exec(|archive| { let opts = ArchiveOptions::new().recursive(false); expect!(archive.archive_with(temp_dir.path(), "dir", &opts)).to(be_ok()); let dir = archive.open("dir")?; expect!(dir.exists()).to(be_ok()).to(be_true()); let file = archive.open("dir/file")?; expect!(file.exists()).to(be_ok()).to(be_false()); Ok(()) }) } // // `ArchiveOptions::preserve_metadata` // #[test] fn archiving_does_not_preserve_file_mtime() -> sqlarfs::Result<()> { // Some time in the past. let expected_mtime = truncate_mtime(SystemTime::now() - Duration::from_secs(60)); let temp_file = tempfile::NamedTempFile::new()?; temp_file.as_file().set_modified(expected_mtime)?; connection()?.exec(|archive| { let opts = ArchiveOptions::new().preserve_metadata(false); expect!(archive.archive_with(temp_file.path(), "file", &opts)).to(be_ok()); let file = archive.open("file")?; expect!(file.metadata()) .to(be_ok()) .to(have_file_metadata()) .map(|metadata| metadata.mtime) .to(be_some()) .to_not(equal(expected_mtime)); expect!(file.metadata()) .to(be_ok()) .to(have_file_metadata()) .map(|metadata| metadata.mtime) .to(be_some()) .to(approx_eq_time(SystemTime::now(), Duration::from_secs(2))); Ok(()) }) } #[test] #[cfg(unix)] fn archiving_does_not_preserve_unix_file_mode() -> sqlarfs::Result<()> { use std::os::unix::fs::PermissionsExt; // These are unlikely to be the default permissions of a temporary file. let expected_mode = 0o444; let temp_file = tempfile::NamedTempFile::new()?; fs::set_permissions(temp_file.path(), fs::Permissions::from_mode(expected_mode))?; // A sanity check to guard against tests passing when they shouldn't. expect!(fs::metadata(temp_file.path())?) .map(|metadata| metadata.permissions().mode()) .to_not(equal(expected_mode)); connection()?.exec(|archive| { let opts = ArchiveOptions::new().preserve_metadata(false); expect!(archive.archive_with(temp_file.path(), "file", &opts)).to(be_ok()); let file = archive.open("file")?; expect!(file.metadata()) .to(be_ok()) .to(have_file_metadata()) .map(|metadata| metadata.mode) .to(be_some()) .to_not(equal(FileMode::from_bits_truncate(expected_mode))); Ok(()) }) } #[test] #[cfg(unix)] fn archiving_with_filesystem_loop_in_parent_errors() -> sqlarfs::Result<()> { use std::os::unix::fs::symlink; // The currently implementation uses recursion and will stack overflow if there's a filesystem // loop before it times out. However, we should still set a timeout in case this implementation // changes to one that doesn't use recursion. with_timeout(Duration::from_secs(1), || { let parent = tempfile::tempdir()?; // Create a symlink that points to its parent. symlink(parent.path(), parent.path().join("symlink"))?; connection()?.exec(|archive| { let opts = ArchiveOptions::new().follow_symlinks(true); expect!(archive.archive_with(parent.path(), "dest", &opts)) .to(be_err()) .to(equal(Error::FilesystemLoop)); Ok(()) }) }) } #[test] #[cfg(unix)] fn archiving_with_filesystem_loop_in_grandparent_errors() -> sqlarfs::Result<()> { use std::os::unix::fs::symlink; with_timeout(Duration::from_secs(1), || { let grandparent = tempfile::tempdir()?; let parent = grandparent.path().join("parent"); fs::create_dir(&parent)?; // Create a symlink that points to its grandparent. symlink(grandparent.path(), parent.join("symlink"))?; connection()?.exec(|archive| { let opts = ArchiveOptions::new().follow_symlinks(true); expect!(archive.archive_with(grandparent.path(), "dest", &opts)) .to(be_err()) .to(equal(Error::FilesystemLoop)); Ok(()) }) }) }