onetime-cli 0.4.1

Encrypt / decrypt files using the one-time pad.
Documentation
use assert_cmd::Command;
use md5_rs::Context;
use scopeguard::defer;
use std::ffi::OsString;
use std::fs::File;
use std::fs::{copy, create_dir, create_dir_all, metadata, read_dir, remove_dir_all, remove_file};
use std::io::{ErrorKind, Read};

const CARGO_BIN_NAME: &str = "onetime-cli";

const FILES_DIR: &str = "./tests/files";
const FILES_ORIG_DIR: &str = "./tests/files_original";

fn copy_files(input_dir: &str, dest_dir: &str) -> std::io::Result<()> {
    fn traverse_dir(dir: &str, dest_dir: &OsString) -> std::io::Result<()> {
        for entry in read_dir(dir)? {
            let entry = entry?;
            let path = entry.path();

            if path.is_dir() {
                let path_str = path.to_str().unwrap();
                let new_dest_dir = {
                    let dir_name = path.file_name().unwrap();

                    let mut dir = dest_dir.to_owned();
                    dir.push("/");
                    dir.push(dir_name);

                    dir
                };

                match create_dir(&new_dest_dir) {
                    Ok(_) => {}
                    Err(e) => match e.kind() {
                        ErrorKind::AlreadyExists => (),
                        _ => {
                            return Err(e);
                        }
                    },
                };

                traverse_dir(path_str, &new_dest_dir)?
            } else if path.is_file() {
                let mut new_file = dest_dir.to_owned();
                new_file.push("/");
                new_file.push(path.file_name().unwrap());

                copy(path, new_file)?;
            }
        }

        Ok(())
    }

    let orig_dir_name = format!("{FILES_ORIG_DIR}/{input_dir}");
    let dest_dir = format!("{FILES_DIR}/{dest_dir}");

    clear_files(&dest_dir);

    if let Err(e) = create_dir_all(&dest_dir) {
        match e.kind() {
            ErrorKind::AlreadyExists => (),
            _ => {
                panic!("{dest_dir}: {e}");
            }
        }
    };

    traverse_dir(&orig_dir_name, &OsString::from(dest_dir))?;

    Ok(())
}

fn clear_files(dir_name: &str) {
    let dir_name = format!("{FILES_DIR}/{dir_name}");

    if let Err(e) = remove_dir_all(dir_name) {
        match e.kind() {
            ErrorKind::NotFound => (),
            _ => {
                panic!("{}", e);
            }
        }
    }
}

fn get_md5_sum(path: &str) -> Result<[u8; 16], std::io::Error> {
    let mut ctx = Context::new();

    let mut file = File::open(path)?;
    let mut buffer = [0u8; 4096];

    loop {
        let bytes = file.read(&mut buffer)?;

        if bytes == 0 {
            break;
        }

        ctx.read(&buffer[..bytes]);
    }

    Ok(ctx.finish())
}

fn assert_path_exists(path: &str) {
    if let Err(e) = metadata(path) {
        panic!("{path}: {e}");
    }
}

fn assert_path_not_found(path: &str) {
    if let Err(e) = metadata(path) {
        match e.kind() {
            ErrorKind::NotFound => (),
            _ => panic!("{path}: {e}"),
        }
    } else {
        panic!("{path} exists");
    }
}

#[test]
fn test_encrypt_decrypt() {
    copy_files("files_1", "test_encrypt_decrypt").unwrap();

    defer! {
        clear_files("test_encrypt_decrypt")
    }

    let original_md5 = get_md5_sum("./tests/files/test_encrypt_decrypt/file1.txt").unwrap();

    // Encrypt command
    let mut cmd = Command::cargo_bin(CARGO_BIN_NAME).unwrap();
    let assert = cmd
        .current_dir("./tests/files/test_encrypt_decrypt")
        .arg("encrypt")
        .arg("file1.txt")
        .assert();

    assert
        .success()
        .stdout("Successfully encrypted file1.txt\n")
        .stderr("");

    remove_file("./tests/files/test_encrypt_decrypt/file1.txt").unwrap();

    // Decrypt command
    let mut cmd = Command::cargo_bin(CARGO_BIN_NAME).unwrap();
    let assert = cmd
        .current_dir("./tests/files/test_encrypt_decrypt")
        .arg("decrypt")
        .arg("file1.txt")
        .assert();

    assert
        .success()
        .stdout("Successfully decrypted file1.txt\n")
        .stderr("");

    let md5_now = get_md5_sum("./tests/files/test_encrypt_decrypt/file1.txt").unwrap();

    assert_eq!(original_md5, md5_now);
}

