alma 0.1.1

A Bevy-native modal text editor with Vim-style navigation.
Documentation
//! Scalar guest value proofs.

use super::{GuestDecodeError, GuestDecodeField, GuestDecodeLimits};
use crate::text_stream::TextRange;
use std::{
    fmt::{Debug, Formatter},
    str,
};

/// Guest string after UTF-8 and byte-limit checks.
#[derive(Clone, Eq, PartialEq)]
pub struct DecodedGuestString {
    /// Validated UTF-8 text.
    value: String,
}

impl DecodedGuestString {
    /// Decodes a guest byte string.
    ///
    /// # Errors
    ///
    /// Returns [`GuestDecodeError`] when the byte sequence is oversized or not UTF-8.
    pub fn from_bytes(
        field: GuestDecodeField,
        bytes: &[u8],
        limits: GuestDecodeLimits,
    ) -> Result<Self, GuestDecodeError> {
        let size = checked_len(field, bytes.len(), limits.max_string_bytes())?;
        let value = str::from_utf8(bytes)
            .map_err(|_source| GuestDecodeError::MalformedUtf8 { field, size })?
            .to_owned();
        Ok(Self { value })
    }

    /// Returns the decoded string.
    #[must_use]
    pub fn as_str(&self) -> &str {
        &self.value
    }

    /// Returns the decoded byte length.
    #[must_use]
    pub const fn byte_len(&self) -> usize {
        self.value.len()
    }

    /// Consumes the proof into owned text.
    #[must_use]
    pub fn into_string(self) -> String {
        self.value
    }
}

impl Debug for DecodedGuestString {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
        formatter
            .debug_struct("DecodedGuestString")
            .field("byte_len", &self.value.len())
            .finish()
    }
}

/// Guest payload after byte-limit checks.
#[derive(Clone, Eq, PartialEq)]
pub struct DecodedPayload {
    /// Bounded opaque bytes.
    bytes: Vec<u8>,
}

impl DecodedPayload {
    /// Decodes opaque guest bytes.
    ///
    /// # Errors
    ///
    /// Returns [`GuestDecodeError`] when the payload exceeds the configured cap.
    pub fn from_bytes(
        field: GuestDecodeField,
        bytes: &[u8],
        limits: GuestDecodeLimits,
    ) -> Result<Self, GuestDecodeError> {
        let _size = checked_len(field, bytes.len(), limits.max_payload_bytes())?;
        Ok(Self {
            bytes: bytes.to_vec(),
        })
    }

    /// Returns decoded bytes.
    #[must_use]
    pub fn as_bytes(&self) -> &[u8] {
        &self.bytes
    }

    /// Consumes the proof into owned bytes.
    #[must_use]
    pub fn into_bytes(self) -> Vec<u8> {
        self.bytes
    }
}

impl Debug for DecodedPayload {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
        formatter
            .debug_struct("DecodedPayload")
            .field("byte_len", &self.bytes.len())
            .finish()
    }
}

/// Guest count after range and host-width checks.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct DecodedCount {
    /// Host-sized scalar.
    value: usize,
}

impl DecodedCount {
    /// Decodes a guest count.
    ///
    /// # Errors
    ///
    /// Returns [`GuestDecodeError`] when the value exceeds runtime or host limits.
    pub fn new(
        field: GuestDecodeField,
        value: u64,
        limits: GuestDecodeLimits,
    ) -> Result<Self, GuestDecodeError> {
        if value > limits.max_count() {
            return Err(GuestDecodeError::CountTooLarge {
                field,
                value,
                max: limits.max_count(),
            });
        }
        let host_max = u64::try_from(usize::MAX).unwrap_or(u64::MAX);
        if value > host_max {
            return Err(GuestDecodeError::CountTooLarge {
                field,
                value,
                max: host_max,
            });
        }
        let value = usize::try_from(value).map_err(|_source| GuestDecodeError::CountTooLarge {
            field,
            value,
            max: host_max,
        })?;
        Ok(Self { value })
    }

    /// Returns the host-sized count.
    #[must_use]
    pub const fn get(self) -> usize {
        self.value
    }
}

/// Guest text range after ordering and host-width checks.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct DecodedTextRange {
    /// Host-sized byte range.
    range: TextRange,
}

impl DecodedTextRange {
    /// Decodes an unchecked text range.
    ///
    /// # Errors
    ///
    /// Returns [`GuestDecodeError`] when the coordinates are reversed or too large.
    pub fn new(
        field: GuestDecodeField,
        start: u64,
        end: u64,
        limits: GuestDecodeLimits,
    ) -> Result<Self, GuestDecodeError> {
        if start > end {
            return Err(GuestDecodeError::ReversedRange { field, start, end });
        }
        let host_max = u64::try_from(usize::MAX).unwrap_or(u64::MAX);
        let max = limits.max_count().min(host_max);
        if end > max {
            return Err(GuestDecodeError::RangeTooLarge {
                field,
                start,
                end,
                max,
            });
        }
        let start = usize::try_from(start).map_err(|_source| GuestDecodeError::RangeTooLarge {
            field,
            start,
            end,
            max,
        })?;
        let end = usize::try_from(end).map_err(|_source| GuestDecodeError::RangeTooLarge {
            field,
            start: u64::try_from(start).unwrap_or(u64::MAX),
            end,
            max,
        })?;
        Ok(Self {
            range: TextRange::new(start, end),
        })
    }

    /// Returns the unchecked text range.
    #[must_use]
    pub const fn get(self) -> TextRange {
        self.range
    }
}

/// Checks a host byte length against a guest policy cap.
fn checked_len(field: GuestDecodeField, len: usize, max: u64) -> Result<u64, GuestDecodeError> {
    let size = u64::try_from(len).unwrap_or(u64::MAX);
    if size > max {
        return Err(GuestDecodeError::PayloadTooLarge { field, size, max });
    }
    Ok(size)
}