# sashite-feen
[](https://crates.io/crates/sashite-feen)
[](https://docs.rs/sashite-feen)
[](https://github.com/sashite/feen.rs/actions/workflows/ci.yml)
[](https://github.com/sashite/feen.rs/blob/main/LICENSE)
> A `no_std`, zero-allocation validator and encoder for **FEEN** board-game positions.
## Overview
**Field Expression Encoding Notation (FEEN)** is a compact, ASCII-only string
that encodes a complete board-game **position** as three space-separated fields:
```text
<piece-placement> <hands> <style-turn>
```
| Piece placement | Board geometry and occupancy (runs of empties, dimensional `/`) | `8/8/…/RNBQK^BNR` |
| Hands | Off-board pieces held by each player, with multiplicities | `3P2B/3p2b` |
| Style–turn | One style per side, and which side is to move | `W/w` |
This crate implements the
[FEEN v1.0.0 specification](https://sashite.dev/specs/feen/1.0.0/). It delegates
piece-token syntax to [EPIN](https://sashite.dev/specs/epin/1.0.0/) and
style-token syntax to [SIN](https://sashite.dev/specs/sin/1.0.0/), adding the
field, dimensional, canonicality, and cardinality rules on top.
A FEEN string is variable-sized, so — unlike a fixed-width token — this crate is
built as a **borrowing, streaming validator** rather than a parser that returns
an owned tree: [`Feen::parse`] validates the input in a single pass and hands
back a *view* that borrows it. Nothing is materialized and nothing is allocated.
An owned, transformable position is available on demand behind the `alloc`
feature as a [`Qi`](https://crates.io/crates/sashite-qi).
## Quick Start
```rust
use sashite_feen::{Feen, Side};
// Validate and parse in one pass; the view borrows the input string.
let feen =
Feen::parse("-rnbqk^bn-r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/-RNBQK^BN-R / W/w")?;
assert_eq!(feen.square_count(), 64);
assert_eq!(feen.piece_count(), 32);
assert_eq!(feen.active_side(), Side::First); // active token `W` is uppercase
// A cheap boolean check when you don't need the view.
assert!(Feen::is_valid("k^+p4+PK^ / W/w")); // a 1-D, 8-square board
# Ok::<(), sashite_feen::ParseError>(())
```
## Installation
```sh
cargo add sashite-feen
```
Or add it to `Cargo.toml`:
```toml
[dependencies]
sashite-feen = "0.1"
```
`sashite-feen` is `no_std`, forbids `unsafe`, and uses no regex engine. Its only
required dependencies are [`sashite-epin`](https://crates.io/crates/sashite-epin)
and [`sashite-sin`](https://crates.io/crates/sashite-sin) (both `no_std` and
allocation-free). The minimum supported Rust version is **1.81**.
## Cargo features
The default build is **strictly allocation-free**: the `alloc` crate is not even
linked, so validation, borrowing iteration, and encoding-to-a-sink work on
targets without an allocator.
- **`alloc`** *(off by default)* — enables the owned position type
[`sashite_qi::Qi`] and the conversions that produce or consume it:
[`Feen::to_qi`], [`encode`], and [`write_feen`]. This is the only part of the
crate that allocates.
- **`serde`** *(off by default)* — provides [`feen_string`], a
`#[serde(with = "…")]` adapter that (de)serializes a `Qi` position as its
canonical FEEN string. Implies `alloc`.
```toml
[dependencies]
sashite-feen = { version = "0.1", features = ["serde"] }
```
## Usage
### Validating and parsing
[`Feen::is_valid`] returns a `bool`; [`Feen::parse`] returns a borrowing view or
a [`ParseError`] explaining the first violation.
```rust
use sashite_feen::Feen;
assert!(Feen::is_valid("8/8/8/8/8/8/8/8 / W/w"));
assert!(Feen::parse("8/8/8/8/8/8/8/8 / W/w").is_ok());
assert!(Feen::parse("8/8/8/8/8/8/8/8 W/w").is_err()); // missing a field
```
### Reading a position
All accessors are `const fn` and borrow nothing beyond `self`. Geometry comes
from [`Shape`]; sides and styles are reported both by turn (active / inactive)
and by side (first / second).
```rust
use sashite_feen::{Feen, Side};
let feen =
Feen::parse("lnsgk^gsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGK^GSNL / J/j")?;
let shape = feen.shape();
assert_eq!(shape.dimension_count(), 2);
assert_eq!(shape.square_count(), 81); // also `feen.square_count()`
assert_eq!(shape.dimensions(), &[9u8, 9]); // sizes along each dimension
assert_eq!(feen.piece_count(), 40); // on-board + in-hand
assert_eq!(feen.board_piece_count(), 40);
assert_eq!(feen.hand_piece_count(), 0);
assert_eq!(feen.active_side(), Side::First); // `J` is uppercase ⇒ first
assert_eq!(feen.inactive_side(), Side::Second);
# Ok::<(), sashite_feen::ParseError>(())
```
### Iterating squares and hands
The board iterator yields one `Option<Piece>` per square in the board's
serialization order (`None` for an empty square); the hand iterators yield
[`HandItem`]s in canonical order. Both are lazy and allocation-free; `Piece` is
an [`sashite_epin::Identifier`].
```rust
use sashite_feen::Feen;
let feen = Feen::parse("4P3/8 P/p W/w")?; // 2 ranks; first hand: one P, second: one p
let occupied = feen.squares().flatten().count();
assert_eq!(occupied, 1); // the lone `P` on the board
for item in feen.first_hand() {
// `item.piece()` is an EPIN identifier; `item.count()` ≥ 1.
assert_eq!(item.count(), 1);
}
# Ok::<(), sashite_feen::ParseError>(())
```
### Owned positions (`alloc`)
With the `alloc` feature, [`Feen::to_qi`] materializes the view into a
[`Qi`](sashite_qi::Qi) — the ecosystem's owned, immutable position type — and
[`encode`] / [`write_feen`] turn an owned position back into a canonical FEEN
string. Re-encoding an unchanged position reproduces the input exactly.
```rust,ignore
use sashite_feen::{encode, Feen};
let feen = Feen::parse("8/8/8/8/8/8/8/8 / W/w")?;
let position = feen.to_qi(); // allocates the owned position
assert_eq!(encode(&position), "8/8/8/8/8/8/8/8 / W/w");
```
Because `Qi` is generic and transformable, you can read a position with FEEN,
edit it with `Qi`'s move-based API, and re-encode it — see `examples/basic.rs`
(`cargo run --example basic --features alloc`).
### Serde (`serde`)
[`feen_string`] lets a `Qi` field round-trip through its canonical FEEN string,
which keeps the serialized form human-readable and portable:
```rust,ignore
use serde::{Deserialize, Serialize};
use sashite_feen::sashite_qi::Qi;
use sashite_feen::sashite_epin::Identifier as Piece;
use sashite_feen::sashite_sin::Identifier as Style;
#[derive(Serialize, Deserialize)]
struct Saved {
#[serde(with = "sashite_feen::feen_string")]
position: Qi<Piece, Style>,
}
```
## The FEEN string
### Piece placement
A run of consecutive empty squares is written as a base-10 count (`≥ 1`, no
leading zeros); every other run is a sequence of EPIN piece tokens. Dimensions
are separated hierarchically: a single `/` separates ranks, `//` separates
2-D layers, `///` separates 3-D cubes, and so on. **Dimensional coherence**
requires that a separator of length `N` only appear between structures that
themselves contain separators of length `N − 1`.
```text
rkr # 1-D, 3 squares
8/8/8/8/8/8/8/8 # 2-D, 8×8 = 64 empty squares
ab/cd//AB/CD # 3-D, 2 layers × 2 ranks × 2 files
```
> **Stricter than the specification:** this crate accepts **regular boards
> only** — every rank within a dimension must have the same length. Inputs the
> specification would allow as irregular are rejected with
> [`ParseError::BoardNotRegular`].
### Hands
`<first-hand>/<second-hand>`, each a separator-free concatenation of items
`[<count>]<piece>` in canonical order. The count is omitted when it is 1 and
must be `≥ 2` when present. The piece's own side (its token's case) is
independent of which hand holds it.
### Style–turn
`<active-style>/<inactive-style>`, each a single SIN letter. **Case** encodes
the player side (uppercase ⇒ `first`, lowercase ⇒ `second`); **position**
encodes the turn (the first token is the active player). The two tokens must be
of opposite case.
## Bounds (assumed for safety)
Inputs are bounded before and during parsing, so memory and time stay bounded
even on untrusted input:
| [`MAX_STRING_LENGTH`] | Maximum input length in bytes (checked first) |
| [`MAX_DIMENSIONS`] | Maximum number of board dimensions |
| [`MAX_DIMENSION_SIZE`]| Maximum number of cells along any one dimension |
A separate internal cap on the total square count (rejected with
[`ParseError::TooManySquares`]) guarantees that any FEEN string this crate
accepts is constructible as a [`Qi`](sashite_qi::Qi) without overflow.
## Errors
[`ParseError`] reports the first violation found. The variants group by field:
| Lexical | `InputTooLong`, `NonAscii`, `FieldCount` |
| Placement | `PlacementEmpty`, `PlacementStartsWithSeparator`, `PlacementEndsWithSeparator`, `EmptySegment`, `InvalidEmptyCount`, `InvalidPieceToken`, `BoardNotRegular`, `DimensionalCoherence`, `TooManyDimensions`, `DimensionTooLarge` |
| Hands | `InvalidHandsDelimiter`, `InvalidHandCount`, `HandNotAggregated`, `HandNotCanonical` |
| Style–turn | `InvalidStyleTurnDelimiter`, `InvalidStyleToken`, `StylesSameCase` |
| Cardinality | `TooManySquares`, `TooManyPieces` |
## Design
- **Allocation-free, borrowing core.** Validation is one left-to-right pass over
the raw bytes; the view and its iterators borrow the input. The heap is touched
only through the optional `alloc`-gated conversions.
- **No `unsafe`, no regex engine.** Parsing matches bytes directly with bounded
integer arithmetic, eliminating ReDoS as an attack vector.
- **Layered on EPIN and SIN.** Piece and style tokens are validated by those
crates; this crate owns only the FEEN-level structure.
- **Canonical-only.** Non-minimal empty counts and non-canonical hand orderings
are rejected, so an accepted string is already in canonical form.
## Ecosystem
FEEN is the position-serialization layer of the
[Sashité](https://sashite.dev/) ecosystem. It describes *how a position is
written*, while the surrounding crates supply the pieces, the styles, and the
owned model:
- [Qi](https://crates.io/crates/sashite-qi) — the owned, immutable position type
- [EPIN](https://sashite.dev/specs/epin/1.0.0/) — piece token syntax
- [SIN](https://sashite.dev/specs/sin/1.0.0/) — style token syntax
- [Game Protocol](https://sashite.dev/game-protocol/) — the shared conceptual foundation
If a behavior here appears to conflict with the
[specification](https://sashite.dev/specs/feen/1.0.0/), the specification is
normative.
## License
Available as open source under the terms of the
[Apache License 2.0](https://opensource.org/licenses/Apache-2.0).