alma 0.1.1

A Bevy-native modal text editor with Vim-style navigation.
Documentation
//! Workspace capability proofs and filesystem-policy tokens.

use super::{PluginWorkspaceReadByteLimit, PluginWorkspaceWriteByteLimit};
use crate::{
    fs_utils::{
        EscapedDisplayText, FileReadError, FileWriteError, FilesystemConfig, ReadTextFile,
        atomic_write_workspace_file, read_existing_workspace_file,
    },
    plugin::{PluginIdentity, WorkspacePath, WorkspacePathRef},
};
use std::fmt::{Debug, Display, Formatter};

/// Authorized workspace operation kind.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WorkspaceAccessKind {
    /// Read bytes or metadata through filesystem policy.
    Read,
    /// Write bytes through filesystem policy and atomic-write handling.
    Write,
}

impl WorkspaceAccessKind {
    /// Stable operation text for diagnostics.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Read => "read",
            Self::Write => "write",
        }
    }
}

impl Display for WorkspaceAccessKind {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
        formatter.write_str(self.as_str())
    }
}

/// Workspace read path after plugin capability authorization.
#[derive(Eq, PartialEq)]
pub struct AuthorizedWorkspaceReadAccess {
    /// Plugin whose grants authorized this read.
    identity: PluginIdentity,
    /// Normalized workspace-relative path.
    path: WorkspacePath,
}

impl AuthorizedWorkspaceReadAccess {
    /// Records read approval before filesystem policy revalidates it.
    #[must_use]
    pub(in crate::plugin::host) const fn new(
        identity: PluginIdentity,
        path: WorkspacePath,
    ) -> Self {
        Self { identity, path }
    }

    /// Returns the plugin identity for display and serialization.
    #[must_use]
    pub fn identity(&self) -> &str {
        self.identity_proof().as_str()
    }

    /// Returns the validated identity that authorized this access proof.
    #[must_use]
    pub const fn identity_proof(&self) -> &PluginIdentity {
        &self.identity
    }

    /// Returns the authorized operation kind.
    #[must_use]
    pub const fn kind(&self) -> WorkspaceAccessKind {
        WorkspaceAccessKind::Read
    }

    /// Returns the normalized workspace-relative path proof.
    #[must_use]
    pub const fn workspace_path(&self) -> WorkspacePathRef<'_> {
        self.path.as_ref()
    }

    /// Returns the normalized workspace-relative path string.
    #[must_use]
    pub fn workspace_relative_path(&self) -> &str {
        self.path.as_str()
    }

    /// Converts this capability proof into the next filesystem-policy boundary.
    #[must_use]
    pub(in crate::plugin::host::workspace_io) fn into_policy_token(
        self,
    ) -> PluginWorkspaceReadToken {
        PluginWorkspaceReadToken {
            identity: self.identity,
            path: self.path,
        }
    }
}

impl Debug for AuthorizedWorkspaceReadAccess {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
        fmt_workspace_access(
            formatter,
            "AuthorizedWorkspaceReadAccess",
            &self.identity,
            self.kind(),
            self.path.as_ref(),
        )
    }
}

/// Workspace write path after plugin capability authorization.
#[derive(Eq, PartialEq)]
pub struct AuthorizedWorkspaceWriteAccess {
    /// Plugin whose grants authorized this write.
    identity: PluginIdentity,
    /// Normalized workspace-relative path.
    path: WorkspacePath,
}

impl AuthorizedWorkspaceWriteAccess {
    /// Records write approval before filesystem policy revalidates it.
    #[must_use]
    pub(in crate::plugin::host) const fn new(
        identity: PluginIdentity,
        path: WorkspacePath,
    ) -> Self {
        Self { identity, path }
    }

    /// Returns the plugin identity for display and serialization.
    #[must_use]
    pub fn identity(&self) -> &str {
        self.identity_proof().as_str()
    }

    /// Returns the validated identity that authorized this access proof.
    #[must_use]
    pub const fn identity_proof(&self) -> &PluginIdentity {
        &self.identity
    }

    /// Returns the authorized operation kind.
    #[must_use]
    pub const fn kind(&self) -> WorkspaceAccessKind {
        WorkspaceAccessKind::Write
    }

    /// Returns the normalized workspace-relative path proof.
    #[must_use]
    pub const fn workspace_path(&self) -> WorkspacePathRef<'_> {
        self.path.as_ref()
    }

    /// Returns the normalized workspace-relative path string.
    #[must_use]
    pub fn workspace_relative_path(&self) -> &str {
        self.path.as_str()
    }

    /// Converts this capability proof into the next filesystem-policy boundary.
    #[must_use]
    pub(in crate::plugin::host::workspace_io) fn into_policy_token(
        self,
    ) -> PluginWorkspaceWriteToken {
        PluginWorkspaceWriteToken {
            identity: self.identity,
            path: self.path,
        }
    }
}

impl Debug for AuthorizedWorkspaceWriteAccess {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
        fmt_workspace_access(
            formatter,
            "AuthorizedWorkspaceWriteAccess",
            &self.identity,
            self.kind(),
            self.path.as_ref(),
        )
    }
}

/// Workspace read token after plugin capability authorization.
///
/// The token deliberately exposes no [`std::path::PathBuf`]. Reads still route through `fs_utils`.
#[derive(Eq, PartialEq)]
pub(in crate::plugin::host::workspace_io) struct PluginWorkspaceReadToken {
    /// Plugin whose grants authorized this read.
    identity: PluginIdentity,
    /// Normalized workspace-relative path.
    path: WorkspacePath,
}

