# 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**
| `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:
| `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
| `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:
| 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