cyclone-runtime-rust 1.0.0

Rust reference runtime for the Cyclone binary wire format: Writer, Reader, DecodeError.
Documentation
# cyclone-runtime

Rust **Reference Implementation** of the runtime layer of the Cyclone binary wire format.

```
User Model  →  Cyclone Compiler / Derive  →  Generated Codec  →  cyclone-runtime  →  Bytes
```

This crate is the last step and nothing else. It turns primitives into bytes and
bytes back into primitives, exactly as RFC-0002 specifies.

## Scope

**Provides**

| Item | Role |
|------|------|
| `Writer` | appends primitives, strings and byte blobs to a `Vec<u8>` |
| `Reader<'a>` | reads them back from a borrowed slice, rejecting malformed input |
| `Limits` | allocation guards for untrusted input |
| `DecodeError` | every way a byte stream can fail to conform |
| `Encode` / `Decode` | the traits a codec implements |
| `to_bytes` / `from_bytes` | the two calls that use those traits |

**Does not provide** - and will not: schema parsing, annotation reading, codec
generation, proc macros, reflection, a registry, a dynamic resolver, runtime
type discovery, or a serialization framework. No dependency outside `std`.

Three consequences worth stating up front:

- There is no `write_model` / `read_model`. A model *is* its fields, written
  back to back in declaration order with nothing in between (RFC-0002 §5).
- There is no `InvalidEnum` error. Which `u32` values an enum admits is schema
  knowledge, so the generated codec validates it - the runtime never could.
- There are **no blanket impls** of `Encode` / `Decode` for `u32`, `String`,
  `Vec<T>` or any other Rust type. `Vec<T>` is an `Array<T>` only because a
  schema said so, and a runtime that guessed it from the Rust type would be
  deciding the wire format by inference.

## Where the impls come from

The runtime cannot tell, and does not care:

| Route | What it is |
|-------|------------|
| `cyclone-codegen-rust` | `#[derive(Network)]` writes the impls in place |
| `cyclone-cli` | the official compiler writes them into a `*.codec.rs` file you own |
| by hand | equally valid |

## Usage

```rust
use cyclone_runtime::{from_bytes, to_bytes, Decode, DecodeError, Encode, Reader, Writer};

struct Item { id: u32, name: String }

// What a codec looks like, whoever wrote it:
impl Encode for Item {
    fn encode(&self, writer: &mut Writer) {
        writer.write_u32(self.id);
        writer.write_string(&self.name);
    }
}

impl Decode for Item {
    fn decode(reader: &mut Reader<'_>) -> Result<Self, DecodeError> {
        Ok(Item {
            id: reader.read_u32()?,
            name: reader.read_string()?,
        })
    }
}

let bytes = to_bytes(&Item { id: 42, name: "Sword".to_owned() });
assert_eq!(bytes, [0x2A, 0, 0, 0, 0x05, 0, 0, 0, b'S', b'w', b'o', b'r', b'd']);

let item = from_bytes::<Item>(&bytes)?;
```

`to_bytes` and `from_bytes` add no framing of their own - they construct a
`Writer` / `Reader` and call the trait. Drive those types directly when several
values share one buffer, when a pre-sized buffer avoids reallocation, or when
you need `Reader::with_limits`.

`from_bytes` decodes **one value** and returns it; leftover bytes are not an
error. A stream that must end exactly at the value (RFC-0002 §9) needs the check
made explicitly:

```rust
let mut reader = Reader::new(&bytes);
let item = Item::decode(&mut reader)?;
assert!(reader.is_empty(), "trailing bytes: the two ends disagree about the schema");
```

For arrays, the runtime writes the count and the codec writes the elements:

```rust
writer.write_array_count(items.len());
for item in items { item.encode(writer); }
```

```rust
let count = reader.read_array_count()?;
let mut items = Vec::new();
for _ in 0..count { items.push(Item::decode(reader)?); }
```

## Wire format at a glance

| Type | Bytes |
|------|-------|
| `bool` | 1 - `0x00` or `0x01`, nothing else |
| `i8` / `u8` | 1 |
| `i16` / `u16` | 2, Little Endian |
| `i32` / `u32` | 4, Little Endian |
| `i64` / `u64` | 8, Little Endian |
| `f32` / `f64` | 4 / 8 - raw IEEE 754 bits, never normalized |
| `String` | `u32` UTF-8 **byte** length, then the bytes |
| `Bytes` | `u32` length, then the raw bytes |
| `Array<T>` | `u32` element count, then each element |
| `Enum` | always `u32` |
| `Model` | its fields concatenated in declaration order |

No varint, no padding, no alignment, no tag id, no object header.

## Decoding untrusted input

`Reader::new` applies `Limits::UNLIMITED` (`u32::MAX` everywhere) - the wire
format's own ceiling and nothing tighter. Anything reading from the network
should lower it:

```rust
use cyclone_runtime::{Limits, Reader};

let reader = Reader::with_limits(bytes, Limits {
    max_string_len: 1024 * 1024,
    max_bytes_len: 1024 * 1024,
    max_array_count: 100_000,
});
```

Two bounds are enforced, and every length is checked **before** anything is
allocated:

| Bound | Condition | Error | Nature |
|-------|-----------|-------|--------|
| Byte stream | `length > remaining bytes` | `UnexpectedEof` | **normative** - always rejected |
| Configuration | `length > Limits::…` | `LengthOverflow` | per-peer, not part of the protocol |

The configured limit is additive: no configuration can permit a length larger
than the bytes actually remaining (RFC-0002 §10.1).

Decoding never panics on malformed input, and a failed read leaves the cursor
where it was - an error cannot desynchronize a caller that inspects it and
carries on. `#![forbid(unsafe_code)]`.

## Conformance

`tests/conformance.rs` runs the RFC-0003 vectors, each test named for the IDs it
covers: §3 primitives, §4 float (including NaN payload preservation and
`-0.0`), §5 string, §6 bytes, §7 array, §8 model and enum, §9 rejects, §10
round-trip. `tests/codec.rs` covers the `Encode` / `Decode` traits and the two
helpers.

```
cargo test
```

The two vectors excluded are N-040 and N-041 - enum values outside the declared
set. They depend on schema knowledge and belong to the generated codec.

## References

- RFC-0001 - What Cyclone is
- RFC-0002 - Wire Format Specification
- RFC-0003 - Conformance