fs-encrypt 0.1.3

CLI tool for file encryption/decryption
Documentation
use std::{error::Error, fs, path::Path};

#[derive(Debug, PartialEq)]
pub struct FilePath {
    pub input_path: String,
    pub output_path: String,
}

#[derive(PartialEq)]
enum PathType {
    File,
    Directory,
}

fn get_pathtype(input_path: &str) -> Result<PathType, Box<dyn Error>> {
    let input_metadata = fs::metadata(input_path)?;

    if input_metadata.is_file() {
        return Ok(PathType::File);
    } else if input_metadata.is_dir() || input_metadata.is_symlink() {
        return Ok(PathType::Directory);
    }

    Err("input path is not an existing file or directory".into())
}

pub fn extend_dir_path<'a>(dir_path: &'a str, file_path: &'a str) -> String {
    format!("{}/{}", dir_path, file_path)
}

pub fn get_filepaths(
    input_path: String,
    output_path: String,
) -> Result<Vec<FilePath>, Box<dyn Error>> {
    let mut filepaths = Vec::<FilePath>::new();
    let pathtype = get_pathtype(&input_path)?;

    if pathtype == PathType::File {
        let path = FilePath {
            input_path,
            output_path,
        };
        filepaths.push(path);
        return Ok(filepaths);
    }

    let dirpaths = fs::read_dir(&input_path)?;
    for entry in dirpaths {
        let filename = match entry?.file_name().into_string() {
            Ok(filename) => filename,
            Err(_) => return Err("could not parse file".into()),
        };

        let ext_input_path = extend_dir_path(&input_path, &filename);
        let ext_output_path = extend_dir_path(&output_path, &filename);

        let mut paths = get_filepaths(ext_input_path, ext_output_path)?;
        filepaths.append(&mut paths);
    }

    Ok(filepaths)
}

pub fn read_file(filepath: &str) -> Result<Vec<u8>, Box<dyn Error>> {
    let contents = fs::read(filepath)?;

    Ok(contents)
}

pub fn enable_write_to_file(filepath: &str) -> Result<(), Box<dyn Error>> {
    let mut permissions = fs::metadata(filepath)?.permissions();
    permissions.set_readonly(false);

    fs::set_permissions(filepath, permissions)?;
    Ok(())
}

pub fn write_file(filepath: &str, contents: Vec<u8>) -> Result<(), Box<dyn Error>> {
    let path = Path::new(filepath);

    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)?;
    }

    let _  = enable_write_to_file(filepath);

    println!("Writing contents to {filepath}");
    fs::write(path, contents)?;

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_read_file_contents() {
        let contents = read_file("./tests/sample/test_file.txt").unwrap();
        let exp_contents = b"\
my text
second line\n"
            .to_vec();

        assert_eq!(contents, exp_contents);
    }

    #[test]
    fn test_write_file_only() {
        let contents = b"foo bar baz".to_vec();
        let path = "./tests/sample/written_file.txt";
        write_file(path, contents).unwrap();

        fs::remove_file(path).unwrap();
    }

    #[test]
    fn test_write_readonly_file() {
        let original_contents = b"foo bar baz".to_vec();
        let path = "./tests/sample/written_file.txt";
        write_file(path, original_contents).unwrap();

        let updated_contents = b"baz bar foo".to_vec();
        let mut permissions = fs::metadata(path).unwrap().permissions();
        permissions.set_readonly(true);
        fs::set_permissions(path, permissions).unwrap();
        write_file(path, updated_contents).unwrap();

        fs::remove_file(path).unwrap();
    }

    #[test]
    fn test_extend_dir_path() {
        let dir_path = "./tests/directory";
        let file_path = extend_dir_path(&dir_path, "foo/bar.txt");

        assert_eq!(file_path, "./tests/directory/foo/bar.txt");
    }

    #[test]
    fn test_write_file_with_dirs() {
        let contents = b"file contents".to_vec();
        let dir_path = "./tests/new_dir";
        let file_path = extend_dir_path(&dir_path, "new_file.txt");

        write_file(&file_path, contents.clone()).unwrap();

        let read_contents = read_file(&file_path).unwrap();
        assert_eq!(read_contents, contents);

        fs::remove_dir_all(&dir_path).unwrap();
    }

    #[test]
    fn test_get_filepaths() {
        let contents = b"".to_vec();

        let dir_path = String::from("./tests/filepaths");
        let input_path = extend_dir_path(&dir_path, "input");
        let output_path = extend_dir_path(&dir_path, "output");

        let inner_file_path = extend_dir_path(&input_path, "dir1/dir2/inner_file.txt");
        write_file(&inner_file_path, contents.clone()).unwrap();

        let base_file_path = extend_dir_path(&input_path, "base.txt");
        write_file(&base_file_path, contents.clone()).unwrap();

        let filepaths = get_filepaths(input_path, output_path).unwrap();

        assert_eq!(
            filepaths[0],
            FilePath {
                input_path: String::from("./tests/filepaths/input/dir1/dir2/inner_file.txt"),
                output_path: String::from("./tests/filepaths/output/dir1/dir2/inner_file.txt"),
            }
        );
        assert_eq!(
            filepaths[1],
            FilePath {
                input_path: String::from("./tests/filepaths/input/base.txt"),
                output_path: String::from("./tests/filepaths/output/base.txt"),
            }
        );

        fs::remove_dir_all(&dir_path).unwrap();
    }

    #[test]
    #[should_panic]
    fn test_nonexistent_input() {
        let input_path = String::from("./tests/non/existent/file.txt");
        let output_path = String::from("./tests");

        get_filepaths(input_path, output_path).unwrap();
    }
}