ferrocrypt 0.2.5

Core Ferrocrypt library: symmetric (XChaCha20-Poly1305 + Argon2id) and hybrid (RSA-4096) encryption utilities.
Documentation
use std::path::Path;

use constant_time_eq::constant_time_eq_32;
use sha3::{Digest, Sha3_256};

use crate::CryptoError;

pub fn normalize_paths(src_file_path: &str, dest_dir_path: &str) -> (String, String) {
    let src_file_path_norm = src_file_path.replace('\\', "/");
    let mut dest_dir_path_norm = dest_dir_path.replace('\\', "/");

    if !dest_dir_path_norm.ends_with('/') && !dest_dir_path_norm.is_empty() {
        dest_dir_path_norm = format!("{}/", dest_dir_path_norm);
    }

    (src_file_path_norm, dest_dir_path_norm)
}

pub fn get_file_stem_to_string(filename: impl AsRef<Path>) -> Result<String, CryptoError> {
    let file_stem_string = filename
        .as_ref()
        .file_stem()
        .ok_or_else(|| CryptoError::Message("Cannot get file stem".to_string()))?
        .to_str()
        .ok_or_else(|| CryptoError::Message("Cannot convert file stem to &str".to_string()))?
        .to_string();

    Ok(file_stem_string)
}

pub fn sha3_32_hash(byte_string: &[u8]) -> Result<[u8; 32], CryptoError> {
    let mut hasher = Sha3_256::new();
    hasher.update(byte_string);
    let digest: [u8; 32] = hasher.finalize().as_slice().try_into()?;

    Ok(digest)
}

/// Compares two 256-bit byte strings in constant time.
pub fn constant_time_compare_256_bit(byte_string1: &[u8; 32], byte_string2: &[u8; 32]) -> bool {
    constant_time_eq_32(byte_string1, byte_string2)
}

pub fn get_duration(seconds: f64) -> String {
    if seconds < 60_f64 {
        format!("{:.2} sec", seconds)
    } else {
        format!("{} min, {:.2} sec", seconds as u32 / 60, seconds % 60_f64)
    }
}

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

    #[test]
    fn test_normalize_paths_unix_style() {
        let (src, dest) = normalize_paths("path/to/file", "path/to/dest");
        assert_eq!(src, "path/to/file");
        assert_eq!(dest, "path/to/dest/");
    }

    #[test]
    fn test_normalize_paths_windows_style() {
        let (src, dest) = normalize_paths("path\\to\\file", "path\\to\\dest");
        assert_eq!(src, "path/to/file");
        assert_eq!(dest, "path/to/dest/");
    }

    #[test]
    fn test_normalize_paths_empty_dest() {
        let (src, dest) = normalize_paths("file.txt", "");
        assert_eq!(src, "file.txt");
        assert_eq!(dest, "");
    }

    #[test]
    fn test_normalize_paths_trailing_slash() {
        let (src, dest) = normalize_paths("file.txt", "dest/");
        assert_eq!(src, "file.txt");
        assert_eq!(dest, "dest/");
    }

    #[test]
    fn test_get_file_stem() {
        let stem = get_file_stem_to_string("path/to/file.txt").unwrap();
        assert_eq!(stem, "file");
    }

    #[test]
    fn test_get_file_stem_no_extension() {
        let stem = get_file_stem_to_string("path/to/file").unwrap();
        assert_eq!(stem, "file");
    }

    #[test]
    fn test_sha3_hash_consistency() {
        let data = b"test data for hashing";
        let hash1 = sha3_32_hash(data).unwrap();
        let hash2 = sha3_32_hash(data).unwrap();
        assert_eq!(hash1, hash2);
    }

    #[test]
    fn test_sha3_hash_different_inputs() {
        let hash1 = sha3_32_hash(b"data1").unwrap();
        let hash2 = sha3_32_hash(b"data2").unwrap();
        assert_ne!(hash1, hash2);
    }

    #[test]
    fn test_sha3_hash_empty_input() {
        let hash = sha3_32_hash(b"").unwrap();
        assert_eq!(hash.len(), 32);
    }

    #[test]
    fn test_constant_time_compare_equal() {
        let data = [42u8; 32];
        assert!(constant_time_compare_256_bit(&data, &data));
    }

    #[test]
    fn test_constant_time_compare_not_equal() {
        let data1 = [42u8; 32];
        let mut data2 = [42u8; 32];
        data2[0] = 43;
        assert!(!constant_time_compare_256_bit(&data1, &data2));
    }

    #[test]
    fn test_constant_time_compare_all_zeros() {
        let data1 = [0u8; 32];
        let data2 = [0u8; 32];
        assert!(constant_time_compare_256_bit(&data1, &data2));
    }

    #[test]
    fn test_get_duration_seconds() {
        let duration_str = get_duration(45.67);
        assert!(duration_str.contains("45.67 sec"));
    }

    #[test]
    fn test_get_duration_minutes() {
        let duration_str = get_duration(125.5);
        assert!(duration_str.contains("2 min"));
        assert!(duration_str.contains("5.50 sec"));
    }

    #[test]
    fn test_get_duration_zero() {
        let duration_str = get_duration(0.0);
        assert!(duration_str.contains("0.00 sec"));
    }

    #[test]
    fn test_get_duration_less_than_second() {
        let duration_str = get_duration(0.123);
        assert!(duration_str.contains("0.12 sec"));
    }

    #[test]
    fn test_secret_string_creation() {
        let secret = SecretString::from("my_secret_password".to_string());
        let debug_str = format!("{:?}", secret);
        assert!(debug_str.contains("Secret"));
    }
}