diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..925104e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: CI + +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + +jobs: + build: + name: Build and Verify + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: Restore Dependencies + run: dotnet restore src/Nalix.Protogen.slnx + + - name: Build Solution + run: dotnet build src/Nalix.Protogen.slnx -c Release --no-restore diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..57dc77c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,77 @@ +name: CD Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + +jobs: + build-binaries: + name: Build Binary for ${{ matrix.rid }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + include: + - os: windows-latest + rid: win-x64 + binary_name: nalix-protogen.exe + - os: ubuntu-latest + rid: linux-x64 + binary_name: nalix-protogen + - os: macos-latest + rid: osx-x64 + binary_name: nalix-protogen + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: Publish Single File Binary + run: | + dotnet publish src/Nalix.Protogen.Cli/Nalix.Protogen.Cli.csproj -c Release -r ${{ matrix.rid }} -p:PublishSingleFile=true -p:PublishTrimmed=false --self-contained true -o ./dist/${{ matrix.rid }} + + - name: Zip Binary (Windows) + if: matrix.os == 'windows-latest' + run: | + Compress-Archive -Path ./dist/${{ matrix.rid }}/* -DestinationPath ./nalix-protogen-${{ matrix.rid }}.zip + + - name: Zip Binary (Unix) + if: matrix.os != 'windows-latest' + run: | + chmod +x ./dist/${{ matrix.rid }}/${{ matrix.binary_name }} + zip -j nalix-protogen-${{ matrix.rid }}.zip ./dist/${{ matrix.rid }}/${{ matrix.binary_name }} + + - name: Upload Binary Artifact + uses: actions/upload-artifact@v4 + with: + name: nalix-protogen-${{ matrix.rid }} + path: nalix-protogen-${{ matrix.rid }}.zip + + create-release: + name: Create GitHub Release + needs: build-binaries + runs-on: ubuntu-latest + steps: + - name: Download Artifacts + uses: actions/download-artifact@v4 + with: + path: ./artifacts + merge-multiple: true + + - name: Display Downloaded Files + run: ls -R ./artifacts + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + files: ./artifacts/nalix-protogen-*.zip + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index e4793c3..6d3e90e 100644 --- a/README.md +++ b/README.md @@ -1,219 +1,80 @@ -# Nalix Protocol Toolkit +# ⚡ Nalix Protogen -A Roslyn-based code generator that produces wire-compatible C and Rust bindings from C# packet definitions. Guarantees identical byte layout across all platforms. +[![GitHub repository](https://img.shields.io/badge/GitHub-ppn--systems%2Fprotogen-blue?logo=github)](https://github.com/ppn-systems/protogen) +[![.NET 10.0](https://img.shields.io/badge/.NET-10.0-blueviolet?logo=dotnet&logoColor=white)](https://dotnet.microsoft.com) +[![License](https://img.shields.io/badge/License-Apache_2.0-orange?logo=apache)](LICENSE) -## Installation +A high-performance, Roslyn-powered code generation utility that compiles C# packet definitions into wire-compatible C and Rust bindings. Ensures identical byte layout, alignment, and serialization order across platforms. -Download the latest release or build from source: +--- -```bash -dotnet publish src/Nalix.Protocol.Toolkit.Cli -c Release -o ./bin -``` - -The output is a self-contained `nalix-protogen` executable. +## 🗺️ Documentation Index -## Usage - -```bash -nalix-protogen --project --c-out [--rust-out ] [options] -nalix-protogen --assembly --c-out [--rust-out ] [options] -``` +To keep this project maintainable as we support additional target languages, detailed specifications are split into dedicated guides: +* **📐 [Protogen Serialization & Type Specification](docs/SPECIFICATION.md):** Primitive and complex type mappings (C/Rust), string formats, C# definition attributes (`[Packet]`, `[SerializeOrder]`), and generated code outputs. -### Options +--- -| Flag | Description | -|------|-------------| -| `-p, --project` | Path to the `.csproj` file containing packet definitions | -| `-a, --assembly` | Path to the compiled `.dll` containing packet definitions | -| `--c-out` | Output directory for C files (`packets.h`, `packets.c`, `packet_test.c`) | -| `--rust-out` | Output directory for Rust files (`packets.rs`, `packet_tests.rs`) | -| `-v, --verbose` | Show detailed output | +## ✨ Key Features -### Examples - -```bash -# Generate both C and Rust bindings -nalix-protogen -p ../Nalix.Codec/Nalix.Codec.csproj --c-out ./gen/c --rust-out ./gen/rs - -# Generate C only -nalix-protogen -p ../Nalix.Codec/Nalix.Codec.csproj --c-out ./gen/c - -# Generate from compiled DLL -nalix-protogen -a ../Nalix.Codec/bin/Release/net10.0/Nalix.Codec.dll --c-out ./gen/c --rust-out ./gen/rs -``` +* **Roslyn-Powered Parser:** Uses `MSBuildWorkspace` to parse `.csproj` source trees or reflection to read compiled assemblies (`.dll`). +* **Multi-Language Bindings:** Generates fully-typed native C structs (with MSVC pack pragmas) and safe Rust structs. +* **Automatic Size Verification:** Builds round-trip test suites (`packet_test.c` and `packet_tests.rs`) to prove serialization/deserialization logic is identical across target languages. -## Supported C# Types +--- -### Primitives +## 🛠️ Installation -| C# Type | C Type | Rust Type | Size | -|---------|--------|-----------|------| -| `byte` | `uint8_t` | `u8` | 1 | -| `sbyte` | `int8_t` | `i8` | 1 | -| `bool` | `uint8_t` | `u8` | 1 | -| `short` | `int16_t` | `i16` | 2 | -| `ushort` | `uint16_t` | `u16` | 2 | -| `int` | `int32_t` | `i32` | 4 | -| `uint` | `uint32_t` | `u32` | 4 | -| `long` | `int64_t` | `i64` | 8 | -| `ulong` | `uint64_t` | `u64` | 8 | -| `float` | `float` | `f32` | 4 | -| `double` | `double` | `f64` | 8 | +Build the self-contained compiler binary from the repository root: -### Special Types - -| C# Type | C Type | Rust Type | Size | -|---------|--------|-----------|------| -| `Bytes32` | `Bytes32` (struct with `uint8_t data[32]`) | `[u8; 32]` | 32 | -| `PacketHeader` | `PacketHeader` (packed struct) | `PacketHeader` | 10 | -| `string` | `NalixString` (length-prefixed UTF-8) | `Vec` | variable | - -**String wire format:** `[int32_t length][utf8 bytes]` — null = length -1, empty = length 0. Matches the Nalix codec `StringFormatter`. - -### Enums - -Enums are mapped by their underlying integer type. Supported underlying types: `byte`, `sbyte`, `short`, `ushort`, `int`, `uint`, `long`, `ulong`. - -```csharp -public enum OpMode : byte { Read = 0, Write = 1 } -// C: uint8_t -// Rust: u8 +```bash +dotnet publish src/Nalix.Protogen.Cli -c Release -o ./bin ``` -## Unsupported Types - -The following C# types have **no native equivalent** in C or Rust and are not supported: - -| C# Type | Reason | -|---------|--------| -| `char` | C `char` is 1 byte, C# `char` is 2 bytes (UTF-16). Incompatible. | -| `decimal` | No equivalent in C or Rust. | -| `DateTime` | No equivalent. Use `long` (ticks) or `ulong` (unix timestamp). | -| `Guid` | No equivalent. Use `Bytes16` or two `ulong` fields. | -| `object` / reference types | Not serializable to wire format. | -| Arrays / collections | Variable-length. Use fixed-size fields. | -| `Nullable` | Not serializable. Use a sentinel value. | - -## Packet Definition Rules - -### Attributes +The output compiles to a self-contained `protogen` executable in the `./bin` directory. -| Attribute | Purpose | -|-----------|---------| -| `[Packet]` | Marks a class as a packet. Must inherit `PacketBase`. | -| `[SerializeOrder(n)]` | Sets the serialization order for payload fields. Lower = first. | -| `[SerializeHeader(n)]` | Sets the serialization order for header fields. Header fields are serialized before payload. | -| `[SerializeIgnore]` | Excludes a field from serialization. | +--- -### Example +## 💻 CLI Usage -```csharp -[Packet] -public sealed class Handshake : PacketBase -{ - [SerializeHeader(0)] - public PacketHeader _header; - - [SerializeOrder(0)] - public byte Stage; - - [SerializeOrder(1)] - public ushort Reason; - - [SerializeOrder(2)] - public ulong SessionToken; - - [SerializeOrder(3)] - public Bytes32 PublicKey; - - [SerializeOrder(4)] - public Bytes32 Nonce; - - [SerializeOrder(5)] - public Bytes32 Proof; - - [SerializeOrder(6)] - public Bytes32 TranscriptHash; -} -``` - -Generates: - -**C** (`packets.h`): -```c -typedef struct { - PacketHeader _header; - uint8_t Stage; - uint16_t Reason; - uint64_t SessionToken; - Bytes32 PublicKey; - Bytes32 Nonce; - Bytes32 Proof; - Bytes32 TranscriptHash; -} Handshake; // 149 bytes -``` - -**Rust** (`packets.rs`): -```rust -#[repr(C)] -pub struct Handshake { - pub _header: PacketHeader, - pub stage: u8, - pub reason: u16, - pub session_token: u64, - pub public_key: [u8; 32], - pub nonce: [u8; 32], - pub proof: [u8; 32], - pub transcript_hash: [u8; 32], -} // 149 bytes +```bash +# General CLI Command Syntax +protogen --project --c-out [--rust-out ] [options] +protogen --assembly --c-out [--rust-out ] [options] ``` -## Generated Output +### Options & Flags -### C Files +| Flag | Long Flag | Description | +| :--- | :--- | :--- | +| `-p` | `--project` | Path to the C# `.csproj` containing packet definitions. | +| `-a` | `--assembly` | Path to the compiled C# `.dll` containing packet definitions. | +| | `--c-out` | Output directory for C files (`packets.h`, `packets.c`, `packet_test.c`). | +| | `--rust-out` | Output directory for Rust files (`packets.rs`, `packets_tests.rs`). | +| `-v` | `--verbose` | Enable verbose logging for MSBuild warnings. | -| File | Description | -|------|-------------| -| `include/packets.h` | Struct definitions, function declarations, size macros | -| `src/packets.c` | Serialize/deserialize/equals implementations | -| `tests/packet_test.c` | Round-trip test for each packet | +### Practical Examples -Compile the test: ```bash -gcc -Iinclude -o packet_test tests/packet_test.c src/packets.c && ./packet_test -``` - -### Rust Files +# Generate C and Rust bindings from a csproj source +protogen -p ../Nalix.Codec/Nalix.Codec.csproj --c-out ./gen/c --rust-out ./gen/rs -| File | Description | -|------|-------------| -| `src/packets.rs` | Struct definitions with serialize/deserialize/equals methods | -| `tests/packet_tests.rs` | Round-trip test for each packet | - -Run the test: -```bash -cargo test +# Generate C-only bindings from a compiled DLL assembly +protogen -a ../Nalix.Codec/bin/Release/net10.0/Nalix.Codec.dll --c-out ./gen/c ``` -## Wire Format +--- -All multi-byte values are encoded in **little-endian** byte order. `Bytes32` and `PacketHeader` fields are copied as raw bytes (no endian conversion needed). +## 📐 Project Architecture -Serialization layout: -``` -[Header fields in SerializeHeader order] [Payload fields in SerializeOrder order] -``` - -## Architecture - -``` -Nalix.Protocol.Toolkit.Abstractions # Interfaces and data models -Nalix.Protocol.Toolkit.Roslyn # Roslyn analyzer + code generators -Nalix.Protocol.Toolkit.Cli # CLI entry point +```plaintext +Nalix.Protogen.Abstractions # Shared models, interfaces, and options contracts. +Nalix.Protogen.Roslyn # MSBuild and Roslyn workspace packet code parser. +Nalix.Protogen.Cli # Console Application entry point (protogen). ``` -The toolkit uses Roslyn (`MSBuildWorkspace`) to analyze C# source, extract packet definitions via attributes, and generate wire-compatible C and Rust code. `PacketHeader` is hardcoded due to its `[StructLayout(Explicit)]` layout. +--- -## License +## ⚖️ License -Apache License 2.0 +Apache License 2.0. See [LICENSE](LICENSE) for details. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..53fdc06 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,85 @@ +# Nalix Protogen Architecture Specification + +This document details the internal architecture, compiler pipeline, and core design decisions of **Nalix.Protogen**. + +--- + +## 1. Pipeline Overview + +**Nalix.Protogen** uses Microsoft Roslyn and MSBuild workspace APIs to analyze C# contract source files, compile them semantically, and generate native, zero-dependency C and Rust memory structures that are byte-for-byte wire compatible. + +```mermaid +graph TD + A[C# Contract Project .csproj] --> B[MSBuildWorkspace] + B --> C[Roslyn Compilation] + C --> D[PacketAnalyzer] + D -->|Semantic Analysis| E[PacketDefinition IR] + E --> F[GeneratorBase] + F -->|CHeaderGenerator| G[packets.h / packets.c] + F -->|RustGenerator| H[packets.rs] +``` + +The pipeline consists of three main phases: +1. **Semantic Parsing:** Building a Roslyn syntax/semantic compilation of the C# contract project. +2. **Intermediate Representation (IR):** Extraction of type characteristics and layout rules into a language-neutral IR. +3. **Generation:** Writing the structures, serialization, deserialization, equality, and validation routines in C and Rust. + +--- + +## 2. Component Breakdown + +The codebase is organized into three clean, single-responsibility assemblies: + +### 💼 `Nalix.Protogen.Abstractions` +Defines the language-agnostic Intermediate Representation (IR): +* **`PacketDefinition`**: Stores the metadata of a packet, including name, full namespace, FNV-1a magic hash, fields, fixed-size status, and wire size. +* **`PacketField`**: Stores properties of a single field (name, order, size in bytes, C type name, Rust type name, and whether it represents a header field). +* **`GenerationContext`**: Captures execution parameters (paths, verbosity, output folders) passed from the CLI. + +### ⚙️ `Nalix.Protogen.Roslyn` +The core parser and code generation engine: +* **`PacketAnalyzer`**: Opens the target `.csproj` or loads a `.dll` assembly, parses the syntax trees, and queries the semantic model. It filters out non-packet types and builds the list of `PacketDefinition` models. +* **`GeneratorBase`**: Coordinates the generation. It calculates base wire sizes and iterates over packets. +* **`CHeaderGenerator`**: Generates packed C structures (`#pragma pack(push, 1)`), endian-safe serialize/deserialize implementations, and helper functions (such as `xxx_free`). +* **`RustGenerator`**: Generates zero-copy Rust structures (`#[repr(C)]`), safe serialize/deserialize slice operations, and in-crate test suites. + +### 💻 `Nalix.Protogen.Cli` +The console application interface. It handles argument parsing, configures the `GenerationContext`, runs the generators, and writes files to the target directories. + +--- + +## 3. Key Design Decisions + +### A. Implicit Header Insertion +In the C# Nalix framework, all packet classes inherit from `PacketBase`, which itself inherits from `FrameBase`. `FrameBase` contains a standard `PacketHeader` (10 bytes). Consequently, C# packets have a header prepended on the wire automatically, without developers declaring it in each subclass. + +Because C and Rust do not support class inheritance, they use flat structs. To guarantee wire layout alignment: +* `PacketAnalyzer` checks if a C# symbol inherits `PacketBase` (`InheritsPacketBaseOfSelf`). +* If true, it automatically inserts an implicit `Header` field of type `PacketHeader` at index `0` of the field list. +* This ensures C and Rust structs match C# byte alignment exactly. + +### B. Deterministic FNV-1a Magic Number Hashing +To avoid requiring developers to manually assign a unique magic number to each packet class via attributes, C# Nalix derives the magic number automatically by hashing the **full type name** (including namespace) using the FNV-1a 32-bit hash algorithm: + +```csharp +private static uint ComputeMagic(string fullName) +{ + uint hash = 2166136261u; + foreach (char c in fullName) + { + hash ^= c; + hash *= 16777619u; + } + return hash; +} +``` + +`PacketAnalyzer` implements this exact hash function, meaning the generated C/Rust files automatically match C#'s runtime packet identification hash. + +### C. Variable-Length Array & String Encoding +C# types like `string` and `ReadOnlyMemory` represent dynamic data and are mapped on the wire as length-prefixed formats: +* **Wire Format:** `[int32_t length][raw payload bytes]` +* A length of `-1` represents a C# `null`. +* A length of `0` represents an empty array/string. +* During serialization/deserialization, C code uses `NalixString` (which contains a pointer to a heap-allocated buffer) and Rust uses `Vec`. +* The static size contribution of these fields is always `4` bytes (the length prefix itself). diff --git a/docs/INTEGRATION.md b/docs/INTEGRATION.md new file mode 100644 index 0000000..5a747d4 --- /dev/null +++ b/docs/INTEGRATION.md @@ -0,0 +1,192 @@ +# Nalix Protogen Integration Guide + +This guide describes how to integrate the generated C and Rust protocol bindings into your applications. + +--- + +## 1. C Integration Guide + +### A. File Setup & Compilation +1. Copy the generated files to your source tree: + * **Headers:** Copy `packets.h` into your `include/` directory. + * **Source:** Copy `packets.c` into your `src/` directory. +2. Ensure your build system includes the header path (e.g., `-Iinclude`) and compiles `packets.c` alongside your code. + ```bash + gcc -Iinclude -o my_app src/main.c src/packets.c + ``` + +### B. Serialization (Writing Packets) +To serialize a structure, allocate a buffer of at least `XXX_SIZE` (which is `#define`d in the header), fill the fields, and call the serialize function: + +```c +#include "packets.h" +#include + +void send_access_request(void) +{ + ObservabilityAccess pkt; + memset(&pkt, 0, sizeof(pkt)); + + // 1. Initialize packet header (Magic number is set automatically by sender if desired) + pkt.Header.MagicNumber = PACKET_OBSERVABILITYACCESS; + pkt.Header.OpCode = 0x0B; + pkt.Header.Flags = 0x03; // RELIABLE | SYSTEM + pkt.Header.Priority = 0x02; // URGENT + pkt.Header.SequenceId = 120; + + // 2. Initialize payload fields + pkt.Stage = 0x01; // REQUEST + pkt.Reason = 0; // NONE + pkt.AccessLevel = 0; // NONE + + // Assign 32-byte hash + Bytes32 key = {{ 0xAA, 0xBB, 0xCC }}; + pkt.AccessKey = key; + + // 3. Serialize into buffer + uint8_t buffer[OBSERVABILITYACCESS_SIZE]; + size_t bytes_written = observabilityaccess_serialize(&pkt, buffer, sizeof(buffer)); + + if (bytes_written == 0) { + printf("Serialization failed! Buffer too small.\n"); + return; + } + + // Send 'buffer' containing 'bytes_written' over the network... +} +``` + +### C. Deserialization (Reading Packets) +When receiving bytes from the wire, call the deserialize function: + +```c +#include "packets.h" +#include + +void handle_received_packet(const uint8_t* net_buffer, size_t net_buffer_size) +{ + // Ensure we have at least the minimum static size + if (net_buffer_size < OBSERVABILITYACCESS_SIZE) { + printf("Packet too short!\n"); + return; + } + + ObservabilityAccess pkt; + memset(&pkt, 0, sizeof(pkt)); + + size_t bytes_read = observabilityaccess_deserialize(&pkt, net_buffer, net_buffer_size); + if (bytes_read == 0) { + printf("Deserialization failed (malformed payload or buffer overflow).\n"); + return; + } + + // Access packet fields + if (pkt.Header.MagicNumber == PACKET_OBSERVABILITYACCESS) { + printf("Received access packet. Stage: %d, Access Level: %d\n", pkt.Stage, pkt.AccessLevel); + } +} +``` + +### D. ⚠️ Memory Management & Freeing +If a packet contains variable-sized fields (like `NalixString` mapped from C# `string` or `ReadOnlyMemory`), **deserialization performs dynamic memory allocation (`malloc`)** for the payload bytes. + +To prevent memory leaks, you **must call the corresponding `xxx_free` helper** when you are done with the packet: + +```c +void process_observation_response(const uint8_t* buf, size_t size) +{ + RuntimeObservation pkt; + memset(&pkt, 0, sizeof(pkt)); + + if (runtimeobservation_deserialize(&pkt, buf, size) > 0) { + // Safe to read observation data + if (pkt.ObservationData.length > 0) { + printf("Data: %s\n", (char*)pkt.ObservationData.data); + } + + // CRITICAL: Free dynamic string/byte memory allocated by deserialize + runtimeobservation_free(&pkt); + } +} +``` + +--- + +## 2. Rust Integration Guide + +### A. Including Generated Packets +Copy the generated file `packets.rs` to your project (usually inside `src/`) and reference it in `lib.rs` or `main.rs`: + +```rust +pub mod packets; + +use packets::{ObservabilityAccess, PacketHeader}; +``` + +### B. Serialization +Use the `.serialize()` method on any packet struct. The buffer must be large enough to hold the fixed size (and dynamic payloads if variable size): + +```rust +use packets::{ObservabilityAccess, PacketHeader}; + +fn run_serialization() -> Result<(), ()> { + let mut pkt = ObservabilityAccess::default(); + + // Fill header + pkt.header.magic_number = ObservabilityAccess::MAGIC; + pkt.header.op_code = 0x0B; + pkt.header.sequence_id = 42; + + // Fill fields + pkt.stage = 2; // RESPONSE + pkt.access_level = 4; // ADMIN + pkt.access_key = [0x55u8; 32]; + + // Allocate buffer + let mut buf = [0u8; ObservabilityAccess::FIXED_SIZE]; + let bytes_written = pkt.serialize(&mut buf)?; + + assert_eq!(bytes_written, ObservabilityAccess::FIXED_SIZE); + // 'buf' is now ready to send + Ok(()) +} +``` + +### C. Deserialization +Call the static `::deserialize()` method passing a byte slice: + +```rust +use packets::ObservabilityAccess; + +fn handle_incoming_bytes(buf: &[u8]) { + match ObservabilityAccess::deserialize(buf) { + Ok(pkt) => { + println!("Parsed packet! Stage={}, Key={:?}", pkt.stage, pkt.access_key); + } + Err(_) => { + println!("Failed to deserialize packet (malformed or short buffer)."); + } + } +} +``` + +### D. Variable-Size Packets (e.g., `Vec`) +When a packet contains dynamic types (mapped as `Vec`), the serialized size changes dynamically. Ensure you allocate a larger buffer or a dynamic vector before serializing: + +```rust +use packets::RuntimeObservation; + +fn serialize_variable_packet(observation_payload: Vec) -> Result, ()> { + let mut pkt = RuntimeObservation::default(); + pkt.stage = 2; + pkt.observation_data = observation_payload; // Vec + + // Allocate buffer with overhead space + let mut buf = vec![0u8; RuntimeObservation::FIXED_SIZE + pkt.observation_data.len()]; + let bytes_written = pkt.serialize(&mut buf)?; + + // Truncate to size actually written + buf.truncate(bytes_written); + Ok(buf) +} +``` diff --git a/docs/SPECIFICATION.md b/docs/SPECIFICATION.md new file mode 100644 index 0000000..e449120 --- /dev/null +++ b/docs/SPECIFICATION.md @@ -0,0 +1,124 @@ +# Nalix Protogen Specification & Type Mapping + +This document details the serialization contract, C# packet definition rules, type mappings, and language binding specifications for the **Nalix Protogen** code generator. + +--- + +## 1. Type Mapping Contract + +To guarantee byte-level wire compatibility across platforms, C# types are mapped to specific unmanaged C and Rust types. + +### A. Primitive Types + +| C# Type | C Type | Rust Type | Size (Bytes) | Endianness | +| :--- | :--- | :--- | :--- | :--- | +| `byte` | `uint8_t` | `u8` | 1 | - | +| `sbyte` | `int8_t` | `i8` | 1 | - | +| `bool` | `uint8_t` | `u8` | 1 | - | +| `short` | `int16_t` | `i16` | 2 | Little-Endian | +| `ushort` | `uint16_t` | `u16` | 2 | Little-Endian | +| `int` | `int32_t` | `i32` | 4 | Little-Endian | +| `uint` | `uint32_t` | `u32` | 4 | Little-Endian | +| `long` | `int64_t` | `i64` | 8 | Little-Endian | +| `ulong` | `uint64_t` | `u64` | 8 | Little-Endian | +| `float` | `float` | `f32` | 4 | IEEE 754 | +| `double` | `double` | `f64` | 8 | IEEE 754 | + +### B. Special & Complex Types + +| C# Type | C Type | Rust Type | Size (Bytes) | Wire Layout | +| :--- | :--- | :--- | :--- | :--- | +| `Bytes32` | `Bytes32` | `[u8; 32]` | 32 | Raw byte array (`uint8_t[32]`) | +| `PacketHeader` | `PacketHeader` | `PacketHeader` | 10 | Packed explicit struct layout | +| `string` | `NalixString` | `Vec` | Variable | `[int32_t length][utf8_bytes]` | + +#### String Wire Format +Strings are encoded using the Nalix codec standard: +* Length is written as a signed 32-bit integer (`int32_t`) in little-endian order. +* A C# `null` string is serialized with length `-1` (no payload bytes). +* An empty string `""` is serialized with length `0` (no payload bytes). +* Non-empty strings are followed by their raw UTF-8 payload bytes. + +--- + +## 2. Unsupported C# Types + +The code generator excludes types without consistent unmanaged stack structures to prevent memory alignment and serialization mismatches: + +| Unsupported C# Type | Reason | Alternative Recommendation | +| :--- | :--- | :--- | +| `char` | C# `char` is UTF-16 (2 bytes), C/Rust `char` is UTF-8 (1 byte). | Use `byte` or `string` instead. | +| `decimal` | Lacks native hardware CPU/FPU operations. | Use `double` or scale integers (fixed-point). | +| `DateTime` | Platform/locale dependent serialization. | Use `long` (Ticks) or `ulong` (Unix epoch seconds). | +| `Guid` | Complex layout and different formatting standards. | Use `Bytes16` or two `ulong` fields. | +| `Nullable` | Requires wrapper heap object allocations. | Use a sentinel value (e.g., `-1`, `0`, or `uint.MaxValue`). | +| Arrays / lists | Dynamic heap references cannot be flat-serialized easily. | Use fixed-size fields or offsets. | + +--- + +## 3. Packet Definition Rules + +C# classes serving as packet models must follow these structuring directives: +* **Class Attributes:** Must decorate the class with `[Packet]`. The class must inherit `PacketBase`. +* **Field Ordering:** Decided by indexing attributes: + * `[SerializeHeader(index)]`: Order of fields serialized in the packet header (written first). + * `[SerializeOrder(index)]`: Order of fields serialized in the packet payload body (written after header). +* **Exclusion:** Decorate private or helper fields with `[SerializeIgnore]` to skip code generation. + +### Definition Example (C#) +```csharp +[Packet] +public sealed class Handshake : PacketBase +{ + [SerializeHeader(0)] + public PacketHeader _header; + + [SerializeOrder(0)] + public byte Stage; + + [SerializeOrder(1)] + public ushort Reason; + + [SerializeOrder(2)] + public ulong SessionToken; + + [SerializeOrder(3)] + public Bytes32 PublicKey; +} +``` + +--- + +## 4. Generated Bindings Outputs + +### C Output (`packets.h`) +```c +#if defined(_MSC_VER) + #pragma pack(push, 1) +#endif + +typedef struct { + PacketHeader _header; + uint8_t Stage; + uint16_t Reason; + uint64_t SessionToken; + Bytes32 PublicKey; +} Handshake; + +#if defined(_MSC_VER) + #pragma pack(pop) +#endif +``` + +### Rust Output (`packets.rs`) +```rust +#[repr(C)] +#[derive(Debug, Clone, Copy, Default, PartialEq)] +pub struct Handshake { + pub _header: PacketHeader, + pub stage: u8, + pub reason: u16, + pub session_token: u64, + pub public_key: [u8; 32], +} +``` diff --git a/examples/c/include/packets.h b/examples/c/include/packets.h new file mode 100644 index 0000000..7b18d3c --- /dev/null +++ b/examples/c/include/packets.h @@ -0,0 +1,84 @@ +// +// Generated by Nalix.Protogen — CHeaderGenerator +// DO NOT EDIT MANUALLY + +#pragma once + +#include +#include +#include +#include + +typedef struct { uint8_t data[32]; } Bytes32; + +// Length-prefixed UTF-8 string (matches Nalix codec wire format) +// null = length -1, empty = length 0, otherwise [int32_t length][utf8 bytes] +typedef struct { int32_t length; uint8_t* data; } NalixString; +void nalix_string_free(NalixString* s); + +#define PACKET_HEADER_SIZE 10 + +#if defined(_MSC_VER) + #pragma pack(push, 1) +#endif + +typedef struct { + uint32_t MagicNumber; + uint16_t OpCode; + uint8_t Flags; + uint8_t Priority; + uint16_t SequenceId; +} PacketHeader; + +#if defined(_MSC_VER) + #pragma pack(pop) +#endif + +// ObservabilityAccess — 46 bytes, magic 0x88DE9D82 +#define PACKET_OBSERVABILITYACCESS 0x88DE9D82U +#define OBSERVABILITYACCESS_SIZE 46 + +#if defined(_MSC_VER) + #pragma pack(push, 1) +#endif + +typedef struct { + PacketHeader Header; + uint8_t Stage; + uint16_t Reason; + uint8_t AccessLevel; + Bytes32 AccessKey; +} ObservabilityAccess; + +#if defined(_MSC_VER) + #pragma pack(pop) +#endif + +size_t observabilityaccess_serialize(const ObservabilityAccess* pkt, uint8_t* buf, size_t buf_size); +size_t observabilityaccess_deserialize(ObservabilityAccess* pkt, const uint8_t* buf, size_t buf_size); +bool observabilityaccess_equals(const ObservabilityAccess* a, const ObservabilityAccess* b); + +// RuntimeObservation — 18 bytes, magic 0x74AE4C55 +#define PACKET_RUNTIMEOBSERVATION 0x74AE4C55U +#define RUNTIMEOBSERVATION_SIZE 18 + +#if defined(_MSC_VER) + #pragma pack(push, 1) +#endif + +typedef struct { + PacketHeader Header; + uint8_t Stage; + uint8_t Target; + uint16_t Reason; + NalixString ObservationData; +} RuntimeObservation; + +#if defined(_MSC_VER) + #pragma pack(pop) +#endif + +size_t runtimeobservation_serialize(const RuntimeObservation* pkt, uint8_t* buf, size_t buf_size); +size_t runtimeobservation_deserialize(RuntimeObservation* pkt, const uint8_t* buf, size_t buf_size); +bool runtimeobservation_equals(const RuntimeObservation* a, const RuntimeObservation* b); + diff --git a/examples/c/src/packets.c b/examples/c/src/packets.c new file mode 100644 index 0000000..7a6b2e1 --- /dev/null +++ b/examples/c/src/packets.c @@ -0,0 +1,103 @@ +// +#include "packets.h" +#include + +void nalix_string_free(NalixString* s) { + if (s && s->data) { free(s->data); s->data = NULL; s->length = 0; } +} + +size_t observabilityaccess_serialize(const ObservabilityAccess* pkt, uint8_t* buf, size_t buf_size) +{ + if (!pkt || !buf || buf_size < 46) return 0; + uint8_t* ptr = buf; + + memcpy(ptr, &pkt->Header, PACKET_HEADER_SIZE); ptr += PACKET_HEADER_SIZE; + ptr[0] = (uint8_t)pkt->Stage; ptr += 1; + ptr[0] = (uint8_t)(pkt->Reason); + ptr[1] = (uint8_t)((pkt->Reason) >> 8); ptr += 2; + ptr[0] = (uint8_t)pkt->AccessLevel; ptr += 1; + memcpy(ptr, pkt->AccessKey.data, 32); ptr += 32; + return 46; +} + +size_t observabilityaccess_deserialize(ObservabilityAccess* pkt, const uint8_t* buf, size_t buf_size) +{ + if (!pkt || !buf || buf_size < 46) return 0; + const uint8_t* ptr = buf; + + memcpy(&pkt->Header, ptr, PACKET_HEADER_SIZE); ptr += PACKET_HEADER_SIZE; + pkt->Stage = (uint8_t)ptr[0]; ptr += 1; + pkt->Reason = (uint16_t)((uint16_t)ptr[0] | ((uint16_t)ptr[1] << 8)); ptr += 2; + pkt->AccessLevel = (uint8_t)ptr[0]; ptr += 1; + memcpy(pkt->AccessKey.data, ptr, 32); ptr += 32; + return 46; +} + +bool observabilityaccess_equals(const ObservabilityAccess* a, const ObservabilityAccess* b) +{ + if (!a || !b) return false; + return memcmp(a, b, sizeof(ObservabilityAccess)) == 0; +} + +size_t runtimeobservation_serialize(const RuntimeObservation* pkt, uint8_t* buf, size_t buf_size) +{ + if (!pkt || !buf || buf_size < 18) return 0; + uint8_t* ptr = buf; + + memcpy(ptr, &pkt->Header, PACKET_HEADER_SIZE); ptr += PACKET_HEADER_SIZE; + ptr[0] = (uint8_t)pkt->Stage; ptr += 1; + ptr[0] = (uint8_t)pkt->Target; ptr += 1; + ptr[0] = (uint8_t)(pkt->Reason); + ptr[1] = (uint8_t)((pkt->Reason) >> 8); ptr += 2; + { + int32_t slen = pkt->ObservationData.length; + if ((size_t)(ptr - buf) + 4 + (slen > 0 ? slen : 0) > buf_size) return 0; + memcpy(ptr, &slen, 4); ptr += 4; + if (slen > 0 && pkt->ObservationData.data) { memcpy(ptr, pkt->ObservationData.data, slen); ptr += slen; } + } + return (size_t)(ptr - buf); +} + +size_t runtimeobservation_deserialize(RuntimeObservation* pkt, const uint8_t* buf, size_t buf_size) +{ + if (!pkt || !buf || buf_size < 18) return 0; + const uint8_t* ptr = buf; + + memcpy(&pkt->Header, ptr, PACKET_HEADER_SIZE); ptr += PACKET_HEADER_SIZE; + pkt->Stage = (uint8_t)ptr[0]; ptr += 1; + pkt->Target = (uint8_t)ptr[0]; ptr += 1; + pkt->Reason = (uint16_t)((uint16_t)ptr[0] | ((uint16_t)ptr[1] << 8)); ptr += 2; + { + int32_t slen; + memcpy(&slen, ptr, 4); ptr += 4; + pkt->ObservationData.length = slen; + if (slen > 0) { + if ((size_t)(ptr - buf) + slen > buf_size) return 0; + pkt->ObservationData.data = (uint8_t*)malloc(slen); + memcpy(pkt->ObservationData.data, ptr, slen); + ptr += slen; + } else { + pkt->ObservationData.data = NULL; + } + } + return (size_t)(ptr - buf); +} + +bool runtimeobservation_equals(const RuntimeObservation* a, const RuntimeObservation* b) +{ + if (!a || !b) return false; + if (memcmp(&a->Header, &b->Header, PACKET_HEADER_SIZE) != 0) return false; + if (memcmp(&a->Stage, &b->Stage, 1) != 0) return false; + if (memcmp(&a->Target, &b->Target, 1) != 0) return false; + if (memcmp(&a->Reason, &b->Reason, 2) != 0) return false; + if (a->ObservationData.length != b->ObservationData.length) return false; + if (a->ObservationData.length > 0 && memcmp(a->ObservationData.data, b->ObservationData.data, a->ObservationData.length) != 0) return false; + return true; +} + +void runtimeobservation_free(RuntimeObservation* pkt) +{ + if (!pkt) return; + nalix_string_free(&pkt->ObservationData); +} + diff --git a/examples/c/tests/packet_test.c b/examples/c/tests/packet_test.c new file mode 100644 index 0000000..4a335b9 --- /dev/null +++ b/examples/c/tests/packet_test.c @@ -0,0 +1,60 @@ +// +// Compile: gcc -I../include -o packet_test packet_test.c && ./packet_test + +#include "packets.h" +#include +#include +#include + +static void test_observabilityaccess_round_trip(void) +{ + ObservabilityAccess pkt; + memset(&pkt, 0, sizeof(pkt)); + memset(&pkt.Header, 0, sizeof(PacketHeader)); + pkt.Stage = 0x02; + pkt.Reason = 0x0066; + pkt.AccessLevel = 0x04; + { Bytes32 tmp = {{ 0x05 }}; pkt.AccessKey = tmp; } + + uint8_t buf[OBSERVABILITYACCESS_SIZE]; + size_t written = observabilityaccess_serialize(&pkt, buf, sizeof(buf)); + assert(written == 46); + + ObservabilityAccess parsed; + memset(&parsed, 0, sizeof(parsed)); + size_t rd = observabilityaccess_deserialize(&parsed, buf, sizeof(buf)); + assert(rd == 46); + assert(observabilityaccess_equals(&pkt, &parsed)); + printf(" [PASS] ObservabilityAccess round-trip\n"); +} + +static void test_runtimeobservation_round_trip(void) +{ + RuntimeObservation pkt; + memset(&pkt, 0, sizeof(pkt)); + memset(&pkt.Header, 0, sizeof(PacketHeader)); + pkt.Stage = 0x02; + pkt.Target = 0x03; + pkt.Reason = 0x0067; + { NalixString tmp = { 0, NULL }; pkt.ObservationData = tmp; } + + uint8_t buf[RUNTIMEOBSERVATION_SIZE]; + size_t written = runtimeobservation_serialize(&pkt, buf, sizeof(buf)); + assert(written == 18); + + RuntimeObservation parsed; + memset(&parsed, 0, sizeof(parsed)); + size_t rd = runtimeobservation_deserialize(&parsed, buf, sizeof(buf)); + assert(rd == 18); + assert(runtimeobservation_equals(&pkt, &parsed)); + printf(" [PASS] RuntimeObservation round-trip\n"); +} + +int main(void) +{ + printf("Nalix Protocol Round-trip Tests\n\n"); + test_observabilityaccess_round_trip(); + test_runtimeobservation_round_trip(); + printf("\nAll tests passed!\n"); + return 0; +} diff --git a/examples/rust/src/packets.rs b/examples/rust/src/packets.rs new file mode 100644 index 0000000..86d850e --- /dev/null +++ b/examples/rust/src/packets.rs @@ -0,0 +1,167 @@ +// +// Generated by Nalix.Protogen — RustGenerator +// DO NOT EDIT MANUALLY + +#![allow(dead_code)] +#![allow(unused_variables)] + +/// Common 10-byte packet header. +#[repr(C)] +#[derive(Debug, Clone, Copy, Default, PartialEq)] +pub struct PacketHeader { + pub magic_number: u32, + pub op_code: u16, + pub flags: u8, + pub priority: u8, + pub sequence_id: u16, +} + +impl PacketHeader { + pub const SIZE: usize = 10; + + pub fn serialize(&self, buf: &mut [u8]) -> Result { + if buf.len() < Self::SIZE { return Err(()); } + let mut offset = 0; + buf[offset..offset+4].copy_from_slice(&(self.magic_number as u32).to_le_bytes()); + offset += 4; + buf[offset..offset+2].copy_from_slice(&(self.op_code as u16).to_le_bytes()); + offset += 2; + buf[offset] = self.flags as u8; + offset += 1; + buf[offset] = self.priority as u8; + offset += 1; + buf[offset..offset+2].copy_from_slice(&(self.sequence_id as u16).to_le_bytes()); + offset += 2; + Ok(Self::SIZE) + } + + pub fn deserialize(buf: &[u8]) -> Result { + if buf.len() < Self::SIZE { return Err(()); } + let mut h = Self::default(); + let mut offset = 0; + h.magic_number = u32::from_le_bytes(buf[offset..offset+4].try_into().unwrap()); + offset += 4; + h.op_code = u16::from_le_bytes([buf[offset], buf[offset+1]]); + offset += 2; + h.flags = buf[offset] as u8; + offset += 1; + h.priority = buf[offset] as u8; + offset += 1; + h.sequence_id = u16::from_le_bytes([buf[offset], buf[offset+1]]); + offset += 2; + Ok(h) + } +} + +/// ObservabilityAccess — wire size: 46 bytes, magic: 0x88DE9D82 +#[repr(C)] +#[derive(Debug, Clone, Copy, Default, PartialEq)] +pub struct ObservabilityAccess { + pub header: PacketHeader, + pub stage: u8, + pub reason: u16, + pub access_level: u8, + pub access_key: [u8; 32], +} + +impl ObservabilityAccess { + pub const FIXED_SIZE: usize = 46; + pub const MAGIC: u32 = 0x88DE9D82; + + pub fn serialize(&self, buf: &mut [u8]) -> Result { + if buf.len() < Self::FIXED_SIZE { return Err(()); } + let mut offset = 0; + buf[offset..offset+10].copy_from_slice(&self.header.serialize_to_bytes()); + offset += 10; + buf[offset] = self.stage as u8; + offset += 1; + buf[offset..offset+2].copy_from_slice(&(self.reason as u16).to_le_bytes()); + offset += 2; + buf[offset] = self.access_level as u8; + offset += 1; + buf[offset..offset+32].copy_from_slice(&self.access_key); + offset += 32; + Ok(Self::FIXED_SIZE) + } + + pub fn deserialize(buf: &[u8]) -> Result { + if buf.len() < Self::FIXED_SIZE { return Err(()); } + let mut pkt = Self::default(); + let mut offset = 0; + pkt.header = PacketHeader::deserialize(&buf[offset..offset+10]).map_err(|_| ())?; + offset += 10; + pkt.stage = buf[offset] as u8; + offset += 1; + pkt.reason = u16::from_le_bytes([buf[offset], buf[offset+1]]); + offset += 2; + pkt.access_level = buf[offset] as u8; + offset += 1; + pkt.access_key.copy_from_slice(&buf[offset..offset+32]); + offset += 32; + Ok(pkt) + } + + pub fn equals(&self, other: &Self) -> bool { + let mut a = [0u8; Self::FIXED_SIZE]; + let mut b = [0u8; Self::FIXED_SIZE]; + let _ = self.serialize(&mut a); + let _ = other.serialize(&mut b); + a == b + } +} + +/// RuntimeObservation — fixed: 18 bytes + variable, magic: 0x74AE4C55 +#[derive(Debug, Default, PartialEq)] +pub struct RuntimeObservation { + pub header: PacketHeader, + pub stage: u8, + pub target: u8, + pub reason: u16, + pub observation_data: u8, +} + +impl RuntimeObservation { + pub const FIXED_SIZE: usize = 18; + pub const MAGIC: u32 = 0x74AE4C55; + + pub fn serialize(&self, buf: &mut [u8]) -> Result { + let required = 18 + + self.observation_data.len() + ; + if buf.len() < required { return Err(()); } + let mut offset = 0; + buf[offset..offset+10].copy_from_slice(&self.header.serialize_to_bytes()); + offset += 10; + buf[offset] = self.stage as u8; + offset += 1; + buf[offset] = self.target as u8; + offset += 1; + buf[offset..offset+2].copy_from_slice(&(self.reason as u16).to_le_bytes()); + offset += 2; + buf[offset..offset+4].copy_from_slice(&(self.observation_data as u32).to_le_bytes()); + offset += 4; + Ok(offset) + } + + pub fn deserialize(buf: &[u8]) -> Result { + if buf.len() < 18 { return Err(()); } + let mut pkt = Self::default(); + let mut offset = 0; + pkt.header = PacketHeader::deserialize(&buf[offset..offset+10]).map_err(|_| ())?; + offset += 10; + pkt.stage = buf[offset] as u8; + offset += 1; + pkt.target = buf[offset] as u8; + offset += 1; + pkt.reason = u16::from_le_bytes([buf[offset], buf[offset+1]]); + offset += 2; + pkt.observation_data = u8::from_le_bytes(buf[offset..offset+4].try_into().unwrap()); + offset += 4; + Ok(pkt) + } + + pub fn equals(&self, other: &Self) -> bool { + self == other + } +} + diff --git a/examples/rust/src/packets_tests.rs b/examples/rust/src/packets_tests.rs new file mode 100644 index 0000000..922ff83 --- /dev/null +++ b/examples/rust/src/packets_tests.rs @@ -0,0 +1,45 @@ +// +// Round-trip serialization tests — append to src/packets.rs + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn observability_access_round_trip() { + let mut pkt = ObservabilityAccess::default(); + pkt.header = PacketHeader::default(); + pkt.stage = 0x02u8; + pkt.reason = 0x0066u16; + pkt.access_level = 0x04u8; + pkt.access_key = [0x05u8; 32]; + + let mut buf = [0u8; ObservabilityAccess::FIXED_SIZE]; + let written = pkt.serialize(&mut buf).expect("serialize failed"); + assert_eq!(written, ObservabilityAccess::FIXED_SIZE); + + let parsed = ObservabilityAccess::deserialize(&buf).expect("deserialize failed"); + assert_eq!(pkt, parsed); + } + + #[test] + fn runtime_observation_round_trip() { + let mut pkt = RuntimeObservation::default(); + pkt.header = PacketHeader::default(); + pkt.stage = 0x02u8; + pkt.target = 0x03u8; + pkt.reason = 0x0067u16; + pkt.observation_data = 0x05u8; + + let mut buf = vec![0u8; RuntimeObservation::FIXED_SIZE + 1024]; + let written = pkt.serialize(&mut buf).expect("serialize failed"); + let parsed = RuntimeObservation::deserialize(&buf[..written]).expect("deserialize failed"); + assert_eq!(pkt, parsed); + } + + #[test] + fn verify_sizes() { + assert_eq!(ObservabilityAccess::FIXED_SIZE, 46); + assert_eq!(RuntimeObservation::FIXED_SIZE, 18); + } +} diff --git a/gen/rust/tests/packet_tests.rs b/examples/rust/tests/packet_tests.rs similarity index 100% rename from gen/rust/tests/packet_tests.rs rename to examples/rust/tests/packet_tests.rs diff --git a/gen/c/include/packets.h b/gen/c/include/packets.h deleted file mode 100644 index feded46..0000000 --- a/gen/c/include/packets.h +++ /dev/null @@ -1,160 +0,0 @@ -// -// Generated by Nalix.Protocol.Toolkit — CHeaderGenerator -// DO NOT EDIT MANUALLY - -#pragma once - -#include -#include -#include -#include - -typedef struct { uint8_t data[32]; } Bytes32; - -// Length-prefixed UTF-8 string (matches Nalix codec wire format) -// null = length -1, empty = length 0, otherwise [int32_t length][utf8 bytes] -typedef struct { int32_t length; uint8_t* data; } NalixString; -void nalix_string_free(NalixString* s); - -#define PACKET_HEADER_SIZE 10 - -#if defined(_MSC_VER) - #pragma pack(push, 1) -#endif - -typedef struct { - uint32_t MagicNumber; - uint16_t OpCode; - uint8_t Flags; - uint8_t Priority; - uint16_t SequenceId; -} PacketHeader; - -#if defined(_MSC_VER) - #pragma pack(pop) -#endif - -// Control — 29 bytes, magic 0xD84C915B -#define PACKET_CONTROL 0xD84C915BU -#define CONTROL_SIZE 29 - -#if defined(_MSC_VER) - #pragma pack(push, 1) -#endif - -typedef struct { - PacketHeader _header; - uint16_t Reason; - uint8_t Type; - int64_t Timestamp; - int64_t MonoTicks; -} Control; - -#if defined(_MSC_VER) - #pragma pack(pop) -#endif - -size_t control_serialize(const Control* pkt, uint8_t* buf, size_t buf_size); -size_t control_deserialize(Control* pkt, const uint8_t* buf, size_t buf_size); -bool control_equals(const Control* a, const Control* b); - -// Directive — 25 bytes, magic 0xDE96BC8B -#define PACKET_DIRECTIVE 0xDE96BC8BU -#define DIRECTIVE_SIZE 25 - -#if defined(_MSC_VER) - #pragma pack(push, 1) -#endif - -typedef struct { - PacketHeader _header; - uint8_t Type; - uint16_t Reason; - uint8_t Action; - uint8_t Control; - uint32_t Arg0; - uint32_t Arg1; - uint16_t Arg2; -} Directive; - -#if defined(_MSC_VER) - #pragma pack(pop) -#endif - -size_t directive_serialize(const Directive* pkt, uint8_t* buf, size_t buf_size); -size_t directive_deserialize(Directive* pkt, const uint8_t* buf, size_t buf_size); -bool directive_equals(const Directive* a, const Directive* b); - -// Handshake — 149 bytes, magic 0xA8CB1867 -#define PACKET_HANDSHAKE 0xA8CB1867U -#define HANDSHAKE_SIZE 149 - -#if defined(_MSC_VER) - #pragma pack(push, 1) -#endif - -typedef struct { - PacketHeader _header; - uint8_t Stage; - uint16_t Reason; - uint64_t SessionToken; - Bytes32 PublicKey; - Bytes32 Nonce; - Bytes32 Proof; - Bytes32 TranscriptHash; -} Handshake; - -#if defined(_MSC_VER) - #pragma pack(pop) -#endif - -size_t handshake_serialize(const Handshake* pkt, uint8_t* buf, size_t buf_size); -size_t handshake_deserialize(Handshake* pkt, const uint8_t* buf, size_t buf_size); -bool handshake_equals(const Handshake* a, const Handshake* b); - -// KeyExchange — 43 bytes, magic 0x73F416B8 -#define PACKET_KEYEXCHANGE 0x73F416B8U -#define KEYEXCHANGE_SIZE 43 - -#if defined(_MSC_VER) - #pragma pack(push, 1) -#endif - -typedef struct { - PacketHeader _header; - uint8_t Stage; - Bytes32 PublicKey; -} KeyExchange; - -#if defined(_MSC_VER) - #pragma pack(pop) -#endif - -size_t keyexchange_serialize(const KeyExchange* pkt, uint8_t* buf, size_t buf_size); -size_t keyexchange_deserialize(KeyExchange* pkt, const uint8_t* buf, size_t buf_size); -bool keyexchange_equals(const KeyExchange* a, const KeyExchange* b); - -// SessionResume — 53 bytes, magic 0x99E39CC5 -#define PACKET_SESSIONRESUME 0x99E39CC5U -#define SESSIONRESUME_SIZE 53 - -#if defined(_MSC_VER) - #pragma pack(push, 1) -#endif - -typedef struct { - PacketHeader _header; - uint8_t Stage; - uint64_t SessionToken; - uint16_t Reason; - Bytes32 Proof; -} SessionResume; - -#if defined(_MSC_VER) - #pragma pack(pop) -#endif - -size_t sessionresume_serialize(const SessionResume* pkt, uint8_t* buf, size_t buf_size); -size_t sessionresume_deserialize(SessionResume* pkt, const uint8_t* buf, size_t buf_size); -bool sessionresume_equals(const SessionResume* a, const SessionResume* b); - diff --git a/gen/c/src/packets.c b/gen/c/src/packets.c deleted file mode 100644 index 22b433c..0000000 --- a/gen/c/src/packets.c +++ /dev/null @@ -1,215 +0,0 @@ -// -#include "packets.h" -#include - -void nalix_string_free(NalixString* s) { - if (s && s->data) { free(s->data); s->data = NULL; s->length = 0; } -} - -size_t control_serialize(const Control* pkt, uint8_t* buf, size_t buf_size) -{ - if (!pkt || !buf || buf_size < 29) return 0; - uint8_t* ptr = buf; - - memcpy(ptr, &pkt->_header, PACKET_HEADER_SIZE); ptr += PACKET_HEADER_SIZE; - ptr[0] = (uint8_t)(pkt->Reason); - ptr[1] = (uint8_t)((pkt->Reason) >> 8); ptr += 2; - ptr[0] = (uint8_t)pkt->Type; ptr += 1; - ptr[0] = (uint8_t)(pkt->Timestamp); - ptr[1] = (uint8_t)((pkt->Timestamp) >> 8); - ptr[2] = (uint8_t)((pkt->Timestamp) >> 16); - ptr[3] = (uint8_t)((pkt->Timestamp) >> 24); - ptr[4] = (uint8_t)((pkt->Timestamp) >> 32); - ptr[5] = (uint8_t)((pkt->Timestamp) >> 40); - ptr[6] = (uint8_t)((pkt->Timestamp) >> 48); - ptr[7] = (uint8_t)((pkt->Timestamp) >> 56); ptr += 8; - ptr[0] = (uint8_t)(pkt->MonoTicks); - ptr[1] = (uint8_t)((pkt->MonoTicks) >> 8); - ptr[2] = (uint8_t)((pkt->MonoTicks) >> 16); - ptr[3] = (uint8_t)((pkt->MonoTicks) >> 24); - ptr[4] = (uint8_t)((pkt->MonoTicks) >> 32); - ptr[5] = (uint8_t)((pkt->MonoTicks) >> 40); - ptr[6] = (uint8_t)((pkt->MonoTicks) >> 48); - ptr[7] = (uint8_t)((pkt->MonoTicks) >> 56); ptr += 8; - return 29; -} - -size_t control_deserialize(Control* pkt, const uint8_t* buf, size_t buf_size) -{ - if (!pkt || !buf || buf_size < 29) return 0; - const uint8_t* ptr = buf; - - memcpy(&pkt->_header, ptr, PACKET_HEADER_SIZE); ptr += PACKET_HEADER_SIZE; - pkt->Reason = (uint16_t)((uint16_t)ptr[0] | ((uint16_t)ptr[1] << 8)); ptr += 2; - pkt->Type = (uint8_t)ptr[0]; ptr += 1; - pkt->Timestamp = (int64_t)((uint64_t)ptr[0] | ((uint64_t)ptr[1] << 8) | ((uint64_t)ptr[2] << 16) | ((uint64_t)ptr[3] << 24) | ((uint64_t)ptr[4] << 32) | ((uint64_t)ptr[5] << 40) | ((uint64_t)ptr[6] << 48) | ((uint64_t)ptr[7] << 56)); ptr += 8; - pkt->MonoTicks = (int64_t)((uint64_t)ptr[0] | ((uint64_t)ptr[1] << 8) | ((uint64_t)ptr[2] << 16) | ((uint64_t)ptr[3] << 24) | ((uint64_t)ptr[4] << 32) | ((uint64_t)ptr[5] << 40) | ((uint64_t)ptr[6] << 48) | ((uint64_t)ptr[7] << 56)); ptr += 8; - return 29; -} - -bool control_equals(const Control* a, const Control* b) -{ - if (!a || !b) return false; - return memcmp(a, b, sizeof(Control)) == 0; -} - -size_t directive_serialize(const Directive* pkt, uint8_t* buf, size_t buf_size) -{ - if (!pkt || !buf || buf_size < 25) return 0; - uint8_t* ptr = buf; - - memcpy(ptr, &pkt->_header, PACKET_HEADER_SIZE); ptr += PACKET_HEADER_SIZE; - ptr[0] = (uint8_t)pkt->Type; ptr += 1; - ptr[0] = (uint8_t)(pkt->Reason); - ptr[1] = (uint8_t)((pkt->Reason) >> 8); ptr += 2; - ptr[0] = (uint8_t)pkt->Action; ptr += 1; - ptr[0] = (uint8_t)pkt->Control; ptr += 1; - ptr[0] = (uint8_t)(pkt->Arg0); - ptr[1] = (uint8_t)((pkt->Arg0) >> 8); - ptr[2] = (uint8_t)((pkt->Arg0) >> 16); - ptr[3] = (uint8_t)((pkt->Arg0) >> 24); ptr += 4; - ptr[0] = (uint8_t)(pkt->Arg1); - ptr[1] = (uint8_t)((pkt->Arg1) >> 8); - ptr[2] = (uint8_t)((pkt->Arg1) >> 16); - ptr[3] = (uint8_t)((pkt->Arg1) >> 24); ptr += 4; - ptr[0] = (uint8_t)(pkt->Arg2); - ptr[1] = (uint8_t)((pkt->Arg2) >> 8); ptr += 2; - return 25; -} - -size_t directive_deserialize(Directive* pkt, const uint8_t* buf, size_t buf_size) -{ - if (!pkt || !buf || buf_size < 25) return 0; - const uint8_t* ptr = buf; - - memcpy(&pkt->_header, ptr, PACKET_HEADER_SIZE); ptr += PACKET_HEADER_SIZE; - pkt->Type = (uint8_t)ptr[0]; ptr += 1; - pkt->Reason = (uint16_t)((uint16_t)ptr[0] | ((uint16_t)ptr[1] << 8)); ptr += 2; - pkt->Action = (uint8_t)ptr[0]; ptr += 1; - pkt->Control = (uint8_t)ptr[0]; ptr += 1; - pkt->Arg0 = (uint32_t)((uint32_t)ptr[0] | ((uint32_t)ptr[1] << 8) | ((uint32_t)ptr[2] << 16) | ((uint32_t)ptr[3] << 24)); ptr += 4; - pkt->Arg1 = (uint32_t)((uint32_t)ptr[0] | ((uint32_t)ptr[1] << 8) | ((uint32_t)ptr[2] << 16) | ((uint32_t)ptr[3] << 24)); ptr += 4; - pkt->Arg2 = (uint16_t)((uint16_t)ptr[0] | ((uint16_t)ptr[1] << 8)); ptr += 2; - return 25; -} - -bool directive_equals(const Directive* a, const Directive* b) -{ - if (!a || !b) return false; - return memcmp(a, b, sizeof(Directive)) == 0; -} - -size_t handshake_serialize(const Handshake* pkt, uint8_t* buf, size_t buf_size) -{ - if (!pkt || !buf || buf_size < 149) return 0; - uint8_t* ptr = buf; - - memcpy(ptr, &pkt->_header, PACKET_HEADER_SIZE); ptr += PACKET_HEADER_SIZE; - ptr[0] = (uint8_t)pkt->Stage; ptr += 1; - ptr[0] = (uint8_t)(pkt->Reason); - ptr[1] = (uint8_t)((pkt->Reason) >> 8); ptr += 2; - ptr[0] = (uint8_t)(pkt->SessionToken); - ptr[1] = (uint8_t)((pkt->SessionToken) >> 8); - ptr[2] = (uint8_t)((pkt->SessionToken) >> 16); - ptr[3] = (uint8_t)((pkt->SessionToken) >> 24); - ptr[4] = (uint8_t)((pkt->SessionToken) >> 32); - ptr[5] = (uint8_t)((pkt->SessionToken) >> 40); - ptr[6] = (uint8_t)((pkt->SessionToken) >> 48); - ptr[7] = (uint8_t)((pkt->SessionToken) >> 56); ptr += 8; - memcpy(ptr, pkt->PublicKey.data, 32); ptr += 32; - memcpy(ptr, pkt->Nonce.data, 32); ptr += 32; - memcpy(ptr, pkt->Proof.data, 32); ptr += 32; - memcpy(ptr, pkt->TranscriptHash.data, 32); ptr += 32; - return 149; -} - -size_t handshake_deserialize(Handshake* pkt, const uint8_t* buf, size_t buf_size) -{ - if (!pkt || !buf || buf_size < 149) return 0; - const uint8_t* ptr = buf; - - memcpy(&pkt->_header, ptr, PACKET_HEADER_SIZE); ptr += PACKET_HEADER_SIZE; - pkt->Stage = (uint8_t)ptr[0]; ptr += 1; - pkt->Reason = (uint16_t)((uint16_t)ptr[0] | ((uint16_t)ptr[1] << 8)); ptr += 2; - pkt->SessionToken = (uint64_t)((uint64_t)ptr[0] | ((uint64_t)ptr[1] << 8) | ((uint64_t)ptr[2] << 16) | ((uint64_t)ptr[3] << 24) | ((uint64_t)ptr[4] << 32) | ((uint64_t)ptr[5] << 40) | ((uint64_t)ptr[6] << 48) | ((uint64_t)ptr[7] << 56)); ptr += 8; - memcpy(pkt->PublicKey.data, ptr, 32); ptr += 32; - memcpy(pkt->Nonce.data, ptr, 32); ptr += 32; - memcpy(pkt->Proof.data, ptr, 32); ptr += 32; - memcpy(pkt->TranscriptHash.data, ptr, 32); ptr += 32; - return 149; -} - -bool handshake_equals(const Handshake* a, const Handshake* b) -{ - if (!a || !b) return false; - return memcmp(a, b, sizeof(Handshake)) == 0; -} - -size_t keyexchange_serialize(const KeyExchange* pkt, uint8_t* buf, size_t buf_size) -{ - if (!pkt || !buf || buf_size < 43) return 0; - uint8_t* ptr = buf; - - memcpy(ptr, &pkt->_header, PACKET_HEADER_SIZE); ptr += PACKET_HEADER_SIZE; - ptr[0] = (uint8_t)pkt->Stage; ptr += 1; - memcpy(ptr, pkt->PublicKey.data, 32); ptr += 32; - return 43; -} - -size_t keyexchange_deserialize(KeyExchange* pkt, const uint8_t* buf, size_t buf_size) -{ - if (!pkt || !buf || buf_size < 43) return 0; - const uint8_t* ptr = buf; - - memcpy(&pkt->_header, ptr, PACKET_HEADER_SIZE); ptr += PACKET_HEADER_SIZE; - pkt->Stage = (uint8_t)ptr[0]; ptr += 1; - memcpy(pkt->PublicKey.data, ptr, 32); ptr += 32; - return 43; -} - -bool keyexchange_equals(const KeyExchange* a, const KeyExchange* b) -{ - if (!a || !b) return false; - return memcmp(a, b, sizeof(KeyExchange)) == 0; -} - -size_t sessionresume_serialize(const SessionResume* pkt, uint8_t* buf, size_t buf_size) -{ - if (!pkt || !buf || buf_size < 53) return 0; - uint8_t* ptr = buf; - - memcpy(ptr, &pkt->_header, PACKET_HEADER_SIZE); ptr += PACKET_HEADER_SIZE; - ptr[0] = (uint8_t)pkt->Stage; ptr += 1; - ptr[0] = (uint8_t)(pkt->SessionToken); - ptr[1] = (uint8_t)((pkt->SessionToken) >> 8); - ptr[2] = (uint8_t)((pkt->SessionToken) >> 16); - ptr[3] = (uint8_t)((pkt->SessionToken) >> 24); - ptr[4] = (uint8_t)((pkt->SessionToken) >> 32); - ptr[5] = (uint8_t)((pkt->SessionToken) >> 40); - ptr[6] = (uint8_t)((pkt->SessionToken) >> 48); - ptr[7] = (uint8_t)((pkt->SessionToken) >> 56); ptr += 8; - ptr[0] = (uint8_t)(pkt->Reason); - ptr[1] = (uint8_t)((pkt->Reason) >> 8); ptr += 2; - memcpy(ptr, pkt->Proof.data, 32); ptr += 32; - return 53; -} - -size_t sessionresume_deserialize(SessionResume* pkt, const uint8_t* buf, size_t buf_size) -{ - if (!pkt || !buf || buf_size < 53) return 0; - const uint8_t* ptr = buf; - - memcpy(&pkt->_header, ptr, PACKET_HEADER_SIZE); ptr += PACKET_HEADER_SIZE; - pkt->Stage = (uint8_t)ptr[0]; ptr += 1; - pkt->SessionToken = (uint64_t)((uint64_t)ptr[0] | ((uint64_t)ptr[1] << 8) | ((uint64_t)ptr[2] << 16) | ((uint64_t)ptr[3] << 24) | ((uint64_t)ptr[4] << 32) | ((uint64_t)ptr[5] << 40) | ((uint64_t)ptr[6] << 48) | ((uint64_t)ptr[7] << 56)); ptr += 8; - pkt->Reason = (uint16_t)((uint16_t)ptr[0] | ((uint16_t)ptr[1] << 8)); ptr += 2; - memcpy(pkt->Proof.data, ptr, 32); ptr += 32; - return 53; -} - -bool sessionresume_equals(const SessionResume* a, const SessionResume* b) -{ - if (!a || !b) return false; - return memcmp(a, b, sizeof(SessionResume)) == 0; -} - diff --git a/gen/c/tests/packet_test.c b/gen/c/tests/packet_test.c deleted file mode 100644 index 629c4dc..0000000 --- a/gen/c/tests/packet_test.c +++ /dev/null @@ -1,133 +0,0 @@ -// -// Compile: gcc -I../include -o packet_test packet_test.c && ./packet_test - -#include "packets.h" -#include -#include -#include - -static void test_control_round_trip(void) -{ - Control pkt; - memset(&pkt, 0, sizeof(pkt)); - memset(&pkt._header, 0, sizeof(PacketHeader)); - pkt.Reason = 0x0065; - pkt.Type = 0x03; - pkt.Timestamp = 0x0000000000002713ULL; - pkt.MonoTicks = 0x0000000000002714ULL; - - uint8_t buf[CONTROL_SIZE]; - size_t written = control_serialize(&pkt, buf, sizeof(buf)); - assert(written == 29); - - Control parsed; - memset(&parsed, 0, sizeof(parsed)); - size_t rd = control_deserialize(&parsed, buf, sizeof(buf)); - assert(rd == 29); - assert(control_equals(&pkt, &parsed)); - printf(" [PASS] Control round-trip\n"); -} - -static void test_directive_round_trip(void) -{ - Directive pkt; - memset(&pkt, 0, sizeof(pkt)); - memset(&pkt._header, 0, sizeof(PacketHeader)); - pkt.Type = 0x02; - pkt.Reason = 0x0066; - pkt.Action = 0x04; - pkt.Control = 0x05; - pkt.Arg0 = 0x000003ED; - pkt.Arg1 = 0x000003EE; - pkt.Arg2 = 0x006B; - - uint8_t buf[DIRECTIVE_SIZE]; - size_t written = directive_serialize(&pkt, buf, sizeof(buf)); - assert(written == 25); - - Directive parsed; - memset(&parsed, 0, sizeof(parsed)); - size_t rd = directive_deserialize(&parsed, buf, sizeof(buf)); - assert(rd == 25); - assert(directive_equals(&pkt, &parsed)); - printf(" [PASS] Directive round-trip\n"); -} - -static void test_handshake_round_trip(void) -{ - Handshake pkt; - memset(&pkt, 0, sizeof(pkt)); - memset(&pkt._header, 0, sizeof(PacketHeader)); - pkt.Stage = 0x02; - pkt.Reason = 0x0066; - pkt.SessionToken = 0x0000000000002713ULL; - { Bytes32 tmp = {{ 0x05 }}; pkt.PublicKey = tmp; } - { Bytes32 tmp = {{ 0x06 }}; pkt.Nonce = tmp; } - { Bytes32 tmp = {{ 0x07 }}; pkt.Proof = tmp; } - { Bytes32 tmp = {{ 0x08 }}; pkt.TranscriptHash = tmp; } - - uint8_t buf[HANDSHAKE_SIZE]; - size_t written = handshake_serialize(&pkt, buf, sizeof(buf)); - assert(written == 149); - - Handshake parsed; - memset(&parsed, 0, sizeof(parsed)); - size_t rd = handshake_deserialize(&parsed, buf, sizeof(buf)); - assert(rd == 149); - assert(handshake_equals(&pkt, &parsed)); - printf(" [PASS] Handshake round-trip\n"); -} - -static void test_keyexchange_round_trip(void) -{ - KeyExchange pkt; - memset(&pkt, 0, sizeof(pkt)); - memset(&pkt._header, 0, sizeof(PacketHeader)); - pkt.Stage = 0x02; - { Bytes32 tmp = {{ 0x03 }}; pkt.PublicKey = tmp; } - - uint8_t buf[KEYEXCHANGE_SIZE]; - size_t written = keyexchange_serialize(&pkt, buf, sizeof(buf)); - assert(written == 43); - - KeyExchange parsed; - memset(&parsed, 0, sizeof(parsed)); - size_t rd = keyexchange_deserialize(&parsed, buf, sizeof(buf)); - assert(rd == 43); - assert(keyexchange_equals(&pkt, &parsed)); - printf(" [PASS] KeyExchange round-trip\n"); -} - -static void test_sessionresume_round_trip(void) -{ - SessionResume pkt; - memset(&pkt, 0, sizeof(pkt)); - memset(&pkt._header, 0, sizeof(PacketHeader)); - pkt.Stage = 0x02; - pkt.SessionToken = 0x0000000000002712ULL; - pkt.Reason = 0x0067; - { Bytes32 tmp = {{ 0x05 }}; pkt.Proof = tmp; } - - uint8_t buf[SESSIONRESUME_SIZE]; - size_t written = sessionresume_serialize(&pkt, buf, sizeof(buf)); - assert(written == 53); - - SessionResume parsed; - memset(&parsed, 0, sizeof(parsed)); - size_t rd = sessionresume_deserialize(&parsed, buf, sizeof(buf)); - assert(rd == 53); - assert(sessionresume_equals(&pkt, &parsed)); - printf(" [PASS] SessionResume round-trip\n"); -} - -int main(void) -{ - printf("Nalix Protocol Round-trip Tests\n\n"); - test_control_round_trip(); - test_directive_round_trip(); - test_handshake_round_trip(); - test_keyexchange_round_trip(); - test_sessionresume_round_trip(); - printf("\nAll tests passed!\n"); - return 0; -} diff --git a/gen/rust/src/packets.rs b/gen/rust/src/packets.rs deleted file mode 100644 index b725db1..0000000 --- a/gen/rust/src/packets.rs +++ /dev/null @@ -1,360 +0,0 @@ -// -// Generated by Nalix.Protocol.Toolkit — RustGenerator -// DO NOT EDIT MANUALLY - -#![allow(dead_code)] -#![allow(unused_variables)] - -/// Common 10-byte packet header. -#[repr(C)] -#[derive(Debug, Clone, Copy, Default, PartialEq)] -pub struct PacketHeader { - pub magic_number: u32, - pub op_code: u16, - pub flags: u8, - pub priority: u8, - pub sequence_id: u16, -} - -impl PacketHeader { - pub const SIZE: usize = 10; - - pub fn serialize(&self, buf: &mut [u8]) -> Result { - if buf.len() < Self::SIZE { return Err(()); } - let mut offset = 0; - buf[offset..offset+4].copy_from_slice(&(self.magic_number as u32).to_le_bytes()); - offset += 4; - buf[offset..offset+2].copy_from_slice(&(self.op_code as u16).to_le_bytes()); - offset += 2; - buf[offset] = self.flags as u8; - offset += 1; - buf[offset] = self.priority as u8; - offset += 1; - buf[offset..offset+2].copy_from_slice(&(self.sequence_id as u16).to_le_bytes()); - offset += 2; - Ok(Self::SIZE) - } - - pub fn deserialize(buf: &[u8]) -> Result { - if buf.len() < Self::SIZE { return Err(()); } - let mut h = Self::default(); - let mut offset = 0; - h.magic_number = u32::from_le_bytes(buf[offset..offset+4].try_into().unwrap()); - offset += 4; - h.op_code = u16::from_le_bytes([buf[offset], buf[offset+1]]); - offset += 2; - h.flags = buf[offset] as u8; - offset += 1; - h.priority = buf[offset] as u8; - offset += 1; - h.sequence_id = u16::from_le_bytes([buf[offset], buf[offset+1]]); - offset += 2; - Ok(h) - } -} - -/// Control — wire size: 29 bytes, magic: 0xD84C915B -#[repr(C)] -#[derive(Debug, Clone, Copy, Default, PartialEq)] -pub struct Control { - pub _header: PacketHeader, - pub reason: u16, - pub type: u8, - pub timestamp: i64, - pub mono_ticks: i64, -} - -impl Control { - pub const FIXED_SIZE: usize = 29; - pub const MAGIC: u32 = 0xD84C915B; - - pub fn serialize(&self, buf: &mut [u8]) -> Result { - if buf.len() < Self::FIXED_SIZE { return Err(()); } - let mut offset = 0; - buf[offset..offset+10].copy_from_slice(&self._header.serialize_to_bytes()); - offset += 10; - buf[offset..offset+2].copy_from_slice(&(self.reason as u16).to_le_bytes()); - offset += 2; - buf[offset] = self.type as u8; - offset += 1; - buf[offset..offset+8].copy_from_slice(&(self.timestamp as u64).to_le_bytes()); - offset += 8; - buf[offset..offset+8].copy_from_slice(&(self.mono_ticks as u64).to_le_bytes()); - offset += 8; - Ok(Self::FIXED_SIZE) - } - - pub fn deserialize(buf: &[u8]) -> Result { - if buf.len() < Self::FIXED_SIZE { return Err(()); } - let mut pkt = Self::default(); - let mut offset = 0; - pkt._header = PacketHeader::deserialize(&buf[offset..offset+10]).map_err(|_| ())?; - offset += 10; - pkt.reason = u16::from_le_bytes([buf[offset], buf[offset+1]]); - offset += 2; - pkt.type = buf[offset] as u8; - offset += 1; - pkt.timestamp = i64::from_le_bytes(buf[offset..offset+8].try_into().unwrap()); - offset += 8; - pkt.mono_ticks = i64::from_le_bytes(buf[offset..offset+8].try_into().unwrap()); - offset += 8; - Ok(pkt) - } - - pub fn equals(&self, other: &Self) -> bool { - let mut a = [0u8; Self::FIXED_SIZE]; - let mut b = [0u8; Self::FIXED_SIZE]; - let _ = self.serialize(&mut a); - let _ = other.serialize(&mut b); - a == b - } -} - -/// Directive — wire size: 25 bytes, magic: 0xDE96BC8B -#[repr(C)] -#[derive(Debug, Clone, Copy, Default, PartialEq)] -pub struct Directive { - pub _header: PacketHeader, - pub type: u8, - pub reason: u16, - pub action: u8, - pub control: u8, - pub arg0: u32, - pub arg1: u32, - pub arg2: u16, -} - -impl Directive { - pub const FIXED_SIZE: usize = 25; - pub const MAGIC: u32 = 0xDE96BC8B; - - pub fn serialize(&self, buf: &mut [u8]) -> Result { - if buf.len() < Self::FIXED_SIZE { return Err(()); } - let mut offset = 0; - buf[offset..offset+10].copy_from_slice(&self._header.serialize_to_bytes()); - offset += 10; - buf[offset] = self.type as u8; - offset += 1; - buf[offset..offset+2].copy_from_slice(&(self.reason as u16).to_le_bytes()); - offset += 2; - buf[offset] = self.action as u8; - offset += 1; - buf[offset] = self.control as u8; - offset += 1; - buf[offset..offset+4].copy_from_slice(&(self.arg0 as u32).to_le_bytes()); - offset += 4; - buf[offset..offset+4].copy_from_slice(&(self.arg1 as u32).to_le_bytes()); - offset += 4; - buf[offset..offset+2].copy_from_slice(&(self.arg2 as u16).to_le_bytes()); - offset += 2; - Ok(Self::FIXED_SIZE) - } - - pub fn deserialize(buf: &[u8]) -> Result { - if buf.len() < Self::FIXED_SIZE { return Err(()); } - let mut pkt = Self::default(); - let mut offset = 0; - pkt._header = PacketHeader::deserialize(&buf[offset..offset+10]).map_err(|_| ())?; - offset += 10; - pkt.type = buf[offset] as u8; - offset += 1; - pkt.reason = u16::from_le_bytes([buf[offset], buf[offset+1]]); - offset += 2; - pkt.action = buf[offset] as u8; - offset += 1; - pkt.control = buf[offset] as u8; - offset += 1; - pkt.arg0 = u32::from_le_bytes(buf[offset..offset+4].try_into().unwrap()); - offset += 4; - pkt.arg1 = u32::from_le_bytes(buf[offset..offset+4].try_into().unwrap()); - offset += 4; - pkt.arg2 = u16::from_le_bytes([buf[offset], buf[offset+1]]); - offset += 2; - Ok(pkt) - } - - pub fn equals(&self, other: &Self) -> bool { - let mut a = [0u8; Self::FIXED_SIZE]; - let mut b = [0u8; Self::FIXED_SIZE]; - let _ = self.serialize(&mut a); - let _ = other.serialize(&mut b); - a == b - } -} - -/// Handshake — wire size: 149 bytes, magic: 0xA8CB1867 -#[repr(C)] -#[derive(Debug, Clone, Copy, Default, PartialEq)] -pub struct Handshake { - pub _header: PacketHeader, - pub stage: u8, - pub reason: u16, - pub session_token: u64, - pub public_key: [u8; 32], - pub nonce: [u8; 32], - pub proof: [u8; 32], - pub transcript_hash: [u8; 32], -} - -impl Handshake { - pub const FIXED_SIZE: usize = 149; - pub const MAGIC: u32 = 0xA8CB1867; - - pub fn serialize(&self, buf: &mut [u8]) -> Result { - if buf.len() < Self::FIXED_SIZE { return Err(()); } - let mut offset = 0; - buf[offset..offset+10].copy_from_slice(&self._header.serialize_to_bytes()); - offset += 10; - buf[offset] = self.stage as u8; - offset += 1; - buf[offset..offset+2].copy_from_slice(&(self.reason as u16).to_le_bytes()); - offset += 2; - buf[offset..offset+8].copy_from_slice(&(self.session_token as u64).to_le_bytes()); - offset += 8; - buf[offset..offset+32].copy_from_slice(&self.public_key); - offset += 32; - buf[offset..offset+32].copy_from_slice(&self.nonce); - offset += 32; - buf[offset..offset+32].copy_from_slice(&self.proof); - offset += 32; - buf[offset..offset+32].copy_from_slice(&self.transcript_hash); - offset += 32; - Ok(Self::FIXED_SIZE) - } - - pub fn deserialize(buf: &[u8]) -> Result { - if buf.len() < Self::FIXED_SIZE { return Err(()); } - let mut pkt = Self::default(); - let mut offset = 0; - pkt._header = PacketHeader::deserialize(&buf[offset..offset+10]).map_err(|_| ())?; - offset += 10; - pkt.stage = buf[offset] as u8; - offset += 1; - pkt.reason = u16::from_le_bytes([buf[offset], buf[offset+1]]); - offset += 2; - pkt.session_token = u64::from_le_bytes(buf[offset..offset+8].try_into().unwrap()); - offset += 8; - pkt.public_key.copy_from_slice(&buf[offset..offset+32]); - offset += 32; - pkt.nonce.copy_from_slice(&buf[offset..offset+32]); - offset += 32; - pkt.proof.copy_from_slice(&buf[offset..offset+32]); - offset += 32; - pkt.transcript_hash.copy_from_slice(&buf[offset..offset+32]); - offset += 32; - Ok(pkt) - } - - pub fn equals(&self, other: &Self) -> bool { - let mut a = [0u8; Self::FIXED_SIZE]; - let mut b = [0u8; Self::FIXED_SIZE]; - let _ = self.serialize(&mut a); - let _ = other.serialize(&mut b); - a == b - } -} - -/// KeyExchange — wire size: 43 bytes, magic: 0x73F416B8 -#[repr(C)] -#[derive(Debug, Clone, Copy, Default, PartialEq)] -pub struct KeyExchange { - pub _header: PacketHeader, - pub stage: u8, - pub public_key: [u8; 32], -} - -impl KeyExchange { - pub const FIXED_SIZE: usize = 43; - pub const MAGIC: u32 = 0x73F416B8; - - pub fn serialize(&self, buf: &mut [u8]) -> Result { - if buf.len() < Self::FIXED_SIZE { return Err(()); } - let mut offset = 0; - buf[offset..offset+10].copy_from_slice(&self._header.serialize_to_bytes()); - offset += 10; - buf[offset] = self.stage as u8; - offset += 1; - buf[offset..offset+32].copy_from_slice(&self.public_key); - offset += 32; - Ok(Self::FIXED_SIZE) - } - - pub fn deserialize(buf: &[u8]) -> Result { - if buf.len() < Self::FIXED_SIZE { return Err(()); } - let mut pkt = Self::default(); - let mut offset = 0; - pkt._header = PacketHeader::deserialize(&buf[offset..offset+10]).map_err(|_| ())?; - offset += 10; - pkt.stage = buf[offset] as u8; - offset += 1; - pkt.public_key.copy_from_slice(&buf[offset..offset+32]); - offset += 32; - Ok(pkt) - } - - pub fn equals(&self, other: &Self) -> bool { - let mut a = [0u8; Self::FIXED_SIZE]; - let mut b = [0u8; Self::FIXED_SIZE]; - let _ = self.serialize(&mut a); - let _ = other.serialize(&mut b); - a == b - } -} - -/// SessionResume — wire size: 53 bytes, magic: 0x99E39CC5 -#[repr(C)] -#[derive(Debug, Clone, Copy, Default, PartialEq)] -pub struct SessionResume { - pub _header: PacketHeader, - pub stage: u8, - pub session_token: u64, - pub reason: u16, - pub proof: [u8; 32], -} - -impl SessionResume { - pub const FIXED_SIZE: usize = 53; - pub const MAGIC: u32 = 0x99E39CC5; - - pub fn serialize(&self, buf: &mut [u8]) -> Result { - if buf.len() < Self::FIXED_SIZE { return Err(()); } - let mut offset = 0; - buf[offset..offset+10].copy_from_slice(&self._header.serialize_to_bytes()); - offset += 10; - buf[offset] = self.stage as u8; - offset += 1; - buf[offset..offset+8].copy_from_slice(&(self.session_token as u64).to_le_bytes()); - offset += 8; - buf[offset..offset+2].copy_from_slice(&(self.reason as u16).to_le_bytes()); - offset += 2; - buf[offset..offset+32].copy_from_slice(&self.proof); - offset += 32; - Ok(Self::FIXED_SIZE) - } - - pub fn deserialize(buf: &[u8]) -> Result { - if buf.len() < Self::FIXED_SIZE { return Err(()); } - let mut pkt = Self::default(); - let mut offset = 0; - pkt._header = PacketHeader::deserialize(&buf[offset..offset+10]).map_err(|_| ())?; - offset += 10; - pkt.stage = buf[offset] as u8; - offset += 1; - pkt.session_token = u64::from_le_bytes(buf[offset..offset+8].try_into().unwrap()); - offset += 8; - pkt.reason = u16::from_le_bytes([buf[offset], buf[offset+1]]); - offset += 2; - pkt.proof.copy_from_slice(&buf[offset..offset+32]); - offset += 32; - Ok(pkt) - } - - pub fn equals(&self, other: &Self) -> bool { - let mut a = [0u8; Self::FIXED_SIZE]; - let mut b = [0u8; Self::FIXED_SIZE]; - let _ = self.serialize(&mut a); - let _ = other.serialize(&mut b); - a == b - } -} - diff --git a/gen/rust/src/packets_tests.rs b/gen/rust/src/packets_tests.rs deleted file mode 100644 index 4078a4f..0000000 --- a/gen/rust/src/packets_tests.rs +++ /dev/null @@ -1,105 +0,0 @@ -// -// Round-trip serialization tests — append to src/packets.rs - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn control_round_trip() { - let mut pkt = Control::default(); - pkt._header = PacketHeader::default(); - pkt.reason = 0x0065u16; - pkt.type = 0x03u8; - pkt.timestamp = 0x0000000000002713i64; - pkt.mono_ticks = 0x0000000000002714i64; - - let mut buf = [0u8; Control::FIXED_SIZE]; - let written = pkt.serialize(&mut buf).expect("serialize failed"); - assert_eq!(written, Control::FIXED_SIZE); - - let parsed = Control::deserialize(&buf).expect("deserialize failed"); - assert_eq!(pkt, parsed); - } - - #[test] - fn directive_round_trip() { - let mut pkt = Directive::default(); - pkt._header = PacketHeader::default(); - pkt.type = 0x02u8; - pkt.reason = 0x0066u16; - pkt.action = 0x04u8; - pkt.control = 0x05u8; - pkt.arg0 = 0x000003EDu32; - pkt.arg1 = 0x000003EEu32; - pkt.arg2 = 0x006Bu16; - - let mut buf = [0u8; Directive::FIXED_SIZE]; - let written = pkt.serialize(&mut buf).expect("serialize failed"); - assert_eq!(written, Directive::FIXED_SIZE); - - let parsed = Directive::deserialize(&buf).expect("deserialize failed"); - assert_eq!(pkt, parsed); - } - - #[test] - fn handshake_round_trip() { - let mut pkt = Handshake::default(); - pkt._header = PacketHeader::default(); - pkt.stage = 0x02u8; - pkt.reason = 0x0066u16; - pkt.session_token = 0x0000000000002713u64; - pkt.public_key = [0x05u8; 32]; - pkt.nonce = [0x06u8; 32]; - pkt.proof = [0x07u8; 32]; - pkt.transcript_hash = [0x08u8; 32]; - - let mut buf = [0u8; Handshake::FIXED_SIZE]; - let written = pkt.serialize(&mut buf).expect("serialize failed"); - assert_eq!(written, Handshake::FIXED_SIZE); - - let parsed = Handshake::deserialize(&buf).expect("deserialize failed"); - assert_eq!(pkt, parsed); - } - - #[test] - fn key_exchange_round_trip() { - let mut pkt = KeyExchange::default(); - pkt._header = PacketHeader::default(); - pkt.stage = 0x02u8; - pkt.public_key = [0x03u8; 32]; - - let mut buf = [0u8; KeyExchange::FIXED_SIZE]; - let written = pkt.serialize(&mut buf).expect("serialize failed"); - assert_eq!(written, KeyExchange::FIXED_SIZE); - - let parsed = KeyExchange::deserialize(&buf).expect("deserialize failed"); - assert_eq!(pkt, parsed); - } - - #[test] - fn session_resume_round_trip() { - let mut pkt = SessionResume::default(); - pkt._header = PacketHeader::default(); - pkt.stage = 0x02u8; - pkt.session_token = 0x0000000000002712u64; - pkt.reason = 0x0067u16; - pkt.proof = [0x05u8; 32]; - - let mut buf = [0u8; SessionResume::FIXED_SIZE]; - let written = pkt.serialize(&mut buf).expect("serialize failed"); - assert_eq!(written, SessionResume::FIXED_SIZE); - - let parsed = SessionResume::deserialize(&buf).expect("deserialize failed"); - assert_eq!(pkt, parsed); - } - - #[test] - fn verify_sizes() { - assert_eq!(Control::FIXED_SIZE, 29); - assert_eq!(Directive::FIXED_SIZE, 25); - assert_eq!(Handshake::FIXED_SIZE, 149); - assert_eq!(KeyExchange::FIXED_SIZE, 43); - assert_eq!(SessionResume::FIXED_SIZE, 53); - } -} diff --git a/src/Nalix.Protocol.Toolkit.Abstractions/Nalix.Protocol.Toolkit.Abstractions.csproj b/src/Nalix.Protocol.Toolkit.Abstractions/Nalix.Protocol.Toolkit.Abstractions.csproj deleted file mode 100644 index b760144..0000000 --- a/src/Nalix.Protocol.Toolkit.Abstractions/Nalix.Protocol.Toolkit.Abstractions.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - - net10.0 - enable - enable - - - diff --git a/src/Nalix.Protocol.Toolkit.Cli/Nalix.Protocol.Toolkit.Cli.csproj b/src/Nalix.Protocol.Toolkit.Cli/Nalix.Protocol.Toolkit.Cli.csproj deleted file mode 100644 index 3aaf862..0000000 --- a/src/Nalix.Protocol.Toolkit.Cli/Nalix.Protocol.Toolkit.Cli.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - net10.0 - enable - enable - Exe - - - - - - - - diff --git a/src/Nalix.Protocol.Toolkit.Roslyn/Nalix.Protocol.Toolkit.Roslyn.csproj b/src/Nalix.Protocol.Toolkit.Roslyn/Nalix.Protocol.Toolkit.Roslyn.csproj deleted file mode 100644 index fbe367f..0000000 --- a/src/Nalix.Protocol.Toolkit.Roslyn/Nalix.Protocol.Toolkit.Roslyn.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - net10.0 - enable - enable - true - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Nalix.Protocol.Toolkit.slnx b/src/Nalix.Protocol.Toolkit.slnx deleted file mode 100644 index 22c5ada..0000000 --- a/src/Nalix.Protocol.Toolkit.slnx +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/Nalix.Protocol.Toolkit.Abstractions/IPacketCodeGenerator.cs b/src/Nalix.Protogen.Abstractions/IPacketCodeGenerator.cs similarity index 95% rename from src/Nalix.Protocol.Toolkit.Abstractions/IPacketCodeGenerator.cs rename to src/Nalix.Protogen.Abstractions/IPacketCodeGenerator.cs index e4d6ad1..f1883b5 100644 --- a/src/Nalix.Protocol.Toolkit.Abstractions/IPacketCodeGenerator.cs +++ b/src/Nalix.Protogen.Abstractions/IPacketCodeGenerator.cs @@ -1,5 +1,5 @@ -namespace Nalix.Protocol.Toolkit.Abstractions; +namespace Nalix.Protogen.Abstractions; public interface IPacketCodeGenerator { diff --git a/src/Nalix.Protogen.Abstractions/Nalix.Protogen.Abstractions.csproj b/src/Nalix.Protogen.Abstractions/Nalix.Protogen.Abstractions.csproj new file mode 100644 index 0000000..23df897 --- /dev/null +++ b/src/Nalix.Protogen.Abstractions/Nalix.Protogen.Abstractions.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/src/Nalix.Protocol.Toolkit.Abstractions/PacketDefinition.cs b/src/Nalix.Protogen.Abstractions/PacketDefinition.cs similarity index 85% rename from src/Nalix.Protocol.Toolkit.Abstractions/PacketDefinition.cs rename to src/Nalix.Protogen.Abstractions/PacketDefinition.cs index 7c87572..58f5042 100644 --- a/src/Nalix.Protocol.Toolkit.Abstractions/PacketDefinition.cs +++ b/src/Nalix.Protogen.Abstractions/PacketDefinition.cs @@ -1,7 +1,7 @@ // Copyright (c) 2026 PPN Corporation. All rights reserved. // Licensed under the Apache License, Version 2.0. -namespace Nalix.Protocol.Toolkit.Abstractions; +namespace Nalix.Protogen.Abstractions; public sealed record PacketDefinition( string Name, diff --git a/src/Nalix.Protocol.Toolkit.Abstractions/PacketField.cs b/src/Nalix.Protogen.Abstractions/PacketField.cs similarity index 86% rename from src/Nalix.Protocol.Toolkit.Abstractions/PacketField.cs rename to src/Nalix.Protogen.Abstractions/PacketField.cs index 745411d..4082139 100644 --- a/src/Nalix.Protocol.Toolkit.Abstractions/PacketField.cs +++ b/src/Nalix.Protogen.Abstractions/PacketField.cs @@ -1,7 +1,7 @@ // Copyright (c) 2026 PPN Corporation. All rights reserved. // Licensed under the Apache License, Version 2.0. -namespace Nalix.Protocol.Toolkit.Abstractions; +namespace Nalix.Protogen.Abstractions; public sealed record PacketField( string Name, diff --git a/src/Nalix.Protogen.Cli/Nalix.Protogen.Cli.csproj b/src/Nalix.Protogen.Cli/Nalix.Protogen.Cli.csproj new file mode 100644 index 0000000..cf443e8 --- /dev/null +++ b/src/Nalix.Protogen.Cli/Nalix.Protogen.Cli.csproj @@ -0,0 +1,16 @@ + + + + net10.0 + enable + enable + Exe + protogen + + + + + + + + diff --git a/src/Nalix.Protocol.Toolkit.Cli/Program.cs b/src/Nalix.Protogen.Cli/Program.cs similarity index 75% rename from src/Nalix.Protocol.Toolkit.Cli/Program.cs rename to src/Nalix.Protogen.Cli/Program.cs index 467a360..9e064b1 100644 --- a/src/Nalix.Protocol.Toolkit.Cli/Program.cs +++ b/src/Nalix.Protogen.Cli/Program.cs @@ -2,19 +2,19 @@ // Licensed under the Apache License, Version 2.0. using Nalix.Abstractions.Exceptions; -using Nalix.Protocol.Toolkit.Abstractions; -using Nalix.Protocol.Toolkit.Roslyn.C; -using Nalix.Protocol.Toolkit.Roslyn.Rust; +using Nalix.Protogen.Abstractions; +using Nalix.Protogen.Roslyn.C; +using Nalix.Protogen.Roslyn.Rust; #pragma warning disable CA1303 // Do not pass literals as localized parameters -namespace Nalix.Protocol.Toolkit.Cli; +namespace Nalix.Protogen.Cli; internal static class Program { private static async Task Main(string[] args) { - Console.WriteLine("Nalix.Protocol.Toolkit — Code Generator"); + Console.WriteLine("Nalix.Protogen — Code Generator"); Console.WriteLine("========================================\n"); if (args.Length == 0 || args.Contains("--help") || args.Contains("-h")) @@ -88,12 +88,7 @@ private static async Task Main(string[] args) } } - private static async Task RunGeneratorAsync( - IPacketCodeGenerator generator, - GenerationContext context, - string outputDir, - string language, - bool verbose) + private static async Task RunGeneratorAsync(IPacketCodeGenerator generator, GenerationContext context, string outputDir, string language, bool verbose) { Console.WriteLine($"Generating {language} bindings..."); @@ -143,20 +138,19 @@ private static async Task RunGeneratorAsync( private static void ShowHelp() { + Console.WriteLine("Description:"); + Console.WriteLine(" A code generator CLI tool to convert C# Nalix packet definitions to unmanaged C and Rust models."); + Console.WriteLine(); Console.WriteLine("Usage:"); - Console.WriteLine(" nalix-protogen --project --c-out [--rust-out ] [options]"); - Console.WriteLine(" nalix-protogen --assembly --c-out [--rust-out ] [options]"); + Console.WriteLine(" protogen [options]"); Console.WriteLine(); Console.WriteLine("Options:"); - Console.WriteLine(" -p, --project Path to .csproj file"); - Console.WriteLine(" -a, --assembly Path to .dll file"); - Console.WriteLine(" --c-out Output directory for C files (packets.h, packets.c, packet_test.c)"); - Console.WriteLine(" --rust-out Output directory for Rust files (packets.rs, packet_tests.rs)"); - Console.WriteLine(" -v, --verbose Show detailed information"); + Console.WriteLine(" -p, --project Path to the C# project (.csproj) containing the packet contracts"); + Console.WriteLine(" -a, --assembly Path to the compiled C# assembly (.dll) containing the packet contracts"); + Console.WriteLine(" --c-out Output directory for the generated C source and headers"); + Console.WriteLine(" --rust-out Output directory for the generated Rust files"); + Console.WriteLine(" -v, --verbose Enable verbose output showing compilation details"); + Console.WriteLine(" -h, --help Display this help documentation"); Console.WriteLine(); - Console.WriteLine("Examples:"); - Console.WriteLine(" nalix-protogen -p ../../Nalix.Codec/Nalix.Codec.csproj --c-out ./c --rust-out ./rs"); - Console.WriteLine(" nalix-protogen -a ../../Nalix.Codec.dll --c-out ./generated/c"); - Console.WriteLine(" nalix-protogen -a ../../Nalix.Codec.dll --rust-out ./generated/rs"); } } diff --git a/src/Nalix.Protocol.Toolkit.Cli/generated/include/nalix_packets.h b/src/Nalix.Protogen.Cli/generated/include/nalix_packets.h similarity index 99% rename from src/Nalix.Protocol.Toolkit.Cli/generated/include/nalix_packets.h rename to src/Nalix.Protogen.Cli/generated/include/nalix_packets.h index 7ead5a3..2eddeb1 100644 --- a/src/Nalix.Protocol.Toolkit.Cli/generated/include/nalix_packets.h +++ b/src/Nalix.Protogen.Cli/generated/include/nalix_packets.h @@ -1,5 +1,5 @@ // -// Generated by Nalix.Protocol.Toolkit +// Generated by Nalix.Protogen // DO NOT EDIT MANUALLY #pragma once diff --git a/src/Nalix.Protocol.Toolkit.Cli/generated/src/nalix_packets.c b/src/Nalix.Protogen.Cli/generated/src/nalix_packets.c similarity index 100% rename from src/Nalix.Protocol.Toolkit.Cli/generated/src/nalix_packets.c rename to src/Nalix.Protogen.Cli/generated/src/nalix_packets.c diff --git a/src/Nalix.Protocol.Toolkit.Roslyn/C/CHeaderGenerator.cs b/src/Nalix.Protogen.Roslyn/C/CHeaderGenerator.cs similarity index 97% rename from src/Nalix.Protocol.Toolkit.Roslyn/C/CHeaderGenerator.cs rename to src/Nalix.Protogen.Roslyn/C/CHeaderGenerator.cs index 19ca8fe..724b781 100644 --- a/src/Nalix.Protocol.Toolkit.Roslyn/C/CHeaderGenerator.cs +++ b/src/Nalix.Protogen.Roslyn/C/CHeaderGenerator.cs @@ -2,9 +2,9 @@ // Licensed under the Apache License, Version 2.0. using System.Text; -using Nalix.Protocol.Toolkit.Abstractions; +using Nalix.Protogen.Abstractions; -namespace Nalix.Protocol.Toolkit.Roslyn.C; +namespace Nalix.Protogen.Roslyn.C; /// /// Generates native C header and source files for Nalix protocol frames. @@ -35,7 +35,7 @@ private static string GenerateHeader( StringBuilder sb = new(32768); _ = sb.AppendLine("// "); - _ = sb.AppendLine("// Generated by Nalix.Protocol.Toolkit — CHeaderGenerator"); + _ = sb.AppendLine("// Generated by Nalix.Protogen — CHeaderGenerator"); _ = sb.AppendLine("// DO NOT EDIT MANUALLY"); _ = sb.AppendLine(); _ = sb.AppendLine("#pragma once"); @@ -138,8 +138,7 @@ private static string GenerateSource(IReadOnlyList nonHeaderPa // Variable-size serialize AppendInvariant(sb, $"size_t {lower}_serialize(const {name}* pkt, uint8_t* buf, size_t buf_size)"); _ = sb.AppendLine("{"); - AppendInvariant(sb, $" if (!pkt || !buf) return 0;"); - AppendInvariant(sb, $" size_t min_size = {fixedSize};"); + AppendInvariant(sb, $" if (!pkt || !buf || buf_size < {fixedSize}) return 0;"); _ = sb.AppendLine(" uint8_t* ptr = buf;"); _ = sb.AppendLine(); foreach (PacketField field in p.Fields) @@ -252,6 +251,7 @@ private static void WriteSerializeField(StringBuilder sb, PacketField field) // Length-prefixed UTF-8: [int32_t length][bytes] _ = sb.AppendLine(" {"); AppendInvariant(sb, $" int32_t slen = pkt->{name}.length;"); + AppendInvariant(sb, $" if ((size_t)(ptr - buf) + 4 + (slen > 0 ? slen : 0) > buf_size) return 0;"); _ = sb.AppendLine(" memcpy(ptr, &slen, 4); ptr += 4;"); AppendInvariant(sb, $" if (slen > 0 && pkt->{name}.data) {{ memcpy(ptr, pkt->{name}.data, slen); ptr += slen; }}"); _ = sb.AppendLine(" }"); @@ -455,6 +455,11 @@ private static string GetCTestValue(PacketField field, int index) return $"{{{{ 0x{index + 1:X2} }}}}"; } + if (field.CType == "NalixString") + { + return "{ 0, NULL }"; + } + return size switch { 1 => FormattableString.Invariant($"0x{index + 1:X2}"), diff --git a/src/Nalix.Protocol.Toolkit.Roslyn/GeneratorBase.cs b/src/Nalix.Protogen.Roslyn/GeneratorBase.cs similarity index 96% rename from src/Nalix.Protocol.Toolkit.Roslyn/GeneratorBase.cs rename to src/Nalix.Protogen.Roslyn/GeneratorBase.cs index b06d51c..dbdedc4 100644 --- a/src/Nalix.Protocol.Toolkit.Roslyn/GeneratorBase.cs +++ b/src/Nalix.Protogen.Roslyn/GeneratorBase.cs @@ -1,9 +1,9 @@ // Copyright (c) 2026 PPN Corporation. All rights reserved. // Licensed under the Apache License, Version 2.0. -using Nalix.Protocol.Toolkit.Abstractions; +using Nalix.Protogen.Abstractions; -namespace Nalix.Protocol.Toolkit.Roslyn; +namespace Nalix.Protogen.Roslyn; /// /// Base class for all packet code generators. @@ -119,6 +119,11 @@ protected static string ToRustType(PacketField field) return "[u8; 32]"; } + if (cs.Contains("ReadOnlyMemory", StringComparison.Ordinal)) + { + return "Vec"; + } + return cs switch { "byte" or "System.Byte" => "u8", diff --git a/src/Nalix.Protocol.Toolkit.Roslyn/KnownAttributes.cs b/src/Nalix.Protogen.Roslyn/KnownAttributes.cs similarity index 95% rename from src/Nalix.Protocol.Toolkit.Roslyn/KnownAttributes.cs rename to src/Nalix.Protogen.Roslyn/KnownAttributes.cs index 8578c88..46e96d3 100644 --- a/src/Nalix.Protocol.Toolkit.Roslyn/KnownAttributes.cs +++ b/src/Nalix.Protogen.Roslyn/KnownAttributes.cs @@ -4,7 +4,7 @@ using Nalix.Abstractions.Networking.Packets; using Nalix.Abstractions.Serialization; -namespace Nalix.Protocol.Toolkit.Roslyn; +namespace Nalix.Protogen.Roslyn; /// /// Centralized, type-safe attribute metadata names. diff --git a/src/Nalix.Protocol.Toolkit.Roslyn/KnownTypes.cs b/src/Nalix.Protogen.Roslyn/KnownTypes.cs similarity index 90% rename from src/Nalix.Protocol.Toolkit.Roslyn/KnownTypes.cs rename to src/Nalix.Protogen.Roslyn/KnownTypes.cs index b52de16..7610d66 100644 --- a/src/Nalix.Protocol.Toolkit.Roslyn/KnownTypes.cs +++ b/src/Nalix.Protogen.Roslyn/KnownTypes.cs @@ -1,10 +1,10 @@ -// Copyright (c) 2026 PPN Corporation. All rights reserved. +// Copyright (c) 2026 PPN Corporation. All rights reserved. // Licensed under the Apache License, Version 2.0. using Nalix.Abstractions.Primitives; using Nalix.Abstractions.Serialization; -namespace Nalix.Protocol.Toolkit.Roslyn; +namespace Nalix.Protogen.Roslyn; /// /// Centralized type-safe metadata cho analyzer. diff --git a/src/Nalix.Protogen.Roslyn/Nalix.Protogen.Roslyn.csproj b/src/Nalix.Protogen.Roslyn/Nalix.Protogen.Roslyn.csproj new file mode 100644 index 0000000..6cf91b9 --- /dev/null +++ b/src/Nalix.Protogen.Roslyn/Nalix.Protogen.Roslyn.csproj @@ -0,0 +1,21 @@ + + + + net10.0 + enable + enable + true + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Nalix.Protocol.Toolkit.Roslyn/PacketAnalyzer.cs b/src/Nalix.Protogen.Roslyn/PacketAnalyzer.cs similarity index 89% rename from src/Nalix.Protocol.Toolkit.Roslyn/PacketAnalyzer.cs rename to src/Nalix.Protogen.Roslyn/PacketAnalyzer.cs index a6cc4fd..e6ba758 100644 --- a/src/Nalix.Protocol.Toolkit.Roslyn/PacketAnalyzer.cs +++ b/src/Nalix.Protogen.Roslyn/PacketAnalyzer.cs @@ -5,9 +5,9 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.MSBuild; -using Nalix.Protocol.Toolkit.Abstractions; +using Nalix.Protogen.Abstractions; -namespace Nalix.Protocol.Toolkit.Roslyn; +namespace Nalix.Protogen.Roslyn; /// /// Production-grade Roslyn analyzer. @@ -155,6 +155,11 @@ public static List AnalyzeFromAssembly(string assemblyPath) )); } + if (HasPacketAttribute(type) && !fields.Any(f => f.CType == "PacketHeader" || f.CSharpType.Contains("PacketHeader", StringComparison.Ordinal))) + { + fields.Insert(0, new PacketField("Header", "Nalix.Abstractions.Primitives.PacketHeader", "PacketHeader", -1, true, 10, true, "Implicit Packet Header")); + } + uint magic = ComputeMagic(type.FullName ?? type.Name); return new PacketDefinition( @@ -162,7 +167,7 @@ public static List AnalyzeFromAssembly(string assemblyPath) FullName: type.FullName ?? type.Name, Magic: magic, Fields: fields, - IsFixedSize: true, + IsFixedSize: !fields.Any(f => f.Size == 0 || f.CType == "NalixString"), FixedSize: 0 ); } @@ -205,6 +210,11 @@ private static bool InheritsPacketBaseOfSelf(INamedTypeSymbol symbol) } } + if (InheritsPacketBaseOfSelf(symbol) && !fields.Any(f => f.CType == "PacketHeader" || f.CSharpType.Contains("PacketHeader", StringComparison.Ordinal))) + { + fields.Insert(0, new PacketField("Header", "Nalix.Abstractions.Primitives.PacketHeader", "PacketHeader", -1, true, 10, true, "Implicit Packet Header")); + } + uint magic = ComputeMagic(symbol.ToDisplayString()); return new PacketDefinition( @@ -301,6 +311,12 @@ private static string ToCType(ITypeSymbol type) return "Bytes32"; } + // ReadOnlyMemory / ReadOnlyMemory + if (type.ToDisplayString().Contains("ReadOnlyMemory", StringComparison.Ordinal)) + { + return "NalixString"; + } + // Enum - Lấy underlying type chính xác if (type.TypeKind == TypeKind.Enum && type is INamedTypeSymbol namedEnum) { @@ -345,6 +361,11 @@ private static int GetTypeSize(ITypeSymbol type) return 32; } + if (type.ToDisplayString().Contains("ReadOnlyMemory", StringComparison.Ordinal)) + { + return 0; // variable-size + } + if (type.SpecialType == SpecialType.System_String) { return 0; // variable-size @@ -373,7 +394,7 @@ private static int GetTypeSize(ITypeSymbol type) SpecialType.System_Int64 or SpecialType.System_UInt64 => 8, SpecialType.System_Single => 4, SpecialType.System_Double => 8, - _ => 4 + _ => 1 }; } @@ -415,11 +436,26 @@ private static string ToCTypeFromReflection(Type t) "Double" => "double", "Boolean" => "uint8_t", "String" => "char*", // sẽ xử lý riêng ở serialize - _ => "uint8_t" + _ => t.Name.StartsWith("ReadOnlyMemory", StringComparison.Ordinal) ? "NalixString" : "uint8_t" }; } - private static int GetTypeSizeFromReflection(Type t) => t.FullName == "Nalix.Abstractions.Primitives.Bytes32" ? 32 : t.IsEnum ? 4 : System.Runtime.InteropServices.Marshal.SizeOf(t); + private static int GetTypeSizeFromReflection(Type t) + { + if (t.FullName == "Nalix.Abstractions.Primitives.Bytes32") return 32; + if (t.Name.StartsWith("ReadOnlyMemory", StringComparison.Ordinal)) return 0; + if (t.IsEnum) return 4; + try + { + return System.Runtime.InteropServices.Marshal.SizeOf(t); + } +#pragma warning disable CA1031 + catch +#pragma warning restore CA1031 + { + return 1; + } + } private static uint ComputeMagic(string fullName) { diff --git a/src/Nalix.Protocol.Toolkit.Roslyn/Rust/RustGenerator.cs b/src/Nalix.Protogen.Roslyn/Rust/RustGenerator.cs similarity index 99% rename from src/Nalix.Protocol.Toolkit.Roslyn/Rust/RustGenerator.cs rename to src/Nalix.Protogen.Roslyn/Rust/RustGenerator.cs index 2a7d1c0..e57633d 100644 --- a/src/Nalix.Protocol.Toolkit.Roslyn/Rust/RustGenerator.cs +++ b/src/Nalix.Protogen.Roslyn/Rust/RustGenerator.cs @@ -2,9 +2,9 @@ // Licensed under the Apache License, Version 2.0. using System.Text; -using Nalix.Protocol.Toolkit.Abstractions; +using Nalix.Protogen.Abstractions; -namespace Nalix.Protocol.Toolkit.Roslyn.Rust; +namespace Nalix.Protogen.Roslyn.Rust; /// /// Generates native Rust structs and serialize/deserialize implementations @@ -34,7 +34,7 @@ private static string GenerateMain( StringBuilder sb = new(65536); _ = sb.AppendLine("// "); - _ = sb.AppendLine("// Generated by Nalix.Protocol.Toolkit — RustGenerator"); + _ = sb.AppendLine("// Generated by Nalix.Protogen — RustGenerator"); _ = sb.AppendLine("// DO NOT EDIT MANUALLY"); _ = sb.AppendLine(); _ = sb.AppendLine("#![allow(dead_code)]"); diff --git a/src/Nalix.Protogen.slnx b/src/Nalix.Protogen.slnx new file mode 100644 index 0000000..154e42a --- /dev/null +++ b/src/Nalix.Protogen.slnx @@ -0,0 +1,5 @@ + + + + +