alma 0.1.1

A Bevy-native modal text editor with Vim-style navigation.
Documentation
//! Error taxonomy for filesystem configuration, path policy, and persistence.

use super::display::EscapedDisplayText;
use std::{
    fmt::{Display, Formatter},
    io,
    path::PathBuf,
};

/// Errors returned while building filesystem configuration.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum FilesystemConfigError {
    /// The current process directory could not be read.
    CurrentDir {
        /// Underlying I/O error.
        source: io::Error,
    },
    /// The selected workspace root could not be canonicalized.
    CanonicalizeRoot {
        /// Underlying I/O error.
        source: io::Error,
    },
    /// The selected workspace root is not a directory.
    RootIsNotDirectory {
        /// Rejected workspace root.
        workspace_root: PathBuf,
    },
}

impl Display for FilesystemConfigError {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::CurrentDir { source } => {
                write!(formatter, "failed to read current directory: {source}")
            }
            Self::CanonicalizeRoot { source } => {
                write!(formatter, "failed to canonicalize workspace root: {source}")
            }
            Self::RootIsNotDirectory { workspace_root } => {
                write!(
                    formatter,
                    "{} is not a directory",
                    display_path(workspace_root)
                )
            }
        }
    }
}

/// Errors returned by path policy checks.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum PathPolicyError {
    /// The current process directory could not be read.
    CurrentDir {
        /// Underlying I/O error.
        source: io::Error,
    },
    /// The requested path has no parent directory.
    MissingParent {
        /// Rejected path.
        path: PathBuf,
    },
    /// The parent directory does not exist or cannot be canonicalized.
    Parent {
        /// Rejected path.
        path: PathBuf,
        /// Underlying I/O error.
        source: io::Error,
    },
    /// The requested path exists in some form but cannot be safely resolved.
    Unresolvable {
        /// Rejected path.
        path: PathBuf,
        /// Underlying I/O error.
        source: io::Error,
    },
    /// The requested path or parent resolves outside the configured workspace root.
    OutsideWorkspace {
        /// Rejected path.
        path: PathBuf,
        /// Configured workspace root.
        workspace_root: PathBuf,
    },
}

impl Display for PathPolicyError {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::CurrentDir { source } => {
                write!(formatter, "failed to read current directory: {source}")
            }
            Self::MissingParent { path } => {
                write!(formatter, "{} has no parent directory", display_path(path))
            }
            Self::Parent { path, source } => {
                write!(
                    formatter,
                    "failed to resolve parent for {}: {source}",
                    display_path(path)
                )
            }
            Self::Unresolvable { path, source } => {
                write!(
                    formatter,
                    "failed to safely resolve {}: {source}",
                    display_path(path)
                )
            }
            Self::OutsideWorkspace {
                path,
                workspace_root,
            } => write!(
                formatter,
                "{} is outside workspace root {}",
                display_path(path),
                display_path(workspace_root)
            ),
        }
    }
}

/// Errors returned while reading a policy-accepted file.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum FileReadError {
    /// Path policy rejected the request.
    Policy(#[source] PathPolicyError),
    /// The path did not resolve to an existing file.
    Missing {
        /// Rejected path.
        path: PathBuf,
    },
    /// Metadata could not be read.
    Metadata {
        /// File path.
        path: PathBuf,
        /// Underlying I/O error.
        source: io::Error,
    },
    /// The path is not a regular file.
    UnsupportedFileType {
        /// Rejected path.
        path: PathBuf,
    },
    /// The file exceeds the configured maximum size.
    TooLarge {
        /// Rejected path.
        path: PathBuf,
        /// Observed size.
        size: u64,
        /// Configured maximum size.
        max_size: u64,
    },
    /// File opening failed.
    Open {
        /// File path.
        path: PathBuf,
        /// Underlying I/O error.
        source: io::Error,
    },
    /// File reading failed.
    Read {
        /// File path.
        path: PathBuf,
        /// Underlying I/O error.
        source: io::Error,
    },
}