#[test]
fn test_encrypt_decrypt_with_manually_set_input_and_output_files() {
    copy_files(
        "files_1",
        "test_encrypt_decrypt_with_manually_set_input_and_output_files",
    )
    .unwrap();

    defer! {
        clear_files("test_encrypt_decrypt_with_manually_set_input_and_output_files")
    }

    let original_md5 = get_md5_sum(
        "./tests/files/test_encrypt_decrypt_with_manually_set_input_and_output_files/file1.txt",
    )
    .unwrap();

    // Encrypt command
    let mut cmd = Command::cargo_bin(CARGO_BIN_NAME).unwrap();
    let assert = cmd
        .current_dir("./tests/files/test_encrypt_decrypt_with_manually_set_input_and_output_files")
        .arg("encrypt")
        .args(["--out1-suffix", "encrypted.part0"])
        .args(["--out2-suffix", "encrypted.part1"])
        .arg("file1.txt")
        .assert();

    assert
        .success()
        .stdout("Successfully encrypted file1.txt\n")
        .stderr("");

    remove_file(
        "./tests/files/test_encrypt_decrypt_with_manually_set_input_and_output_files/file1.txt",
    )
    .unwrap();
    assert_path_exists(&format!("{FILES_DIR}/test_encrypt_decrypt_with_manually_set_input_and_output_files/file1.txt.encrypted.part0"));
    assert_path_exists(&format!("{FILES_DIR}/test_encrypt_decrypt_with_manually_set_input_and_output_files/file1.txt.encrypted.part1"));

    // Decrypt command
    let mut cmd = Command::cargo_bin(CARGO_BIN_NAME).unwrap();
    let assert = cmd
        .current_dir("./tests/files/test_encrypt_decrypt_with_manually_set_input_and_output_files")
        .arg("decrypt")
        .args(["--in1-suffix", "encrypted.part1"])
        .args(["--in2-suffix", "encrypted.part0"])
        .arg("file1.txt")
        .assert();

    assert
        .success()
        .stdout("Successfully decrypted file1.txt\n")
        .stderr("");

    let md5_now = get_md5_sum(
        "./tests/files/test_encrypt_decrypt_with_manually_set_input_and_output_files/file1.txt",
    )
    .unwrap();

    assert_eq!(original_md5, md5_now);
}

#[test]
fn test_delete_after_encryption() {
    copy_files("files_1", "test_delete_after_encryption").unwrap();

    defer! {
        clear_files("test_delete_after_encryption")
    }

    let mut cmd = Command::cargo_bin(CARGO_BIN_NAME).unwrap();
    let assert = cmd
        .current_dir("./tests/files/test_delete_after_encryption")
        .arg("encrypt")
        .arg("--rm")
        .arg("file1.txt")
        .assert();

    assert
        .success()
        .stdout("Successfully encrypted file1.txt\n")
        .stderr("");

    assert_path_not_found(&format!(
        "{FILES_DIR}/test_delete_after_encryption/file1.txt"
    ));
    assert_path_exists(&format!(
        "{FILES_DIR}/test_delete_after_encryption/file1.txt.otp.0"
    ));
    assert_path_exists(&format!(
        "{FILES_DIR}/test_delete_after_encryption/file1.txt.otp.1"
    ));
}

#[test]
fn test_delete_after_decryption() {
    copy_files("files_2", "test_delete_after_decryption").unwrap();

    defer! {
        clear_files("test_delete_after_decryption")
    }

    let mut cmd = Command::cargo_bin(CARGO_BIN_NAME).unwrap();
    let assert = cmd
        .current_dir("./tests/files/test_delete_after_decryption")
        .arg("decrypt")
        .arg("--rm")
        .arg("file2.txt")
        .assert();

    assert
        .success()
        .stdout("Successfully decrypted file2.txt\n")
        .stderr("");

    assert_path_not_found(&format!(
        "{FILES_DIR}/test_delete_after_decryption/file2.txt.otp.0"
    ));
    assert_path_not_found(&format!(
        "{FILES_DIR}/test_delete_after_decryption/file2.txt.otp.1"
    ));
    assert_path_exists(&format!(
        "{FILES_DIR}/test_delete_after_decryption/file2.txt"
    ));
}

#[test]
fn test_encrypt_decrypt_quiet() {
    copy_files("files_1", "test_encrypt_decrypt_quiet").unwrap();

    defer! {
        clear_files("test_encrypt_decrypt_quiet")
    }

    let original_md5 = get_md5_sum("./tests/files/test_encrypt_decrypt_quiet/file1.txt").unwrap();

    // Encrypt command
    let mut cmd = Command::cargo_bin(CARGO_BIN_NAME).unwrap();
    let assert = cmd
        .current_dir("./tests/files/test_encrypt_decrypt_quiet")
        .arg("-q")
        .arg("encrypt")
        .arg("file1.txt")
        .assert();

    assert
        .success()
        .stdout("")
        .stderr("");

    remove_file("./tests/files/test_encrypt_decrypt_quiet/file1.txt").unwrap();

    // Decrypt command
    let mut cmd = Command::cargo_bin(CARGO_BIN_NAME).unwrap();
    let assert = cmd
        .current_dir("./tests/files/test_encrypt_decrypt_quiet")
        .arg("-q")
        .arg("decrypt")
        .arg("file1.txt")
        .assert();

    assert
        .success()
        .stdout("")
        .stderr("");

    let md5_now = get_md5_sum("./tests/files/test_encrypt_decrypt_quiet/file1.txt").unwrap();

    assert_eq!(original_md5, md5_now);
}