impl PluginWorkspaceReadToken {
    /// Returns the normalized workspace-relative path proof.
    #[must_use]
    pub const fn workspace_path(&self) -> WorkspacePathRef<'_> {
        self.path.as_ref()
    }

    /// Returns the normalized workspace-relative path string.
    #[must_use]
    pub fn workspace_relative_path(&self) -> &str {
        self.path.as_str()
    }

    /// Reads an existing workspace file through filesystem policy.
    ///
    /// # Errors
    ///
    /// Returns [`PluginWorkspaceReadError`] when `fs_utils` rejects the bounded read.
    pub(in crate::plugin::host::workspace_io) fn read_existing(
        self,
        filesystem: &FilesystemConfig,
        max_bytes: PluginWorkspaceReadByteLimit,
    ) -> Result<ReadTextFile, PluginWorkspaceReadError> {
        read_existing_workspace_file(self.path.as_str(), filesystem, max_bytes.get())
            .map_err(PluginWorkspaceReadError::Read)
    }
}

impl Debug for PluginWorkspaceReadToken {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
        fmt_workspace_access(
            formatter,
            "PluginWorkspaceReadToken",
            &self.identity,
            WorkspaceAccessKind::Read,
            self.path.as_ref(),
        )
    }
}

/// Workspace write token after plugin capability authorization.
///
/// The token deliberately exposes no [`std::path::PathBuf`]. Writes still route through
/// `fs_utils`.
#[derive(Eq, PartialEq)]
pub(in crate::plugin::host::workspace_io) struct PluginWorkspaceWriteToken {
    /// Plugin whose grants authorized this write.
    identity: PluginIdentity,
    /// Normalized workspace-relative path.
    path: WorkspacePath,
}

impl PluginWorkspaceWriteToken {
    /// Returns the normalized workspace-relative path proof.
    #[must_use]
    pub const fn workspace_path(&self) -> WorkspacePathRef<'_> {
        self.path.as_ref()
    }

    /// Returns the normalized workspace-relative path string.
    #[must_use]
    pub fn workspace_relative_path(&self) -> &str {
        self.path.as_str()
    }

    /// Writes a workspace file through filesystem policy and atomic replacement.
    ///
    /// # Errors
    ///
    /// Returns [`PluginWorkspaceWriteError`] when the payload exceeds the caller-provided cap or
    /// `fs_utils` rejects the write.
    pub(in crate::plugin::host::workspace_io) fn write_atomic(
        self,
        bytes: &[u8],
        filesystem: &FilesystemConfig,
        max_bytes: PluginWorkspaceWriteByteLimit,
    ) -> Result<EscapedDisplayText, PluginWorkspaceWriteError> {
        let size = u64::try_from(bytes.len()).unwrap_or(u64::MAX);
        if size > max_bytes.get() {
            return Err(PluginWorkspaceWriteError::PayloadTooLarge {
                size,
                max_size: max_bytes.get(),
            });
        }
        atomic_write_workspace_file(self.path.as_str(), bytes, filesystem)
            .map(|path| EscapedDisplayText::from_path(&path))
            .map_err(PluginWorkspaceWriteError::Write)
    }
}

impl Debug for PluginWorkspaceWriteToken {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
        fmt_workspace_access(
            formatter,
            "PluginWorkspaceWriteToken",
            &self.identity,
            WorkspaceAccessKind::Write,
            self.path.as_ref(),
        )
    }
}

/// Formats workspace authority without disclosing the workspace-relative path.
fn fmt_workspace_access(
    formatter: &mut Formatter<'_>,
    type_name: &'static str,
    identity: &PluginIdentity,
    kind: WorkspaceAccessKind,
    path: WorkspacePathRef<'_>,
) -> std::fmt::Result {
    formatter
        .debug_struct(type_name)
        .field("identity", identity)
        .field("kind", &kind)
        .field("path_byte_len", &path.as_str().len())
        .finish()
}

/// Filesystem-policy read rejection at the plugin boundary.
#[derive(thiserror::Error)]
#[non_exhaustive]
pub enum PluginWorkspaceReadError {
    /// Read rejected by filesystem policy.
    Read(#[source] FileReadError),
}

impl Debug for PluginWorkspaceReadError {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Read(_source) => formatter.write_str("Read"),
        }
    }
}

impl Display for PluginWorkspaceReadError {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Read(_source) => formatter.write_str("plugin workspace read denied"),
        }
    }
}

/// Filesystem-policy write rejection at the plugin boundary.
#[derive(thiserror::Error)]
#[non_exhaustive]
pub enum PluginWorkspaceWriteError {
    /// Write payload exceeds budget.
    PayloadTooLarge {
        /// Observed bytes.
        size: u64,
        /// Accepted bytes.
        max_size: u64,
    },
    /// Write rejected by filesystem policy.
    Write(#[source] FileWriteError),
}

impl Debug for PluginWorkspaceWriteError {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::PayloadTooLarge { size, max_size } => formatter
                .debug_struct("PayloadTooLarge")
                .field("size", size)
                .field("max_size", max_size)
                .finish(),
            Self::Write(_source) => formatter.write_str("Write"),
        }
    }
}

impl Display for PluginWorkspaceWriteError {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::PayloadTooLarge { size, max_size } => write!(
                formatter,
                "plugin workspace payload is {size} bytes, exceeding the {max_size} byte limit"
            ),
            Self::Write(_source) => formatter.write_str("plugin workspace write denied"),
        }
    }
}