impl Display for FileReadError {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Policy(error) => error.fmt(formatter),
            Self::Missing { path } => {
                write!(formatter, "{} does not exist", display_path(path))
            }
            Self::Metadata { path, source } => {
                write!(
                    formatter,
                    "failed to read metadata for {}: {source}",
                    display_path(path)
                )
            }
            Self::UnsupportedFileType { path } => {
                write!(formatter, "{} is not a regular file", display_path(path))
            }
            Self::TooLarge {
                path,
                size,
                max_size,
            } => write!(
                formatter,
                "{} is {size} bytes, exceeding the {max_size} byte limit",
                display_path(path)
            ),
            Self::Open { path, source } => {
                write!(formatter, "failed to open {}: {source}", display_path(path))
            }
            Self::Read { path, source } => {
                write!(formatter, "failed to read {}: {source}", display_path(path))
            }
        }
    }
}

/// Errors returned while atomically writing a policy-accepted file.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum FileWriteError {
    /// Path policy rejected the request.
    Policy(#[source] PathPolicyError),
    /// Metadata could not be read.
    Metadata {
        /// File path.
        path: PathBuf,
        /// Underlying I/O error.
        source: io::Error,
    },
    /// The path is not a regular file.
    UnsupportedFileType {
        /// Rejected path.
        path: PathBuf,
    },
    /// The target path has no parent directory.
    MissingParent {
        /// Rejected path.
        path: PathBuf,
    },
    /// Creating the temporary file failed.
    CreateTemp {
        /// Temporary path.
        path: PathBuf,
        /// Underlying I/O error.
        source: io::Error,
    },
    /// Writing to the temporary file failed.
    WriteTemp {
        /// Temporary path.
        path: PathBuf,
        /// Underlying I/O error.
        source: io::Error,
    },
    /// Flushing the temporary file failed.
    SyncTemp {
        /// Temporary path.
        path: PathBuf,
        /// Underlying I/O error.
        source: io::Error,
    },
    /// Applying existing file permissions to the temporary file failed.
    SetTempPermissions {
        /// Temporary path.
        path: PathBuf,
        /// Underlying I/O error.
        source: io::Error,
    },
    /// Renaming the temporary file into place failed.
    Rename {
        /// Temporary path.
        temp_path: PathBuf,
        /// Target path.
        target_path: PathBuf,
        /// Underlying I/O error.
        source: io::Error,
    },
}

impl Display for FileWriteError {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Policy(error) => error.fmt(formatter),
            Self::Metadata { path, source } => {
                write!(
                    formatter,
                    "failed to read metadata for {}: {source}",
                    display_path(path)
                )
            }
            Self::UnsupportedFileType { path } => {
                write!(formatter, "{} is not a regular file", display_path(path))
            }
            Self::MissingParent { path } => {
                write!(formatter, "{} has no parent directory", display_path(path))
            }
            Self::CreateTemp { path, source } => {
                write!(
                    formatter,
                    "failed to create temporary file {}: {source}",
                    display_path(path)
                )
            }
            Self::WriteTemp { path, source } => {
                write!(
                    formatter,
                    "failed to write temporary file {}: {source}",
                    display_path(path)
                )
            }
            Self::SyncTemp { path, source } => {
                write!(
                    formatter,
                    "failed to sync temporary file {}: {source}",
                    display_path(path)
                )
            }
            Self::SetTempPermissions { path, source } => {
                write!(
                    formatter,
                    "failed to set permissions on temporary file {}: {source}",
                    display_path(path)
                )
            }
            Self::Rename {
                temp_path,
                target_path,
                source,
            } => write!(
                formatter,
                "failed to rename {} to {}: {source}",
                display_path(temp_path),
                display_path(target_path)
            ),
        }
    }
}

/// Escapes a path for user-facing one-line diagnostics.
fn display_path(path: &std::path::Path) -> String {
    EscapedDisplayText::from_path(path).as_str().to_owned()
}