d-engine-core 0.2.3

Pure Raft consensus algorithm - for building custom Raft-based systems
Documentation
use std::fs::OpenOptions;
use std::fs::create_dir_all;
use std::io::Read;
use std::path::Path;
use std::path::PathBuf;

use sha2::Digest;
use sha2::Sha256;
use tokio::fs;
use tokio::io::AsyncReadExt;
use tokio::io::AsyncWriteExt;
use tokio::io::BufWriter;
use tracing::debug;
use tracing::error;

use crate::ConvertError;
use crate::FileError;
use crate::Result;
use crate::StorageError;

/// Creates parent directories for the given path.
/// e.g. path = "/tmp/a/b/c", "/tmp/a/b" will be crated
/// e.g. path = "/tmp/a/b/c/", "/tmp/a/b/c" will be crated
/// e.g. path = "/tmp/a/b/x.txt", "/tmp/a/b" will be crated
pub fn create_parent_dir_if_not_exist(path: &Path) -> Result<()> {
    /// Check if the path ends with the system separator
    fn has_trailing_separator(path: &Path) -> bool {
        let s = path.to_string_lossy();
        !s.is_empty() && s.ends_with(std::path::MAIN_SEPARATOR)
    }

    // Core logic: Prioritize detection of the end delimiter
    let dir_to_create = if has_trailing_separator(path) {
        path //Explicitly indicate the directory path and create a complete path
    } else {
        path.parent().unwrap_or(path) // file path, create parent directory
    };

    if !dir_to_create.exists() {
        if let Err(e) = create_dir_all(dir_to_create) {
            error!(?e, "create_parent_dir_if_not_exist failed.");
            return Err(StorageError::PathError {
                path: path.to_path_buf(),
                source: e,
            }
            .into());
        }
    }

    Ok(())
}

pub fn open_file_for_append(path: PathBuf) -> Result<std::fs::File> {
    create_parent_dir_if_not_exist(&path)?;
    let log_file = match OpenOptions::new().append(true).create(true).open(&path) {
        Ok(f) => f,
        Err(e) => {
            return Err(StorageError::PathError { path, source: e }.into());
        }
    };
    Ok(log_file)
}

#[allow(dead_code)]
pub async fn write_into_file(
    path: PathBuf,
    buf: Vec<u8>,
) {
    if let Some(parent) = path.parent() {
        if let Err(e) = tokio::fs::create_dir_all(parent).await {
            error!("failed to crate dir with error({})", e);
        } else {
            debug!("created successfully: {:?}", path);
        }
    }

    let file = tokio::fs::OpenOptions::new()
        .create(true)
        .append(true)
        .open(path)
        .await
        .unwrap();
    let mut active_file = BufWriter::new(file);
    active_file.write_all(&buf).await.unwrap();
    active_file.flush().await.unwrap();
}

/// Asynchronous function for safely deleting files
///
/// # Parameters
/// - `path`: The file path to be deleted, accepting any type that implements `AsRef<Path>`
///
/// # Error
/// Returns a custom FileError error type, including various possible failure scenarios
#[allow(dead_code)]
pub async fn delete_file<P: AsRef<Path>>(path: P) -> Result<()> {
    let path = path.as_ref();
    let display_path = path.display().to_string();

    // Normalized path processing
    let canonical_path = match path.canonicalize() {
        Ok(p) => p,
        Err(e) => return Err(map_canonicalize_error(e, &display_path).into()),
    };

    // Get file metadata
    let metadata = match fs::metadata(&canonical_path).await {
        Ok(m) => m,
        Err(e) => return Err(map_metadata_error(e, &display_path).into()),
    };

    // Verify file type
    if metadata.is_dir() {
        return Err(FileError::IsDirectory(display_path).into());
    }

    // Perform deletion
    match fs::remove_file(&canonical_path).await {
        Ok(_) => Ok(()),
        Err(e) => Err(map_remove_error(e, &display_path).into()),
    }
}

/// Handle path canonicalization error
fn map_canonicalize_error(
    error: std::io::Error,
    path: &str,
) -> FileError {
    match error.kind() {
        std::io::ErrorKind::NotFound => FileError::NotFound(path.to_string()),
        std::io::ErrorKind::PermissionDenied => FileError::PermissionDenied(path.to_string()),
        _ => FileError::InvalidPath(format!("{path}: {error}")),
    }
}

/// Handle metadata retrieval errors
fn map_metadata_error(
    error: std::io::Error,
    path: &str,
) -> FileError {
    match error.kind() {
        std::io::ErrorKind::PermissionDenied => FileError::PermissionDenied(path.to_string()),
        std::io::ErrorKind::NotFound => FileError::NotFound(path.to_string()),
        _ => FileError::UnknownIo(format!("{path}: {error}")),
    }
}

/// Handle delete operation errors
fn map_remove_error(
    error: std::io::Error,
    path: &str,
) -> FileError {
    match error.kind() {
        std::io::ErrorKind::PermissionDenied => FileError::PermissionDenied(path.to_string()),
        std::io::ErrorKind::NotFound => FileError::NotFound(path.to_string()),
        std::io::ErrorKind::Other if is_file_busy(&error) => FileError::FileBusy(path.to_string()),
        _ => FileError::UnknownIo(format!("{path}: {error}")),
    }
}

