sashite-feen 0.1.0

Field Expression Encoding Notation (FEEN): a compact, ASCII-only, no_std, zero-allocation validator and encoder for board-game positions in abstract strategy games, built on EPIN and SIN.
Documentation
//! Top-level FEEN validation.
//!
//! [`parse`] is the single validation entry point. It bounds the input, splits
//! it into the three space-separated fields, delegates each field to its
//! validator ([`placement`], [`hands`], [`style_turn`]), enforces the
//! Game-Protocol cardinality bound (pieces ≤ squares), and returns a borrowing
//! [`Parsed`] view. It allocates nothing and borrows the input throughout.

use sashite_sin::Identifier as Style;

use crate::error::ParseError;
use crate::limits::{MAX_SQUARE_COUNT, MAX_STRING_LENGTH};
use crate::shape::Shape;
use crate::{hands, placement, style_turn};

/// The validated components of a FEEN string, borrowing the input.
///
/// The two field slices ([`placement`](Parsed::placement) and
/// [`hands`](Parsed::hands)) are retained so the view can iterate squares and
/// hand items lazily; the remaining fields are the parsed metadata.
#[derive(Debug, Clone, Copy)]
pub(crate) struct Parsed<'a> {
    pub(crate) placement: &'a [u8],
    pub(crate) hands: &'a [u8],
    pub(crate) shape: Shape,
    pub(crate) board_pieces: u32,
    pub(crate) hand_pieces: u32,
    pub(crate) active: Style,
    pub(crate) inactive: Style,
}

/// Validates a FEEN string and returns its borrowed components.
pub(crate) fn parse(input: &str) -> Result<Parsed<'_>, ParseError> {
    let bytes = input.as_bytes();

    // Bound the work before doing any (length first, then character set).
    if bytes.len() > MAX_STRING_LENGTH {
        return Err(ParseError::InputTooLong);
    }
    if !input.is_ascii() {
        return Err(ParseError::NonAscii);
    }

    // Exactly three non-empty fields separated by single ASCII spaces. Tokens
    // never contain a space, so a valid string holds exactly two of them; any
    // stray, leading, trailing, or doubled space yields a wrong field count.
    let mut fields = bytes.split(|&b| b == b' ');
    let (Some(placement_field), Some(hands_field), Some(style_field), None) =
        (fields.next(), fields.next(), fields.next(), fields.next())
    else {
        return Err(ParseError::FieldCount);
    };
    if placement_field.is_empty() || hands_field.is_empty() || style_field.is_empty() {
        return Err(ParseError::FieldCount);
    }

    let (shape, board_pieces) = placement::validate(placement_field)?;
    let hand_pieces = hands::validate(hands_field)?;
    let (active, inactive) = style_turn::validate(style_field)?;

    // Cardinality (Game Protocol §5.5): the number of pieces cannot exceed the
    // number of squares. The board is first capped at `MAX_SQUARE_COUNT` so that
    // a position FEEN accepts can always be represented as a `sashite-qi`
    // position; `board_pieces` is already bounded by the square count, and the
    // saturating add guards against an absurd hand multiplicity.
    let squares = shape.square_count();
    if squares > MAX_SQUARE_COUNT {
        return Err(ParseError::TooManySquares);
    }
    let pieces = board_pieces.saturating_add(hand_pieces);
    if pieces > squares {
        return Err(ParseError::TooManyPieces);
    }

    Ok(Parsed {
        placement: placement_field,
        hands: hands_field,
        shape,
        board_pieces,
        hand_pieces,
        active,
        inactive,
    })
}