jsonhash 0.2.0

A command-line tool to generate hash values for files. SHA256 and MD5. Output and Error messages in JSON format.
Documentation
use std::path::Path;
use std::fs;
use std::time::{SystemTime, UNIX_EPOCH};

use crate::hashinterface::HashAlgorithm;
use crate::sha256hasher::Sha256Hasher;
use crate::md5hasher::Md5Hasher;
use crate::response::{Request, Response, OutputJsonMessage};
use crate::error::{JsonHashError, ErrorMessage};
use crate::cli::Cli;

/// Enum to represent available hash algorithms
#[derive(Debug, Clone, Copy)]
pub enum Algorithm {
    Sha256,
    Md5,
}

impl Algorithm {
    pub fn from_str(s: &str) -> Result<Self, JsonHashError> {
        match s.to_lowercase().as_str() {
            "sha256" => Ok(Algorithm::Sha256),
            "md5" => Ok(Algorithm::Md5),
            _ => Err(JsonHashError::HashAlgorithmInvalid),
        }
    }
    
    pub fn create_hasher(&self) -> Box<dyn HashAlgorithm> {
        match self {
            Algorithm::Sha256 => Box::new(Sha256Hasher::new()),
            Algorithm::Md5 => Box::new(Md5Hasher::new()),
        }
    }
}

/// Process file path to extract absolute path, filename, and short path
pub fn process_file_path(filepath: &str) -> (String, String, String) {
    let path = Path::new(filepath);
    let absfilepath = std::fs::canonicalize(path)
        .unwrap_or_else(|_| path.to_path_buf())
        .to_string_lossy()
        .into_owned();

    let filename = path
        .file_name()
        .unwrap_or_default()
        .to_string_lossy()
        .into_owned();

    // Get current directory for shortpath calculation
    let current_dir = std::env::current_dir()
        .unwrap_or_else(|_| std::path::PathBuf::from("."))
        .file_name()
        .unwrap_or_default()
        .to_string_lossy()
        .into_owned();
    
    // Get parent directory
    let parent_dir = path.parent().unwrap_or_else(|| Path::new(""));
    let parent_folder = parent_dir
        .file_name()
        .unwrap_or_default()
        .to_string_lossy()
        .into_owned();

    // For shortpath, use current directory name + "/" + parent folder name + "/" + filename
    let shortpath = if parent_folder.is_empty() {
        format!("{}/{}", current_dir, filename)
    } else {
        format!("{}/{}/{}", current_dir, parent_folder, filename)
    };

    (absfilepath, filename, shortpath)
}

/// Compute content hash and path hashes
pub fn compute_digests(filepath: &str, content: &[u8], algorithm: Algorithm) -> Result<(String, String, String), JsonHashError> {
    // Compute content hash
    let mut hasher = algorithm.create_hasher();
    hasher.update(content);
    let hash = hasher.finalize();
    
    // Get absolute filepath for pathid computation
    let (absfilepath, _, _) = process_file_path(filepath);
    
    // Compute pathid using the same algorithm
    let mut pathid_hasher = algorithm.create_hasher();
    pathid_hasher.update(absfilepath.as_bytes());
    let pathid = pathid_hasher.finalize();
    
    // Compute shortpathid using the same algorithm
    let (_, _, shortpath) = process_file_path(filepath);
    let mut shortpathid_hasher = algorithm.create_hasher();
    shortpathid_hasher.update(shortpath.as_bytes());
    let shortpathid = shortpathid_hasher.finalize();
    
    Ok((hash, pathid, shortpathid))
}

/// Process a file and generate the appropriate response or error
pub fn process_file(cli: &Cli) -> OutputJsonMessage {
    // Build request object
    let start_time = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("Time went backwards")
        .as_nanos();
    
    let mut params = std::collections::HashMap::new();
    params.insert("filepath".to_string(), serde_json::Value::String(cli.filepath.clone()));
    params.insert("alg".to_string(), serde_json::Value::String(cli.alg.clone()));
    if let Some(id) = &cli.id {
        params.insert("id".to_string(), serde_json::Value::String(id.clone()));
    } else {
        params.insert("id".to_string(), serde_json::Value::Null);
    }
    
    let request = Request {
        method: "jsonhash".to_string(),
        params,
        ts: start_time,
        version: env!("CARGO_PKG_VERSION").to_string(),
    };
    
    // Validate algorithm
    let algorithm = match Algorithm::from_str(&cli.alg) {
        Ok(alg) => alg,
        Err(e) => return OutputJsonMessage::error(request, ErrorMessage::from(e)),
    };
    
    // Check if file exists and is a file (not directory)
    let path = Path::new(&cli.filepath);
    if !path.exists() {
        return OutputJsonMessage::error(request, ErrorMessage::from(JsonHashError::FileDoesNotExist));
    }
    
    if !path.is_file() {
        return OutputJsonMessage::error(request, ErrorMessage::from(JsonHashError::NotAFile));
    }
    
    // Read file content
    let content = match fs::read(&cli.filepath) {
        Ok(content) => content,
        Err(_) => return OutputJsonMessage::error(request, ErrorMessage::from(JsonHashError::FileOpeningError)),
    };
    
    // Process file paths
    let (absfilepath, filename, shortpath) = process_file_path(&cli.filepath);
    
    // Compute hashes
    let (hash, pathid, shortpathid) = match compute_digests(&cli.filepath, &content, algorithm) {
        Ok(digests) => digests,
        Err(e) => return OutputJsonMessage::error(request, ErrorMessage::from(e)),
    };
    
    // Build response
    let end_time = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("Time went backwards")
        .as_nanos();
    
    let response = Response {
        ts: end_time,
        absfilepath,
        hash,
        filename,
        shortpath,
        pathid,
        shortpathid,
    };
    
    OutputJsonMessage::success(request, response)
}