/// Detect file occupation status (cross-platform processing)
fn is_file_busy(error: &std::io::Error) -> bool {
    #[cfg(windows)]
    {
        error.raw_os_error() == Some(32) // ERROR_SHARING_VIOLATION
    }
    #[cfg(unix)]
    {
        error.raw_os_error() == Some(16) // EBUSY
    }
    #[cfg(not(any(windows, unix)))]
    {
        false
    }
}

pub fn validate_checksum(
    data: &[u8],
    expected: &[u8],
) -> bool {
    let mut hasher = crc32fast::Hasher::new();
    hasher.update(data);
    hasher.finalize().to_be_bytes() == expected
}

/// Converts a byte vector into a fixed-size 32-byte checksum array
///
/// # Parameters
/// - `checksum`: Input byte vector representing a checksum value
///
/// # Returns
/// - `Ok([u8; 32])`: Successfully converted checksum array
/// - `Err(String)`: Error with detailed reason if conversion fails
///
/// # Errors
/// Returns an error in the following cases:
/// 1. Input vector length is not exactly 32 bytes
/// 2. Input vector is empty (special case handling)
///
/// # Example
/// ```rust
/// use d_engine_core::file_io::convert_vec_checksum;
///
/// let vec_checksum = vec![0u8; 32];
/// let array_checksum = convert_vec_checksum(vec_checksum).unwrap();
/// assert_eq!(array_checksum, [0u8; 32]);
/// ```
pub fn convert_vec_checksum(checksum: Vec<u8>) -> Result<[u8; 32]> {
    match checksum.len() {
        // Exact size match - use efficient conversion
        32 => checksum.try_into().map_err(|_| {
            ConvertError::ConversionFailure("Conversion failed despite correct length".to_string())
                .into()
        }),

        // Handle empty vector as special case
        0 => Err(
            ConvertError::ConversionFailure("Empty checksum vector provided".to_string()).into(),
        ),

        // All other length mismatches
        len => Err(ConvertError::ConversionFailure(format!(
            "Invalid checksum length: expected 32 bytes, got {len} bytes"
        ))
        .into()),
    }
}

/// Computes a SHA-256 checksum for a file by hashing its contents.
///
/// The algorithm:
/// 1. Opens the file at the specified path
/// 2. Reads the file content in chunks to handle large files efficiently
/// 3. Updates the hasher with each chunk of bytes
/// 4. Finalizes and returns the SHA-256 hash
///
/// Notes:
/// - Processes the entire file content sequentially
/// - Uses buffered reading to handle large files efficiently
/// - Consistent across platforms and file systems
#[allow(dead_code)]
pub async fn compute_checksum_from_file_path(file_path: &Path) -> Result<[u8; 32]> {
    let mut file = tokio::fs::File::open(file_path).await.map_err(StorageError::IoError)?;

    let mut hasher = Sha256::new();
    let mut buffer = [0u8; 4096]; // 4KB buffer

    loop {
        let bytes_read = file.read(&mut buffer).await.map_err(StorageError::IoError)?;

        if bytes_read == 0 {
            break;
        }

        hasher.update(&buffer[..bytes_read]);
    }

    Ok(hasher.finalize().into())
}

/// Validates compressed file format using magic numbers and extensions
/// Referenced  flate2 header validation principles and  security practices
pub(crate) fn validate_compressed_format(path: &Path) -> Result<()> {
    // 1. Check if the file exists
    if !path.exists() {
        return Err(FileError::NotFound(path.display().to_string()).into());
    }
    // 2. Check file size
    if let Ok(metadata) = std::fs::metadata(path) {
        if metadata.len() < 10 {
            return Err(FileError::TooSmall(metadata.len()).into());
        }
    }

    // 3. Check the file extension
    let ext = path
        .extension()
        .and_then(|s| s.to_str())
        .ok_or_else(|| FileError::InvalidExt("Invalid file extension".into()))?;

    if !matches!(ext.to_lowercase().as_str(), "gz" | "tgz" | "snap") {
        return Err(FileError::InvalidExt(format!("Invalid compression extension: {ext}")).into());
    }

    // 4. Verify magic numbers (GZIP header checks)
    let mut file = std::fs::File::open(path).map_err(StorageError::IoError)?;
    let mut header = [0u8; 2];
    file.read_exact(&mut header).map_err(StorageError::IoError)?;

    // GZIP magic numbers: 0x1f 0x8b
    if header != [0x1f, 0x8b] {
        return Err(FileError::InvalidGzipHeader("Invalid GZIP header".to_string()).into());
    }

    Ok(())
}

#[allow(dead_code)]
pub(super) async fn is_dir(path: &Path) -> Result<bool> {
    let metadata = fs::metadata(path).await.map_err(StorageError::IoError)?;

    // Check if it's a directory
    Ok(metadata.is_dir())
}