//! ## Host //! //! `host` is the module which provides functionalities to host file system /** * MIT License * * termscp - Copyright (c) 2021 Christian Visintin * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ // ext use std::fs::{self, File, Metadata, OpenOptions}; use std::path::{Path, PathBuf}; use std::time::SystemTime; use thiserror::Error; use wildmatch::WildMatch; // Metadata ext #[cfg(target_family = "unix")] use std::fs::set_permissions; #[cfg(target_family = "unix")] use std::os::unix::fs::{MetadataExt, PermissionsExt}; // Locals use crate::fs::{FsDirectory, FsEntry, FsFile, UnixPex}; /// ## HostErrorType /// /// HostErrorType provides an overview of the specific host error #[derive(Error, Debug)] pub enum HostErrorType { #[error("No such file or directory")] NoSuchFileOrDirectory, #[error("File is readonly")] ReadonlyFile, #[error("Could not access directory")] DirNotAccessible, #[error("Could not access file")] FileNotAccessible, #[error("File already exists")] FileAlreadyExists, #[error("Could not create file")] CouldNotCreateFile, #[error("Command execution failed")] ExecutionFailed, #[error("Could not delete file")] DeleteFailed, } /// ### HostError /// /// HostError is a wrapper for the error type and the exact io error #[derive(Debug)] pub struct HostError { pub error: HostErrorType, ioerr: Option, path: Option, } impl HostError { /// ### new /// /// Instantiates a new HostError pub(crate) fn new(error: HostErrorType, errno: Option, p: &Path) -> Self { HostError { error, ioerr: errno, path: Some(p.to_path_buf()), } } } impl From for HostError { fn from(error: HostErrorType) -> Self { HostError { error, ioerr: None, path: None, } } } impl std::fmt::Display for HostError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { let p_str: String = match self.path.as_ref() { None => String::new(), Some(p) => format!(" ({})", p.display().to_string()), }; match &self.ioerr { Some(err) => write!(f, "{}: {}{}", self.error, err, p_str), None => write!(f, "{}{}", self.error, p_str), } } } /// ## Localhost /// /// Localhost is the entity which holds the information about the current directory and host. /// It provides functions to navigate across the local host file system pub struct Localhost { wrkdir: PathBuf, files: Vec, } impl Localhost { /// ### new /// /// Instantiates a new Localhost struct pub fn new(wrkdir: PathBuf) -> Result { debug!("Initializing localhost at {}", wrkdir.display()); let mut host: Localhost = Localhost { wrkdir, files: Vec::new(), }; // Check if dir exists if !host.file_exists(host.wrkdir.as_path()) { error!( "Failed to initialize localhost: {} doesn't exist", host.wrkdir.display() ); return Err(HostError::new( HostErrorType::NoSuchFileOrDirectory, None, host.wrkdir.as_path(), )); } // Retrieve files for provided path host.files = match host.scan_dir(host.wrkdir.as_path()) { Ok(files) => files, Err(err) => { error!( "Failed to initialize localhost: could not scan wrkdir: {}", err ); return Err(err); } }; info!("Localhost initialized with success"); Ok(host) } /// ### pwd /// /// Print working directory pub fn pwd(&self) -> PathBuf { self.wrkdir.clone() } /// ### list_dir /// /// List files in current directory #[allow(dead_code)] pub fn list_dir(&self) -> Vec { self.files.clone() } /// ### change_wrkdir /// /// Change working directory with the new provided directory pub fn change_wrkdir(&mut self, new_dir: &Path) -> Result { let new_dir: PathBuf = self.to_abs_path(new_dir); info!("Changing localhost directory to {}...", new_dir.display()); // Check whether directory exists if !self.file_exists(new_dir.as_path()) { error!("Could not change directory: No such file or directory"); return Err(HostError::new( HostErrorType::NoSuchFileOrDirectory, None, new_dir.as_path(), )); } // Change directory if let Err(err) = std::env::set_current_dir(new_dir.as_path()) { error!("Could not enter directory: {}", err); return Err(HostError::new( HostErrorType::NoSuchFileOrDirectory, Some(err), new_dir.as_path(), )); } let prev_dir: PathBuf = self.wrkdir.clone(); // Backup location // Update working directory // Change dir self.wrkdir = new_dir; // Scan new directory self.files = match self.scan_dir(self.wrkdir.as_path()) { Ok(files) => files, Err(err) => { error!("Could not scan new directory: {}", err); // Restore directory self.wrkdir = prev_dir; return Err(err); } }; debug!("Changed directory to {}", self.wrkdir.display()); Ok(self.wrkdir.clone()) } /// ### mkdir /// /// Make a directory at path and update the file list (only if relative) pub fn mkdir(&mut self, dir_name: &Path) -> Result<(), HostError> { self.mkdir_ex(dir_name, false) } /// ### mkdir_ex /// /// Extended option version of makedir. /// ignex: don't report error if directory already exists pub fn mkdir_ex(&mut self, dir_name: &Path, ignex: bool) -> Result<(), HostError> { let dir_path: PathBuf = self.to_abs_path(dir_name); info!("Making directory {}", dir_path.display()); // If dir already exists, return Error if dir_path.exists() { match ignex { true => return Ok(()), false => { return Err(HostError::new( HostErrorType::FileAlreadyExists, None, dir_path.as_path(), )) } } } match std::fs::create_dir(dir_path.as_path()) { Ok(_) => { // Update dir if dir_name.is_relative() { self.files = self.scan_dir(self.wrkdir.as_path())?; } info!("Created directory {}", dir_path.display()); Ok(()) } Err(err) => { error!("Could not make directory: {}", err); Err(HostError::new( HostErrorType::CouldNotCreateFile, Some(err), dir_path.as_path(), )) } } } /// ### remove /// /// Remove file entry pub fn remove(&mut self, entry: &FsEntry) -> Result<(), HostError> { match entry { FsEntry::Directory(dir) => { // If file doesn't exist; return error debug!("Removing directory {}", dir.abs_path.display()); if !dir.abs_path.as_path().exists() { error!("Directory doesn't exist"); return Err(HostError::new( HostErrorType::NoSuchFileOrDirectory, None, dir.abs_path.as_path(), )); } // Remove match std::fs::remove_dir_all(dir.abs_path.as_path()) { Ok(_) => { // Update dir self.files = self.scan_dir(self.wrkdir.as_path())?; info!("Removed directory {}", dir.abs_path.display()); Ok(()) } Err(err) => { error!("Could not remove directory: {}", err); Err(HostError::new( HostErrorType::DeleteFailed, Some(err), dir.abs_path.as_path(), )) } } } FsEntry::File(file) => { // If file doesn't exist; return error debug!("Removing file {}", file.abs_path.display()); if !file.abs_path.as_path().exists() { error!("File doesn't exist"); return Err(HostError::new( HostErrorType::NoSuchFileOrDirectory, None, file.abs_path.as_path(), )); } // Remove match std::fs::remove_file(file.abs_path.as_path()) { Ok(_) => { // Update dir self.files = self.scan_dir(self.wrkdir.as_path())?; info!("Removed file {}", file.abs_path.display()); Ok(()) } Err(err) => { error!("Could not remove file: {}", err); Err(HostError::new( HostErrorType::DeleteFailed, Some(err), file.abs_path.as_path(), )) } } } } } /// ### rename /// /// Rename file or directory to new name pub fn rename(&mut self, entry: &FsEntry, dst_path: &Path) -> Result<(), HostError> { let abs_path: PathBuf = entry.get_abs_path(); match std::fs::rename(abs_path.as_path(), dst_path) { Ok(_) => { // Scan dir self.files = self.scan_dir(self.wrkdir.as_path())?; debug!( "Moved file {} to {}", entry.get_abs_path().display(), dst_path.display() ); Ok(()) } Err(err) => { error!( "Failed to move {} to {}: {}", entry.get_abs_path().display(), dst_path.display(), err ); Err(HostError::new( HostErrorType::CouldNotCreateFile, Some(err), abs_path.as_path(), )) } } } /// ### copy /// /// Copy file to destination path pub fn copy(&mut self, entry: &FsEntry, dst: &Path) -> Result<(), HostError> { // Get absolute path of dest let dst: PathBuf = self.to_abs_path(dst); info!( "Copying file {} to {}", entry.get_abs_path().display(), dst.display() ); // Match entry match entry { FsEntry::File(file) => { // Copy file // If destination path is a directory, push file name let dst: PathBuf = match dst.as_path().is_dir() { true => { let mut p: PathBuf = dst.clone(); p.push(file.name.as_str()); p } false => dst.clone(), }; // Copy entry path to dst path if let Err(err) = std::fs::copy(file.abs_path.as_path(), dst.as_path()) { error!("Failed to copy file: {}", err); return Err(HostError::new( HostErrorType::CouldNotCreateFile, Some(err), file.abs_path.as_path(), )); } info!("File copied"); } FsEntry::Directory(dir) => { // If destination path doesn't exist, create destination if !dst.exists() { debug!("Directory {} doesn't exist; creating it", dst.display()); self.mkdir(dst.as_path())?; } // Scan dir let dir_files: Vec = self.scan_dir(dir.abs_path.as_path())?; // Iterate files for dir_entry in dir_files.iter() { // Calculate dst let mut sub_dst: PathBuf = dst.clone(); sub_dst.push(dir_entry.get_name()); // Call function recursively self.copy(dir_entry, sub_dst.as_path())?; } } } // Reload directory if dst is pwd match dst.is_dir() { true => { if dst == self.pwd().as_path() { self.files = self.scan_dir(self.wrkdir.as_path())?; } else if let Some(parent) = dst.parent() { // If parent is pwd, scan directory if parent == self.pwd().as_path() { self.files = self.scan_dir(self.wrkdir.as_path())?; } } } false => { if let Some(parent) = dst.parent() { // If parent is pwd, scan directory if parent == self.pwd().as_path() { self.files = self.scan_dir(self.wrkdir.as_path())?; } } } } Ok(()) } /// ### stat /// /// Stat file and create a FsEntry #[cfg(target_family = "unix")] pub fn stat(&self, path: &Path) -> Result { info!("Stating file {}", path.display()); let path: PathBuf = self.to_abs_path(path); let attr: Metadata = match fs::metadata(path.as_path()) { Ok(metadata) => metadata, Err(err) => { error!("Could not read file metadata: {}", err); return Err(HostError::new( HostErrorType::FileNotAccessible, Some(err), path.as_path(), )); } }; let file_name: String = String::from(path.file_name().unwrap().to_str().unwrap_or("")); // Match dir / file Ok(match path.is_dir() { true => FsEntry::Directory(FsDirectory { name: file_name, abs_path: path.clone(), last_change_time: attr.modified().unwrap_or(SystemTime::UNIX_EPOCH), last_access_time: attr.accessed().unwrap_or(SystemTime::UNIX_EPOCH), creation_time: attr.created().unwrap_or(SystemTime::UNIX_EPOCH), symlink: match fs::read_link(path.as_path()) { Ok(p) => match self.stat(p.as_path()) { Ok(entry) => Some(Box::new(entry)), Err(_) => None, }, Err(_) => None, }, user: Some(attr.uid()), group: Some(attr.gid()), unix_pex: Some(self.u32_to_mode(attr.mode())), }), false => { // Is File let extension: Option = path .extension() .map(|s| String::from(s.to_str().unwrap_or(""))); FsEntry::File(FsFile { name: file_name, abs_path: path.clone(), last_change_time: attr.modified().unwrap_or(SystemTime::UNIX_EPOCH), last_access_time: attr.accessed().unwrap_or(SystemTime::UNIX_EPOCH), creation_time: attr.created().unwrap_or(SystemTime::UNIX_EPOCH), size: attr.len() as usize, ftype: extension, symlink: match fs::read_link(path.as_path()) { Ok(p) => match self.stat(p.as_path()) { Ok(entry) => Some(Box::new(entry)), Err(_) => None, }, Err(_) => None, // Ignore errors }, user: Some(attr.uid()), group: Some(attr.gid()), unix_pex: Some(self.u32_to_mode(attr.mode())), }) } }) } /// ### stat /// /// Stat file and create a FsEntry #[cfg(target_os = "windows")] pub fn stat(&self, path: &Path) -> Result { let path: PathBuf = self.to_abs_path(path); info!("Stating file {}", path.display()); let attr: Metadata = match fs::metadata(path.as_path()) { Ok(metadata) => metadata, Err(err) => { error!("Could not read file metadata: {}", err); return Err(HostError::new( HostErrorType::FileNotAccessible, Some(err), path.as_path(), )); } }; let file_name: String = String::from(path.file_name().unwrap().to_str().unwrap_or("")); // Match dir / file Ok(match path.is_dir() { true => FsEntry::Directory(FsDirectory { name: file_name, abs_path: path.clone(), last_change_time: attr.modified().unwrap_or(SystemTime::UNIX_EPOCH), last_access_time: attr.accessed().unwrap_or(SystemTime::UNIX_EPOCH), creation_time: attr.created().unwrap_or(SystemTime::UNIX_EPOCH), symlink: match fs::read_link(path.as_path()) { Ok(p) => match self.stat(p.as_path()) { Ok(entry) => Some(Box::new(entry)), Err(_) => None, // Ignore errors }, Err(_) => None, }, user: None, group: None, unix_pex: None, }), false => { // Is File let extension: Option = match path.extension() { Some(s) => Some(String::from(s.to_str().unwrap_or(""))), None => None, }; FsEntry::File(FsFile { name: file_name, abs_path: path.clone(), last_change_time: attr.modified().unwrap_or(SystemTime::UNIX_EPOCH), last_access_time: attr.accessed().unwrap_or(SystemTime::UNIX_EPOCH), creation_time: attr.created().unwrap_or(SystemTime::UNIX_EPOCH), size: attr.len() as usize, ftype: extension, symlink: match fs::read_link(path.as_path()) { Ok(p) => match self.stat(p.as_path()) { Ok(entry) => Some(Box::new(entry)), Err(_) => None, }, Err(_) => None, }, user: None, group: None, unix_pex: None, }) } }) } /// ### exec /// /// Execute a command on localhost pub fn exec(&self, cmd: &str) -> Result { // Make command let args: Vec<&str> = cmd.split(' ').collect(); let cmd: &str = args.first().unwrap(); let argv: &[&str] = &args[1..]; info!("Executing command: {} {:?}", cmd, argv); match std::process::Command::new(cmd).args(argv).output() { Ok(output) => match std::str::from_utf8(&output.stdout) { Ok(s) => { info!("Command output: {}", s); Ok(s.to_string()) } Err(_) => Ok(String::new()), }, Err(err) => { error!("Failed to run command: {}", err); Err(HostError::new( HostErrorType::ExecutionFailed, Some(err), self.wrkdir.as_path(), )) } } } /// ### chmod /// /// Change file mode to file, according to UNIX permissions #[cfg(target_family = "unix")] pub fn chmod(&self, path: &Path, pex: (u8, u8, u8)) -> Result<(), HostError> { let path: PathBuf = self.to_abs_path(path); // Get metadta match fs::metadata(path.as_path()) { Ok(metadata) => { let mut mpex = metadata.permissions(); mpex.set_mode(self.mode_to_u32(pex)); match set_permissions(path.as_path(), mpex) { Ok(_) => { info!("Changed mode for {} to {:?}", path.display(), pex); Ok(()) } Err(err) => { error!("Could not change mode for file {}: {}", path.display(), err); Err(HostError::new( HostErrorType::FileNotAccessible, Some(err), path.as_path(), )) } } } Err(err) => { error!( "Chmod failed; could not read metadata for file {}: {}", path.display(), err ); Err(HostError::new( HostErrorType::FileNotAccessible, Some(err), path.as_path(), )) } } } /// ### open_file_read /// /// Open file for read pub fn open_file_read(&self, file: &Path) -> Result { let file: PathBuf = self.to_abs_path(file); info!("Opening file {} for read", file.display()); if !self.file_exists(file.as_path()) { error!("File doesn't exist!"); return Err(HostError::new( HostErrorType::NoSuchFileOrDirectory, None, file.as_path(), )); } match OpenOptions::new() .create(false) .read(true) .write(false) .open(file.as_path()) { Ok(f) => Ok(f), Err(err) => { error!("Could not open file for read: {}", err); Err(HostError::new( HostErrorType::FileNotAccessible, Some(err), file.as_path(), )) } } } /// ### open_file_write /// /// Open file for write pub fn open_file_write(&self, file: &Path) -> Result { let file: PathBuf = self.to_abs_path(file); info!("Opening file {} for write", file.display()); match OpenOptions::new() .create(true) .write(true) .truncate(true) .open(file.as_path()) { Ok(f) => Ok(f), Err(err) => { error!("Failed to open file: {}", err); match self.file_exists(file.as_path()) { true => Err(HostError::new( HostErrorType::ReadonlyFile, Some(err), file.as_path(), )), false => Err(HostError::new( HostErrorType::FileNotAccessible, Some(err), file.as_path(), )), } } } } /// ### file_exists /// /// Returns whether provided file path exists pub fn file_exists(&self, path: &Path) -> bool { path.exists() } /// ### scan_dir /// /// Get content of the current directory as a list of fs entry pub fn scan_dir(&self, dir: &Path) -> Result, HostError> { info!("Reading directory {}", dir.display()); match std::fs::read_dir(dir) { Ok(e) => { let mut fs_entries: Vec = Vec::new(); for entry in e.flatten() { // NOTE: 0.4.1, don't fail if stat for one file fails match self.stat(entry.path().as_path()) { Ok(entry) => fs_entries.push(entry), Err(e) => error!("Failed to stat {}: {}", entry.path().display(), e), } } Ok(fs_entries) } Err(err) => Err(HostError::new( HostErrorType::DirNotAccessible, Some(err), dir, )), } } /// ### find /// /// Find files matching `search` on localhost starting from current directory. Search supports recursive search of course. /// The `search` argument supports wilcards ('*', '?') pub fn find(&self, search: &str) -> Result, HostError> { self.iter_search(self.wrkdir.as_path(), &WildMatch::new(search)) } // -- privates /// ### iter_search /// /// Recursive call for `find` method. /// Search in current directory for files which match `filter`. /// If a directory is found in current directory, `iter_search` will be called using that dir as argument. fn iter_search(&self, dir: &Path, filter: &WildMatch) -> Result, HostError> { // Scan directory let mut drained: Vec = Vec::new(); match self.scan_dir(dir) { Err(err) => Err(err), Ok(entries) => { // Iter entries /* For each entry: - if is dir: call iter_search with `dir` - push `iter_search` result to `drained` - if is file: check if it matches `filter` - if it matches `filter`: push to to filter */ for entry in entries.iter() { match entry { FsEntry::Directory(dir) => { // If directory matches; push directory to drained if filter.matches(dir.name.as_str()) { drained.push(FsEntry::Directory(dir.clone())); } drained.append(&mut self.iter_search(dir.abs_path.as_path(), filter)?); } FsEntry::File(file) => { if filter.matches(file.name.as_str()) { drained.push(FsEntry::File(file.clone())); } } } } Ok(drained) } } } /// ### u32_to_mode /// /// Return string with format xxxxxx to tuple of permissions (user, group, others) #[cfg(target_family = "unix")] fn u32_to_mode(&self, mode: u32) -> (UnixPex, UnixPex, UnixPex) { let user: UnixPex = UnixPex::from(((mode >> 6) & 0x7) as u8); let group: UnixPex = UnixPex::from(((mode >> 3) & 0x7) as u8); let others: UnixPex = UnixPex::from((mode & 0x7) as u8); (user, group, others) } /// mode_to_u32 /// /// Convert owner,group,others to u32 #[cfg(target_family = "unix")] fn mode_to_u32(&self, mode: (u8, u8, u8)) -> u32 { ((mode.0 as u32) << 6) + ((mode.1 as u32) << 3) + mode.2 as u32 } /// ### to_abs_path /// /// Convert path to absolute path fn to_abs_path(&self, p: &Path) -> PathBuf { // Convert to abs path match p.is_relative() { true => { let mut path: PathBuf = self.wrkdir.clone(); path.push(p); path } false => PathBuf::from(p), } } } #[cfg(test)] mod tests { use super::*; #[cfg(target_family = "unix")] use crate::utils::test_helpers::{create_sample_file, make_fsentry}; use crate::utils::test_helpers::{make_dir_at, make_file_at}; use pretty_assertions::assert_eq; #[cfg(target_family = "unix")] use std::fs::File; #[cfg(target_family = "unix")] use std::io::Write; #[cfg(target_family = "unix")] use std::os::unix::fs::{symlink, PermissionsExt}; #[test] fn test_host_error_new() { let error: HostError = HostError::new(HostErrorType::CouldNotCreateFile, None, Path::new("/tmp")); assert!(error.ioerr.is_none()); assert_eq!(error.path.as_ref().unwrap(), Path::new("/tmp")); } #[test] #[cfg(target_family = "unix")] fn test_host_localhost_new() { let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); assert_eq!(host.wrkdir, PathBuf::from("/dev")); // Scan dir let entries = std::fs::read_dir(PathBuf::from("/dev").as_path()).unwrap(); let mut counter: usize = 0; for _ in entries { counter = counter + 1; } assert_eq!(host.files.len(), counter); } #[test] #[cfg(target_os = "windows")] fn test_host_localhost_new() { let host: Localhost = Localhost::new(PathBuf::from("C:\\users")).ok().unwrap(); assert_eq!(host.wrkdir, PathBuf::from("C:\\users")); // Scan dir let entries = std::fs::read_dir(PathBuf::from("C:\\users").as_path()).unwrap(); let mut counter: usize = 0; for _ in entries { counter = counter + 1; } assert_eq!(host.files.len(), counter); } #[test] #[should_panic] fn test_host_localhost_new_bad() { Localhost::new(PathBuf::from("/omargabber/123/345")) .ok() .unwrap(); } #[test] #[cfg(target_family = "unix")] fn test_host_localhost_pwd() { let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); assert_eq!(host.pwd(), PathBuf::from("/dev")); } #[test] #[cfg(target_family = "unix")] fn test_host_localhost_list_files() { let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); // Scan dir let entries = std::fs::read_dir(PathBuf::from("/dev").as_path()).unwrap(); let mut counter: usize = 0; for _ in entries { counter = counter + 1; } assert_eq!(host.list_dir().len(), counter); } #[test] #[cfg(target_family = "unix")] fn test_host_localhost_change_dir() { let mut host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); let new_dir: PathBuf = PathBuf::from("/dev"); assert!(host.change_wrkdir(new_dir.as_path()).is_ok()); // Verify new files // Scan dir let entries = std::fs::read_dir(PathBuf::from(new_dir).as_path()).unwrap(); let mut counter: usize = 0; for _ in entries { counter = counter + 1; } assert_eq!(host.files.len(), counter); } #[test] #[cfg(target_family = "unix")] #[should_panic] fn test_host_localhost_change_dir_failed() { let mut host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); let new_dir: PathBuf = PathBuf::from("/omar/gabber/123/456"); assert!(host.change_wrkdir(new_dir.as_path()).is_ok()); } #[test] #[cfg(target_family = "unix")] fn test_host_localhost_open_read() { let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); // Create temp file let file: tempfile::NamedTempFile = create_sample_file(); assert!(host.open_file_read(file.path()).is_ok()); } #[test] #[cfg(target_family = "unix")] #[should_panic] fn test_host_localhost_open_read_err_no_such_file() { let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); assert!(host .open_file_read(PathBuf::from("/bin/foo-bar-test-omar-123-456-789.txt").as_path()) .is_ok()); } #[test] #[cfg(any(target_os = "macos", target_os = "linux"))] fn test_host_localhost_open_read_err_not_accessible() { let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); let file: tempfile::NamedTempFile = create_sample_file(); //let mut perms = fs::metadata(file.path())?.permissions(); fs::set_permissions(file.path(), PermissionsExt::from_mode(0o222)).unwrap(); //fs::set_permissions(file.path(), perms)?; assert!(host.open_file_read(file.path()).is_err()); } #[test] #[cfg(target_family = "unix")] fn test_host_localhost_open_write() { let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); // Create temp file let file: tempfile::NamedTempFile = create_sample_file(); assert!(host.open_file_write(file.path()).is_ok()); } #[test] #[cfg(any(target_os = "macos", target_os = "linux"))] fn test_host_localhost_open_write_err() { let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); let file: tempfile::NamedTempFile = create_sample_file(); //let mut perms = fs::metadata(file.path())?.permissions(); fs::set_permissions(file.path(), PermissionsExt::from_mode(0o444)).unwrap(); //fs::set_permissions(file.path(), perms)?; assert!(host.open_file_write(file.path()).is_err()); } #[cfg(target_family = "unix")] #[test] fn test_host_localhost_symlinks() { let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); // Create sample file assert!(File::create(format!("{}/foo.txt", tmpdir.path().display()).as_str()).is_ok()); // Create symlink assert!(symlink( format!("{}/foo.txt", tmpdir.path().display()), format!("{}/bar.txt", tmpdir.path().display()) ) .is_ok()); // Get dir let host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); let files: Vec = host.list_dir(); // Verify files let file_0: &FsEntry = files.get(0).unwrap(); match file_0 { FsEntry::File(file_0) => { if file_0.name == String::from("foo.txt") { assert!(file_0.symlink.is_none()); } else { assert_eq!( *file_0.symlink.as_ref().unwrap().get_abs_path(), PathBuf::from(format!("{}/foo.txt", tmpdir.path().display())) ); } } _ => panic!("expected entry 0 to be file: {:?}", file_0), }; // Verify simlink let file_1: &FsEntry = files.get(1).unwrap(); match file_1 { FsEntry::File(file_1) => { if file_1.name == String::from("bar.txt") { assert_eq!( *file_1.symlink.as_ref().unwrap().get_abs_path(), PathBuf::from(format!("{}/foo.txt", tmpdir.path().display())) ); } else { assert!(file_1.symlink.is_none()); } } _ => panic!("expected entry 0 to be file: {:?}", file_1), }; } #[test] #[cfg(target_family = "unix")] fn test_host_localhost_mkdir() { let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); let files: Vec = host.list_dir(); assert_eq!(files.len(), 0); // There should be 0 files now assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_ok()); let files: Vec = host.list_dir(); assert_eq!(files.len(), 1); // There should be 1 file now // Try to re-create directory assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_err()); // Try abs path assert!(host .mkdir_ex(PathBuf::from("/tmp/test_dir_123456789").as_path(), true) .is_ok()); // Fail assert!(host .mkdir_ex( PathBuf::from("/aaaa/oooooo/tmp/test_dir_123456789").as_path(), true ) .is_err()); } #[test] #[cfg(target_family = "unix")] fn test_host_localhost_remove() { let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); // Create sample file assert!(File::create(format!("{}/foo.txt", tmpdir.path().display()).as_str()).is_ok()); let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); let files: Vec = host.list_dir(); assert_eq!(files.len(), 1); // There should be 1 file now // Remove file assert!(host.remove(files.get(0).unwrap()).is_ok()); // There should be 0 files now let files: Vec = host.list_dir(); assert_eq!(files.len(), 0); // There should be 0 files now // Create directory assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_ok()); // Delete directory let files: Vec = host.list_dir(); assert_eq!(files.len(), 1); // There should be 1 file now assert!(host.remove(files.get(0).unwrap()).is_ok()); // Remove unexisting directory assert!(host .remove(&make_fsentry(PathBuf::from("/a/b/c/d"), true)) .is_err()); assert!(host .remove(&make_fsentry(PathBuf::from("/aaaaaaa"), false)) .is_err()); } #[test] #[cfg(target_family = "unix")] fn test_host_localhost_rename() { let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); // Create sample file let src_path: PathBuf = PathBuf::from(format!("{}/foo.txt", tmpdir.path().display()).as_str()); assert!(File::create(src_path.as_path()).is_ok()); let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); let files: Vec = host.list_dir(); assert_eq!(files.len(), 1); // There should be 1 file now assert_eq!(files.get(0).unwrap().get_name(), "foo.txt"); // Rename file let dst_path: PathBuf = PathBuf::from(format!("{}/bar.txt", tmpdir.path().display()).as_str()); assert!(host .rename(files.get(0).unwrap(), dst_path.as_path()) .is_ok()); // There should be still 1 file now, but named bar.txt let files: Vec = host.list_dir(); assert_eq!(files.len(), 1); // There should be 0 files now assert_eq!(files.get(0).unwrap().get_name(), "bar.txt"); // Fail let bad_path: PathBuf = PathBuf::from("/asdailsjoidoewojdijow/ashdiuahu"); assert!(host .rename(files.get(0).unwrap(), bad_path.as_path()) .is_err()); } #[cfg(target_family = "unix")] #[test] fn test_host_chmod() { let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); let file: tempfile::NamedTempFile = create_sample_file(); let host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); // mode_to_u32 assert_eq!(host.mode_to_u32((6, 4, 4)), 0o644); assert_eq!(host.mode_to_u32((7, 7, 5)), 0o775); // Chmod to file assert!(host.chmod(file.path(), (7, 7, 5)).is_ok()); // Chmod to dir assert!(host.chmod(tmpdir.path(), (7, 5, 0)).is_ok()); // Error assert!(host .chmod(Path::new("/tmp/krgiogoiegj/kwrgnoerig"), (7, 7, 7)) .is_err()); } #[cfg(target_family = "unix")] #[test] fn test_host_copy_file_absolute() { let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); // Create file in tmpdir let mut file1_path: PathBuf = PathBuf::from(tmpdir.path()); file1_path.push("foo.txt"); // Write file 1 let mut file1: File = File::create(file1_path.as_path()).ok().unwrap(); assert!(file1.write_all(b"Hello world!\n").is_ok()); // Get file 2 path let mut file2_path: PathBuf = PathBuf::from(tmpdir.path()); file2_path.push("bar.txt"); // Create host let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); let file1_entry: FsEntry = host.files.get(0).unwrap().clone(); assert_eq!(file1_entry.get_name(), String::from("foo.txt")); // Copy assert!(host.copy(&file1_entry, file2_path.as_path()).is_ok()); // Verify host has two files assert_eq!(host.files.len(), 2); // Fail copy assert!(host .copy( &make_fsentry(PathBuf::from("/a/a7/a/a7a"), false), PathBuf::from("571k422i").as_path() ) .is_err()); } #[cfg(target_family = "unix")] #[test] fn test_host_copy_file_relative() { let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); // Create file in tmpdir let mut file1_path: PathBuf = PathBuf::from(tmpdir.path()); file1_path.push("foo.txt"); // Write file 1 let mut file1: File = File::create(file1_path.as_path()).ok().unwrap(); assert!(file1.write_all(b"Hello world!\n").is_ok()); // Get file 2 path let file2_path: PathBuf = PathBuf::from("bar.txt"); // Create host let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); let file1_entry: FsEntry = host.files.get(0).unwrap().clone(); assert_eq!(file1_entry.get_name(), String::from("foo.txt")); // Copy assert!(host.copy(&file1_entry, file2_path.as_path()).is_ok()); // Verify host has two files assert_eq!(host.files.len(), 2); } #[cfg(target_family = "unix")] #[test] fn test_host_copy_directory_absolute() { let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); // Create directory in tmpdir let mut dir_src: PathBuf = PathBuf::from(tmpdir.path()); dir_src.push("test_dir/"); assert!(std::fs::create_dir(dir_src.as_path()).is_ok()); // Create file in src dir let mut file1_path: PathBuf = dir_src.clone(); file1_path.push("foo.txt"); // Write file 1 let mut file1: File = File::create(file1_path.as_path()).ok().unwrap(); assert!(file1.write_all(b"Hello world!\n").is_ok()); // Copy dir src to dir ddest let mut dir_dest: PathBuf = PathBuf::from(tmpdir.path()); dir_dest.push("test_dest_dir/"); // Create host let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); let dir_src_entry: FsEntry = host.files.get(0).unwrap().clone(); assert_eq!(dir_src_entry.get_name(), String::from("test_dir")); // Copy assert!(host.copy(&dir_src_entry, dir_dest.as_path()).is_ok()); // Verify host has two files assert_eq!(host.files.len(), 2); // Verify dir_dest contains foo.txt let mut test_file_path: PathBuf = dir_dest.clone(); test_file_path.push("foo.txt"); assert!(host.stat(test_file_path.as_path()).is_ok()); } #[cfg(target_family = "unix")] #[test] fn test_host_copy_directory_relative() { let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); // Create directory in tmpdir let mut dir_src: PathBuf = PathBuf::from(tmpdir.path()); dir_src.push("test_dir/"); assert!(std::fs::create_dir(dir_src.as_path()).is_ok()); // Create file in src dir let mut file1_path: PathBuf = dir_src.clone(); file1_path.push("foo.txt"); // Write file 1 let mut file1: File = File::create(file1_path.as_path()).ok().unwrap(); assert!(file1.write_all(b"Hello world!\n").is_ok()); // Copy dir src to dir ddest let dir_dest: PathBuf = PathBuf::from("test_dest_dir/"); // Create host let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); let dir_src_entry: FsEntry = host.files.get(0).unwrap().clone(); assert_eq!(dir_src_entry.get_name(), String::from("test_dir")); // Copy assert!(host.copy(&dir_src_entry, dir_dest.as_path()).is_ok()); // Verify host has two files assert_eq!(host.files.len(), 2); // Verify dir_dest contains foo.txt let mut test_file_path: PathBuf = dir_dest.clone(); test_file_path.push("foo.txt"); assert!(host.stat(test_file_path.as_path()).is_ok()); } #[test] fn test_host_exec() { let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); let host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); // Execute #[cfg(target_family = "unix")] assert_eq!(host.exec("echo 5").ok().unwrap().as_str(), "5\n"); #[cfg(target_os = "windows")] assert_eq!(host.exec("echo 5").ok().unwrap().as_str(), "5\r\n"); } #[test] fn test_host_find() { let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); let dir_path: &Path = tmpdir.path(); // Make files assert!(make_file_at(dir_path, "pippo.txt").is_ok()); assert!(make_file_at(dir_path, "foo.jpg").is_ok()); // Make nested struct assert!(make_dir_at(dir_path, "examples").is_ok()); let mut subdir: PathBuf = PathBuf::from(dir_path); subdir.push("examples/"); assert!(make_file_at(subdir.as_path(), "omar.txt").is_ok()); assert!(make_file_at(subdir.as_path(), "errors.txt").is_ok()); assert!(make_file_at(subdir.as_path(), "screenshot.png").is_ok()); assert!(make_file_at(subdir.as_path(), "examples.csv").is_ok()); let host: Localhost = Localhost::new(PathBuf::from(dir_path)).ok().unwrap(); // Find txt files let mut result: Vec = host.find("*.txt").ok().unwrap(); result.sort_by_key(|x: &FsEntry| x.get_name().to_lowercase()); // There should be 3 entries assert_eq!(result.len(), 3); // Check names (they should be sorted alphabetically already; NOTE: examples/ comes before pippo.txt) assert_eq!(result[0].get_name(), "errors.txt"); assert_eq!(result[1].get_name(), "omar.txt"); assert_eq!(result[2].get_name(), "pippo.txt"); // Search for directory let mut result: Vec = host.find("examples*").ok().unwrap(); result.sort_by_key(|x: &FsEntry| x.get_name().to_lowercase()); assert_eq!(result.len(), 2); assert_eq!(result[0].get_name(), "examples"); assert_eq!(result[1].get_name(), "examples.csv"); } #[test] fn test_host_fmt_error() { let err: HostError = HostError::new( HostErrorType::CouldNotCreateFile, Some(std::io::Error::from(std::io::ErrorKind::AddrInUse)), Path::new("/tmp"), ); assert_eq!( format!("{}", err), String::from("Could not create file: address in use (/tmp)"), ); assert_eq!( format!("{}", HostError::from(HostErrorType::DeleteFailed)), String::from("Could not delete file") ); assert_eq!( format!("{}", HostError::from(HostErrorType::ExecutionFailed)), String::from("Command execution failed"), ); assert_eq!( format!("{}", HostError::from(HostErrorType::DirNotAccessible)), String::from("Could not access directory"), ); assert_eq!( format!("{}", HostError::from(HostErrorType::NoSuchFileOrDirectory)), String::from("No such file or directory") ); assert_eq!( format!("{}", HostError::from(HostErrorType::ReadonlyFile)), String::from("File is readonly") ); assert_eq!( format!("{}", HostError::from(HostErrorType::FileNotAccessible)), String::from("Could not access file") ); assert_eq!( format!("{}", HostError::from(HostErrorType::FileAlreadyExists)), String::from("File already exists") ); } }