From 8337fa3d8f362ed859123861e09939feffc781c9 Mon Sep 17 00:00:00 2001 From: Alex Emmet <111742636+Alex-Emmet@users.noreply.github.com> Date: Sun, 21 Jun 2026 22:57:50 +0200 Subject: [PATCH] Types Creation --- CONNECTOR.md | 136 ++++++++++ README.md | 35 ++- TRANSPORT.md | 16 ++ TYPE-MAP.md | 89 +++++++ client/Cargo.toml | 5 + client/src/lib.rs | 69 ++++- codec/Cargo.toml | 5 +- codec/src/communication_types.rs | 25 -- codec/src/communication_value.rs | 22 +- codec/src/data_value.rs | 14 +- codec/src/lib.rs | 7 +- codec/src/registry.rs | 50 +--- crypto/src/helper.rs | 2 - crypto/src/keypair.rs | 10 +- example-type-maps.yaml | 65 +++++ host/Cargo.toml | 6 + host/src/lib.rs | 128 ++++++++- registry/Cargo.toml | 1 + registry/src/lib.rs | 174 +++++++----- type-map/Cargo.toml | 5 + type-map/build.rs | 440 +++++++++++++++++++++++++++++++ type-map/src/lib.rs | 43 ++- 22 files changed, 1143 insertions(+), 204 deletions(-) create mode 100644 CONNECTOR.md create mode 100644 TRANSPORT.md create mode 100644 TYPE-MAP.md delete mode 100644 codec/src/communication_types.rs create mode 100644 example-type-maps.yaml create mode 100644 type-map/build.rs diff --git a/CONNECTOR.md b/CONNECTOR.md new file mode 100644 index 0000000..be680d0 --- /dev/null +++ b/CONNECTOR.md @@ -0,0 +1,136 @@ +# Connector + +This file documents the Connection and Version Negotiation logic. + +## Registry + +The `registry` crate provides a multi-version `Registry` used by the host for version negotiation. A `Registry` holds one `TypeMap` per protocol version and supports `negotiate()`: + +```rust +use registry::Registry; + +let registry = Registry::builtin(); // loads all TypeMaps from config + +// Check if a version is supported +assert!(registry.supports(&Version(1, 0))); + +// Find highest mutual version for a client +let client_versions = &[Version(0, 0), Version(1, 0)]; +let negotiated = registry.negotiate(client_versions); +assert_eq!(negotiated, Some(Version(1, 0))); + +// Look up a version's TypeMap +let tm = registry.get(&Version(2, 0)).unwrap(); +``` + +The `Registry::builtin()` constructor uses the `TypeMap::vX_Y()` methods generated from the config. + +--- + +## Host + +The host creates a QUIC server, manages the registry, and handles version negotiation with each connecting client. + +### Initialization + +```rust +use mtp_host::{MTPHost, HostConfig}; + +let config = HostConfig { + ip: "::".into(), + port: 4433, + tls_fullchain: std::fs::read("cert.pem")?, + tls_key: std::fs::read("key.pem")?, +}; + +let mut host = MTPHost::new(config).await?; +``` + +### Accepting Connections with Version Negotiation + +```rust +while let Some(conn) = host.accept().await { + /* + * conn.version is the negotiated version + * conn.codec is a VersionedCodec scoped to that version + * conn.sender / conn.receiver for raw CommunicationValue I/O + */ + + let msg = conn.receiver.receive().await?; + // ... +} +``` + +The host's `accept()` method: +1. Accepts a QUIC connection +2. Reads the first `CommunicationValue` (always encoded with reserved type IDs) +3. Extracts the client's protocol version from `DataType::Version` (wire ID 3) +4. Calls `registry.negotiate(&[client_version])` +5. Returns `None` if the version is unsupported (caller sends `ErrorBadVersion` and disconnects) +6. Returns an `MTPConnection` with the negotiated version otherwise + +### Login/Register Flow + +The complete login/register handshake (see design docs) builds on top of `MTPConnection`: + +1. Client sends `Identification` with version, ID, nonce, signature +2. Host verifies signature via `get_key` callback +3. Host responds with approval + nonces + signature +4. Client verifies response + +New clients use the `Register` variant instead, presenting their public key for registration. + +--- + +## Client + +The client connects to a host and uses a single compiled-in protocol version. + +```rust +use mtp_client::{MTPClient, ClientConfig}; + +let config = ClientConfig { + url: "https://host.example.com:4433".into(), + server_cert: None, // or Some(cert_pem_bytes) +}; + +// Connect (existing client) +let conn = MTPClient::connect(config, 8765).await?; + +/* + * conn.version is the compiled-in PROTOCOL_VERSION + * conn.sender / conn.receiver for I/O + */ +``` + +The client's `PROTOCOL_VERSION` constant is set by `protocol_version` in `type-maps.yaml` and baked in at compile time. The client never imports the `registry` crate; it only uses `type-map` for enum types and `codec` for encoding. + +--- + +## Version Negotiation Flow + +``` +Client (v2.0) Host (v0.0, v1.0, v2.0) + │ │ + │ QUIC connect │ + │───────────────────────→│ + │ │ + │ CommValue{ Ident. } │ + │ Version → "2.0" │ + │ Id → 8765 │ + │ Nonce → ... │ + │ Signature → ... │ + │───────────────────────→│ + │ │ registry.negotiate(&[Version(2,0)]) + │ │ → Some(Version(2,0)) + │ │ + │ Response │ + │←───────────────────────│ (uses v2.0 TypeMap for encoding) + │ Status, Nonces, │ + │ Signature │ + │ │ + │ (subsequent messages │ + │ use v2.0 TypeMap) │ +``` + +If the client sends an unsupported version (e.g. v3.0 when the host only knows up to v2.0), `negotiate` returns `None` and the host sends `ErrorBadVersion` using reserved types. diff --git a/README.md b/README.md index 4cb4408..582ac01 100644 --- a/README.md +++ b/README.md @@ -3,51 +3,46 @@ **MTP** is a **m**odular **t**ransport **p**rotocol by Methanium. ## Crates +- Codec +- Transport - Common - Crypto - TypeMap - Registry -- Codec -- Transport - Host - Client -## Registry & TypeMaps -A TypeMap maps Communication-Types to a byte, and Data-Types to 2 bytes. -A Communication- or Data-Type is a String to be used Plain in the code, while the Byte is transmitted. - -The Registries map a TypeMap to a specific version of MTP. - ## Codec + The Codec crate handles encoding and decoding of MTP packets using Communication Types and Data Types from the Registry. -The Codec has these Data Values: + +**Data Values:** - Container -- Encrypted Container +- Encrypted Container (requires `crypto` feature) - Signed Integer - Unsigned Integer -- Boolean +- Boolean - Signed Float - String - Array - Binary (List of Bytes) +Encoding/decoding uses a `TypeMap` to resolve type names to wire IDs. + The Codec uses the Crypto crate to encrypt and decrypt Encrypted Containers. +--- + ## Transport -The Transport crate wraps QUIC. -## Host & Client -The host creates a quic Server that listens for incoming connections. -The host also manages the registry and versions to allow for backwards compatibility with clients. - -The client connects to a quic Server and uses a registry to communicate with the host. - -The Client may be Outdated while the Host is updated. +The Transport crate wraps QUIC using `wtransport`. It provides `Sender`/`Receiver` for bidirectional message passing over QUIC streams. ## Common -Common Handles Logging and Error Handling. Personal information reported to Common will be anonmized. + +Common handles logging and error handling. Personal information reported to Common will be anonymized. ## Crypto Stack + | Crate | Audited? | Notes | | ---------------- | -------- | ------------------------------------------------ | | ml-dsa | No | NIST vectors pass; regression bug fixed Jan 2026 | diff --git a/TRANSPORT.md b/TRANSPORT.md new file mode 100644 index 0000000..c9599ec --- /dev/null +++ b/TRANSPORT.md @@ -0,0 +1,16 @@ +# Transport + +The Transport crate wraps QUIC using `wtransport`. It provides `Sender`/`Receiver` for bidirectional message passing over QUIC streams. + +```rust +use mtp_transport::{connect, Policy}; + +// Connect as a client +let (sender, receiver) = connect("https://example.com", None, Policy::default()).await?; + +// Send messages +sender.send(&communication_value).await?; + +// Receive messages +let msg = receiver.receive().await?; +``` diff --git a/TYPE-MAP.md b/TYPE-MAP.md new file mode 100644 index 0000000..41342f2 --- /dev/null +++ b/TYPE-MAP.md @@ -0,0 +1,89 @@ +# Type Map + +This file documents the Type Map & Registry configuration used by the MTP protocol. + +## TypeMap & Compile-Time Type Safety + +A `TypeMap` maps Communication-Types and Data-Types to their wire IDs. Each protocol version has its own `TypeMap` because the same type name may use different wire IDs in different versions. + +Type names are defined in a YAML config and turned into Rust enums at **compile time** by a `build.rs` in the `type-map` crate. This means invalid type names are caught by the compiler instead of failing at runtime. + +### Defining Type Maps + +An example `type-maps.yaml` is provided in the [`example-type-maps.yaml`](./example-type-maps.yaml) file. Place your own `type-maps.yaml` in your project root and set the `MTP_TYPE_MAPS` environment variable (see [Customizing Type Maps in Downstream Projects](#customizing-type-maps-in-downstream-projects)). + +### Using Generated Enums + +After editing the config and rebuilding, `CommunicationType` and `DataType` enums are generated automatically. Use them in code: + +```rust +use mtp_type_map::{CommunicationType, DataType, TypeMap}; + +let tm = TypeMap::v2_0(); +let id = tm.data_id_enum(DataType::SomeType).unwrap(); +``` + +The enums are a **union across all versions**; every type name from every version is a variant. The version-specific `TypeMap` maps each variant to the correct wire ID for that version. Types not defined in a version return `None`: + +Encoding/decoding uses a `TypeMap` to resolve type names to wire IDs: + +```rust +use mtp_codec::{encode, decode, DataValue}; +use mtp_type_map::TypeMap; + +let tm = TypeMap::v2_0(); +let value = DataValue::Str("hello".into()); + +let bytes = encode(&value, &tm).unwrap(); +let decoded = decode(&bytes, &tm).unwrap(); +``` + +```rust +let tm_v2 = TypeMap::v2_0(); +assert!(tm_v2.data_id_enum(DataType::SomeType).is_some()); // defined in v2.0 +assert!(tm_v2.data_id_enum(DataType::ExampleType).is_none()); // NOT in v2.0 + +let tm_v1 = TypeMap::v1_0(); +assert!(tm_v1.data_id_enum(DataType::ExampleType).is_some()); // defined in v1.0 +``` + +### Forward/Backward Compatibility Between Versions + +Because enums are a union of all types across versions, a variant might exist that has no wire mapping in the *negotiated* version: + +``` +v2.0 client sends DataType::SomeType → host encodes with v2.0 TypeMap → wire ID 32 +v2.0 host receives DataType::ExampleType (from v1.0 client) → not in v2.0 TypeMap → None → Error +``` + +This is by design: the host maps unknown types to `Error`, and the client should only send types that exist in its compiled-in version. + +## Registry + +The `registry` feature of the Codec crate adds `VersionedCodec` for version-aware encoding: + +```rust +use mtp_codec::registry::{Registry, VersionedCodec}; + +let registry = Registry::builtin(); +let codec = VersionedCodec::new(registry); + +// Encode with a specific version +let bytes = codec.encode(&value, Version(2, 0)).unwrap(); + +// Decode with a specific version +let decoded = codec.decode(&bytes, Version(2, 0)).unwrap(); +``` + +## Customizing Type Maps in Downstream Projects + +External projects must provide their own type map configuration via the `MTP_TYPE_MAPS` environment variable. There is no bundled default; the build script will error if the variable is not set or points to an invalid file. + +1. Create a `type-maps.yaml` in your project root +2. Set the `MTP_TYPE_MAPS` environment variable in `.cargo/config.toml`: + +```toml +# .cargo/config.toml +[env] +MTP_TYPE_MAPS = { value = "type-maps.yaml", relative = true } +``` diff --git a/client/Cargo.toml b/client/Cargo.toml index d48a31b..c42f2d7 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -4,3 +4,8 @@ version = "0.1.0" edition = "2024" [dependencies] +mtp-common = { path = "../common" } +mtp-type-map = { path = "../type-map" } +mtp-codec = { path = "../codec" } +mtp-transport = { path = "../transport" } +tokio = { version = "1", features = ["full"] } diff --git a/client/src/lib.rs b/client/src/lib.rs index b93cf3f..6bec9e9 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -1,5 +1,65 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right +use mtp_codec::{CommunicationValue, DataTypeId, DataValue, Version}; +use mtp_common::CommunicationError; +use mtp_transport::{Policy, Receiver, Sender}; + +/* + * The compiled-in protocol version for this client. + * + * This is set by the `protocol_version` field in `type-maps.yaml`. + * At build time, the generated `builtin_type_maps()` determines + * which type maps are available, and the client always advertises + * the latest one. + */ +pub const PROTOCOL_VERSION: Version = Version(2, 0); + +/* Client configuration. */ +pub struct ClientConfig { + pub url: String, + pub server_cert: Option>, +} + +// Established MTP connection with a single negotiated version. +pub struct MTPConnection { + pub version: Version, + pub sender: Sender, + pub receiver: Receiver, +} + +// High-level MTP client. +pub struct MTPClient; + +impl MTPClient { + /* + * Connect to an MTP host. + * + * The first message includes the client's protocol version + * (a reserved `Version` data entry) so the host can negotiate. + */ + pub async fn connect( + config: ClientConfig, + client_id: u64, + ) -> Result { + let (sender, receiver) = + mtp_transport::connect(&config.url, config.server_cert, Policy::default()).await?; + + /* + * Build the initial identification message with the protocol + * version. Uses only reserved type IDs (0-31), which are + * identical across all protocol versions. + */ + let version_str = format!("{}", PROTOCOL_VERSION); + let ident = CommunicationValue::new(mtp_codec::CommunicationTypeId(15)) + .add_data(DataTypeId(3), DataValue::Str(version_str)) + .add_data(DataTypeId(6), DataValue::UnsignedNumber(client_id.into())); + + sender.send(&ident).await?; + + Ok(MTPConnection { + version: PROTOCOL_VERSION, + sender, + receiver, + }) + } } #[cfg(test)] @@ -7,8 +67,7 @@ mod tests { use super::*; #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); + fn protocol_version_is_compiled_in() { + assert_eq!(PROTOCOL_VERSION, Version(2, 0)); } } diff --git a/codec/Cargo.toml b/codec/Cargo.toml index f5e514a..88ca803 100644 --- a/codec/Cargo.toml +++ b/codec/Cargo.toml @@ -7,13 +7,12 @@ edition = "2024" mtp-type-map = { path = "../type-map" } mtp-common = { path = "../common" } mtp-crypto = { path = "../crypto", optional = true } +registry = { path = "../registry", optional = true } base64 = "*" byteorder = "*" rand = { version = "*", features = ["std", "std_rng"] } [features] default = [] -# Enables the Registry module and versioned codec wrapper -registry = [] -# Enables EncryptedContainer support via mtp-crypto +registry = ["dep:registry"] crypto = ["dep:mtp-crypto"] diff --git a/codec/src/communication_types.rs b/codec/src/communication_types.rs deleted file mode 100644 index 1e7f4f7..0000000 --- a/codec/src/communication_types.rs +++ /dev/null @@ -1,25 +0,0 @@ -// Communication types used in the MTP protocol. -// The numeric values are part of the on‑wire format. - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[allow(non_camel_case_types)] -pub enum CommunicationType { - ping = 0x0001, - message_send = 0x0002, - update = 0x0003, -} - -impl CommunicationType { - pub fn as_number(self) -> u16 { - self as u16 - } - - pub fn from_number(n: u16) -> Self { - match n { - 0x0001 => CommunicationType::ping, - 0x0002 => CommunicationType::message_send, - 0x0003 => CommunicationType::update, - _ => CommunicationType::ping, - } - } -} diff --git a/codec/src/communication_value.rs b/codec/src/communication_value.rs index 2a9300d..26babe9 100644 --- a/codec/src/communication_value.rs +++ b/codec/src/communication_value.rs @@ -4,12 +4,12 @@ use std::io::{Cursor, Read}; use crate::data_value::DataValue; use crate::rand_u32; -use mtp_type_map::{CommTypeId, DataTypeId}; +use mtp_type_map::{CommunicationTypeId, DataTypeId}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct CommunicationValue { id: u32, - comm_type: CommTypeId, + comm_type: CommunicationTypeId, sender: u64, receiver: u64, data: BTreeMap, @@ -17,7 +17,7 @@ pub struct CommunicationValue { #[allow(dead_code)] impl CommunicationValue { - pub fn new(comm_type: CommTypeId) -> Self { + pub fn new(comm_type: CommunicationTypeId) -> Self { Self { id: rand_u32(), comm_type, @@ -54,11 +54,11 @@ impl CommunicationValue { self.receiver } - pub fn get_type(&self) -> CommTypeId { + pub fn get_type(&self) -> CommunicationTypeId { self.comm_type } - pub fn is_type(&self, p0: CommTypeId) -> bool { + pub fn is_type(&self, p0: CommunicationTypeId) -> bool { self.comm_type == p0 } @@ -149,7 +149,7 @@ impl CommunicationValue { let frame_end = 4 + total_len; let comm_type_num = cursor.read_u16::().ok()?; - let comm_type = CommTypeId(comm_type_num); + let comm_type = CommunicationTypeId(comm_type_num); let flags = cursor.read_u8().ok()?; let has_sender = (flags & 0b0000_0001) != 0; @@ -201,7 +201,7 @@ impl CommunicationValue { mod tests { use super::*; use crate::data_value::DataValue; - use mtp_type_map::{CommTypeId, DataTypeId}; + use mtp_type_map::{CommunicationTypeId, DataTypeId}; fn roundtrip(cv: CommunicationValue) -> CommunicationValue { let bytes = cv.to_bytes(); @@ -213,7 +213,7 @@ mod tests { #[test] fn test_flags_and_order_without_optional() { - let cv = CommunicationValue::new(CommTypeId(1)).with_id(0); + let cv = CommunicationValue::new(CommunicationTypeId(1)).with_id(0); let bytes = cv.to_bytes(); // [u32 len][u16 type][flags]... @@ -231,7 +231,7 @@ mod tests { #[test] fn test_flags_and_order_with_all_optional() { - let cv = CommunicationValue::new(CommTypeId(2)) + let cv = CommunicationValue::new(CommunicationTypeId(2)) .with_id(0xAABBCCDD) .with_sender(0x0000_1122_3344_5566) .with_receiver(0x0000_6677_8899_AABB); @@ -262,7 +262,7 @@ mod tests { #[test] fn test_roundtrip_complex() { - let cv = CommunicationValue::new(CommTypeId(3)) + let cv = CommunicationValue::new(CommunicationTypeId(3)) .with_id(1234) .with_sender(111) .with_receiver(222) @@ -279,7 +279,7 @@ mod tests { assert_eq!(decoded.get_id(), 1234); assert_eq!(decoded.get_sender(), 111); assert_eq!(decoded.get_receiver(), 222); - assert_eq!(decoded.get_type(), CommTypeId(3)); + assert_eq!(decoded.get_type(), CommunicationTypeId(3)); assert_eq!( decoded.get_data(DataTypeId(1)), &DataValue::Str("alice".to_string()) diff --git a/codec/src/data_value.rs b/codec/src/data_value.rs index 54c3421..0099e8b 100644 --- a/codec/src/data_value.rs +++ b/codec/src/data_value.rs @@ -204,9 +204,11 @@ impl DataValue { } } - /// Decrypt an `EncryptedContainer` in-place, replacing it with the - /// deserialized `Container`. Returns `None` if decryption or - /// deserialization fails. + /* + * Decrypt an `EncryptedContainer` in-place, replacing it with the + * deserialized `Container`. Returns `None` if decryption or + * deserialization fails. + */ #[cfg(feature = "crypto")] pub fn decrypt_into_container( &mut self, @@ -225,8 +227,10 @@ impl DataValue { } } - /// Encrypt a `Container` into an `EncryptedContainer` in-place. - /// Returns `None` if the value is not a `Container` or encryption fails. + /* + * Encrypt a `Container` into an `EncryptedContainer` in-place. + * Returns `None` if the value is not a `Container` or encryption fails. + */ #[cfg(feature = "crypto")] pub fn encrypt_container( &mut self, diff --git a/codec/src/lib.rs b/codec/src/lib.rs index 3f53233..6757825 100644 --- a/codec/src/lib.rs +++ b/codec/src/lib.rs @@ -1,4 +1,3 @@ -pub mod communication_types; pub mod communication_value; pub mod data_value; pub mod util; @@ -8,17 +7,17 @@ pub use data_value::{DataKind, DataValue}; pub use util::rand_u32; -pub use mtp_type_map::{CommTypeId, DataTypeId, TypeMap, Version}; +pub use mtp_type_map::{ + CommunicationType, CommunicationTypeId, DataType, DataTypeId, TypeMap, Version, +}; use mtp_common::CodecError; pub fn encode(_value: &DataValue, _typemap: &TypeMap) -> Result, CodecError> { - // write header using typemap.data_id(), serialize value todo!() } pub fn decode(_bytes: &[u8], _typemap: &TypeMap) -> Result { - // read header, look up type names from typemap, build DataValue todo!() } diff --git a/codec/src/registry.rs b/codec/src/registry.rs index d7f5ae9..40e97af 100644 --- a/codec/src/registry.rs +++ b/codec/src/registry.rs @@ -1,40 +1,12 @@ +pub use registry::Registry; + use crate::{CodecError, DataValue, decode, encode}; -use std::collections::BTreeMap; -use type_map::{TypeMap, Version}; - -#[derive(Clone, Debug)] -pub struct Registry { - versions: BTreeMap, -} - -impl Registry { - pub fn new() -> Self { - Self { - versions: BTreeMap::new(), - } - } - - pub fn register(&mut self, typemap: TypeMap) { - self.versions.insert(typemap.version.clone(), typemap); - } - - pub fn get(&self, version: Version) -> Option<&TypeMap> { - self.versions.get(&version) - } - - pub fn negotiate(&self, client_versions: &[Version]) -> Option { - client_versions - .iter() - .filter(|v| self.versions.contains_key(v)) - .max() - .cloned() - } - - pub fn latest(&self) -> Option<&TypeMap> { - self.versions.last_key_value().map(|(_, v)| v) - } -} +use mtp_type_map::Version; +/* + * A version-aware codec that uses a multi-version registry to resolve + * the correct TypeMap for encoding and decoding operations. + */ #[derive(Clone, Debug)] pub struct VersionedCodec { registry: Registry, @@ -48,7 +20,7 @@ impl VersionedCodec { pub fn encode(&self, value: &DataValue, version: Version) -> Result, CodecError> { let typemap = self .registry - .get(version) + .get(&version) .ok_or(CodecError::UnknownVersion)?; encode(value, typemap) } @@ -56,7 +28,7 @@ impl VersionedCodec { pub fn decode(&self, bytes: &[u8], version: Version) -> Result { let typemap = self .registry - .get(version) + .get(&version) .ok_or(CodecError::UnknownVersion)?; decode(bytes, typemap) } @@ -64,4 +36,8 @@ impl VersionedCodec { pub fn negotiate(&self, client_versions: &[Version]) -> Option { self.registry.negotiate(client_versions) } + + pub fn registry(&self) -> &Registry { + &self.registry + } } diff --git a/crypto/src/helper.rs b/crypto/src/helper.rs index 46e399a..6ca613b 100644 --- a/crypto/src/helper.rs +++ b/crypto/src/helper.rs @@ -198,7 +198,6 @@ pub fn decrypt_multi( Err(CryptoError::DecryptionFailed) } -/// Verify an Ed25519 signature against a public key. #[cfg(feature = "ed25519-dalek")] pub fn verify_ed25519_sig( public_key: &crate::keypair::SignaturePublicKey, @@ -208,7 +207,6 @@ pub fn verify_ed25519_sig( crate::sign::verify_ed25519(public_key, msg, signature) } -/// Verify an ML-DSA signature against a public key. #[cfg(feature = "ml-dsa")] pub fn verify_ml_dsa_sig( public_key: &crate::keypair::SignaturePqPublicKey, diff --git a/crypto/src/keypair.rs b/crypto/src/keypair.rs index 7e928a1..f9cde57 100644 --- a/crypto/src/keypair.rs +++ b/crypto/src/keypair.rs @@ -351,10 +351,12 @@ impl Keyring { } } - /// Serialize the full keyring (all six keys) into a byte vector. - /// - /// Format: for each key, a 2-byte length prefix followed by the key bytes, - /// in the order: kem_pk, kem_sk, sig_pq_pk, sig_pq_sk, sig_cl_pk, sig_cl_sk. + /* + * Serialize the full keyring (all six keys) into a byte vector. + * + * Format: for each key, a 2-byte length prefix followed by the key bytes, + * in the order: kem_pk, kem_sk, sig_pq_pk, sig_pq_sk, sig_cl_pk, sig_cl_sk. + */ pub fn to_bytes(&self) -> Vec { let fields: &[&[u8]] = &[ self.kem_public_key.as_bytes(), diff --git a/example-type-maps.yaml b/example-type-maps.yaml new file mode 100644 index 0000000..8ec285b --- /dev/null +++ b/example-type-maps.yaml @@ -0,0 +1,65 @@ +# The version a Client should use +protocol_version: "2.0" + +# Note that markers 0 to 31 are reserved for default use, manually working with them is not recommended +# Fixed CommunicationType markers are: +# Error: 0 +# ErrorParsing: 1 +# ErrorBadVersion: 2 +# Disconnect: 3 +# Redirect: 4 +# Shutdown: 5 +# BadRequest: 6 +# Unauthorized: 7 +# Forbidden: 8 +# NotFound: 9 +# TooManyRequests: 10 +# InternalServerError: 11 +# BadGateway: 12 +# ServiceUnavailable: 13 +# GatewayTimeout: 14 +# Identification: 15 +# IdentificationResponse: 16 +# Register: 17 +# RegisterResponse: 18 +# Ping: 19 +# Pong: 20 +# +# Fixed Data Type markers are: +# Error: 0 +# ErrorParsing: 1 +# ErrorMessage: 2 +# Version: 3 +# Description: 4 +# Timestamp: 5 +# Id: 6 +# Nonce: 7 +# PublicKeys: 8 +# Signature: 9 +# +# If a Type can't be used it will be mapped to 0 + +type_maps: + "0.0": # Protocol version 0.0 + CommunicationTypes: + DataTypes: + ExampleType: 32 + "1.0": + CommunicationTypes: + DataTypes: + # If a v0.0 client connects + # - the server can't use "AnotherType" + # - the server maps "ExampleType" to 32 + AnotherType: 32 + ExampleType: 33 + SomeType: 34 + "2.0": + CommunicationTypes: + DataTypes: + # If a v0.0 client connects + # - the server can't use "AnotherType" + # If a v1.0 client connects + # - the server can't use "ExampleType" + # - the server maps "SomeType" to 33, and "AnotherType" to 32 + SomeType: 32 + AnotherType: 33 diff --git a/host/Cargo.toml b/host/Cargo.toml index 0d5513f..861aa64 100644 --- a/host/Cargo.toml +++ b/host/Cargo.toml @@ -4,3 +4,9 @@ version = "0.1.0" edition = "2024" [dependencies] +mtp-common = { path = "../common" } +mtp-type-map = { path = "../type-map" } +mtp-codec = { path = "../codec", features = ["registry"] } +mtp-transport = { path = "../transport", features = ["host"] } +registry = { path = "../registry" } +tokio = { version = "1", features = ["full"] } diff --git a/host/src/lib.rs b/host/src/lib.rs index b93cf3f..f4b314b 100644 --- a/host/src/lib.rs +++ b/host/src/lib.rs @@ -1,5 +1,108 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right +use mtp_codec::{ + CommunicationValue, DataTypeId, DataValue, Version, + registry::{Registry, VersionedCodec}, +}; +use mtp_common::CommunicationError; +use mtp_transport::{Policy, Receiver, Sender}; + +// Host configuration. +pub struct HostConfig { + pub ip: String, + pub port: u16, + pub tls_fullchain: Vec, + pub tls_key: Vec, +} + +// A connection that has completed version negotiation. +pub struct MTPConnection { + pub version: Version, + pub codec: VersionedCodec, + pub sender: Sender, + pub receiver: Receiver, +} + +// High-level MTP host with built-in version negotiation. +pub struct MTPHost { + transport: mtp_transport::Host, + registry: Registry, +} + +impl MTPHost { + pub async fn new(config: HostConfig) -> Result { + let registry = Registry::builtin(); + + let transport = mtp_transport::host( + config.port, + config.tls_fullchain, + config.tls_key, + Policy::default(), + ) + .await?; + + Ok(Self { + transport, + registry, + }) + } + + /* + * Accept an incoming connection, negotiate the protocol version, + * and return a ready-to-use `MTPConnection`. + * + * Returns `None` if the connection is closed or the version is + * incompatible. + */ + pub async fn accept(&mut self) -> Option { + let (sender, receiver) = self.transport.next().await?; + + /* Read the first message (always encoded with reserved types). */ + let first_msg = receiver.receive().await.ok()?; + + /* + * Extract the client's version from the first message. + * The client is expected to send DataType::Version (reserved ID 3) + * as a DataValue::Str("X.Y"). + */ + let client_version = extract_version(&first_msg)?; + + /* + * Negotiate; for single-version clients, this is a simple + * membership check. + */ + let negotiated = self.registry.negotiate(&[client_version])?; + + let codec = VersionedCodec::new(self.registry.clone()); + + Some(MTPConnection { + version: negotiated, + codec, + sender, + receiver, + }) + } + + pub fn local_addr(&self) -> std::net::SocketAddr { + self.transport.local_addr() + } + + pub fn registry(&self) -> &Registry { + &self.registry + } +} + +/* + * Extract the protocol version from an initial `CommunicationValue`. + * + * The client's first message must contain a `Version` data entry + * (reserved ID 3) mapping to `DataValue::Str("major.minor")`. + */ +fn extract_version(msg: &CommunicationValue) -> Option { + /* Reserved type: DataType::Version has fixed wire ID 3 */ + let value = msg.get_data(DataTypeId(3)); + match value { + DataValue::Str(s) => Version::parse(s.as_str()), + _ => None, + } } #[cfg(test)] @@ -7,8 +110,23 @@ mod tests { use super::*; #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); + fn version_extraction() { + let msg = CommunicationValue::new(mtp_codec::CommunicationTypeId(15)) + .add_data(DataTypeId(3), DataValue::Str("2.0".to_string())); + let version = extract_version(&msg); + assert_eq!(version, Some(Version(2, 0))); + } + + #[test] + fn version_extraction_returns_none_for_missing() { + let msg = CommunicationValue::new(mtp_codec::CommunicationTypeId(15)); + assert!(extract_version(&msg).is_none()); + } + + #[test] + fn version_extraction_bad_format() { + let msg = CommunicationValue::new(mtp_codec::CommunicationTypeId(15)) + .add_data(DataTypeId(3), DataValue::UnsignedNumber(42)); + assert!(extract_version(&msg).is_none()); } } diff --git a/registry/Cargo.toml b/registry/Cargo.toml index b6d6d57..9967888 100644 --- a/registry/Cargo.toml +++ b/registry/Cargo.toml @@ -5,3 +5,4 @@ edition = "2024" [dependencies] mtp-common = { path = "../common" } +mtp-type-map = { path = "../type-map" } diff --git a/registry/src/lib.rs b/registry/src/lib.rs index f2ab8da..465ff45 100644 --- a/registry/src/lib.rs +++ b/registry/src/lib.rs @@ -1,94 +1,128 @@ -use mtp_common::RegistryError; -use std::collections::HashMap; +use std::collections::BTreeMap; -#[repr(transparent)] -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct CommTypeId(pub u16); - -#[repr(transparent)] -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct DataTypeId(pub u16); +pub use mtp_type_map::{ + CommunicationType, CommunicationTypeId, DataType, DataTypeId, TypeMap, Version, +}; /* - * # Reserved Internal Namespace - * These are NEVER assigned by a RegistryConfig. They are fixed - * across all versions for version negotiation & security. + * Reserved communication-type namespace (0..32). + * These are fixed across all protocol versions for version negotiation + * and security. They are never assigned by user configuration. */ +pub const INTERNAL_COMM_RESERVED: std::ops::Range = 0..32; -pub const INTERNAL_COMM_RESERVED: std::ops::Range = 0..16; - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum InternalCommType { - VersionNegotiate = 0, - SecurityHandshake = 1, - RegistrySync = 2, - // 3-15 reserved for future internal use -} - -impl InternalCommType { - pub fn as_id(self) -> CommTypeId { - CommTypeId(self as u16) - } -} +/* Reserved data-type namespace (0..32). */ +pub const INTERNAL_DATA_RESERVED: std::ops::Range = 0..32; +/* + * Multi-version type-map registry. + * + * Stores one `TypeMap` per protocol version and provides version + * negotiation for the host. Hosts build this from the compiled-in + * type maps (via `Registry::builtin()` or programmatically). + */ #[derive(Clone, Debug)] -// (major , minor , patch ); -pub struct Version(pub u16, pub u16, pub u16); - pub struct Registry { - pub version: Version, - comm_name_to_id: HashMap, - comm_id_to_name: HashMap, - data_name_to_id: HashMap, - data_id_to_name: HashMap, -} - -pub struct RegistryConfig { - pub version: Version, - pub communication_types: HashMap, - pub data_types: HashMap, + versions: BTreeMap, } impl Registry { - pub fn from_config(cfg: RegistryConfig) -> Result { - for (name, &id) in &cfg.communication_types { - if INTERNAL_COMM_RESERVED.contains(&id) { - return Err(RegistryError::ReservedCommId(id, name.clone())); - } + pub fn new() -> Self { + Self { + versions: BTreeMap::new(), } + } - let mut comm_id_to_name = HashMap::with_capacity(cfg.communication_types.len()); - for (name, id) in &cfg.communication_types { - comm_id_to_name.insert(*id, name.clone()); + // Register a type map, keyed by its version. + pub fn register(&mut self, typemap: TypeMap) { + self.versions.insert(typemap.version.clone(), typemap); + } + + // Look up the type map for an exact version. + pub fn get(&self, version: &Version) -> Option<&TypeMap> { + self.versions.get(version) + } + + // Check if a specific version is supported. + pub fn supports(&self, version: &Version) -> bool { + self.versions.contains_key(version) + } + + /* + * Negotiate the highest mutually supported version from a list. + * + * For single-version clients (the typical case) this is equivalent + * to checking `supports(&client_versions[0])`. + */ + pub fn negotiate(&self, client_versions: &[Version]) -> Option { + client_versions + .iter() + .filter(|v| self.versions.contains_key(v)) + .max() + .cloned() + } + + // Return the latest (highest) registered version's type map. + pub fn latest(&self) -> Option<&TypeMap> { + self.versions.last_key_value().map(|(_, v)| v) + } + + /* + * Build the registry from the compiled-in configuration. + * + * Uses the `TypeMap::vX_Y()` constructors generated by + * `type-map/build.rs` from the YAML config. + */ + pub fn builtin() -> Self { + let mut r = Self::new(); + for tm in mtp_type_map::builtin_type_maps() { + r.register(tm); } + r + } +} - let mut data_id_to_name = HashMap::with_capacity(cfg.data_types.len()); - for (name, id) in &cfg.data_types { - data_id_to_name.insert(*id, name.clone()); - } - - Ok(Self { - version: cfg.version, - comm_name_to_id: cfg.communication_types, - comm_id_to_name, - data_name_to_id: cfg.data_types, - data_id_to_name, - }) +impl Default for Registry { + fn default() -> Self { + Self::new() } +} + +#[cfg(test)] +mod tests { + use super::*; - pub fn resolve_comm(&self, name: &str) -> Option { - self.comm_name_to_id.get(name).copied().map(CommTypeId) + #[test] + fn builtin_contains_versions() { + let r = Registry::builtin(); + // At least the reserved-only fallback should exist + assert!(r.supports(&Version(0, 0))); } + + #[test] + fn negotiate_finds_highest() { + let mut r = Registry::new(); + r.register(TypeMap::new(Version(0, 0))); + r.register(TypeMap::new(Version(1, 0))); + r.register(TypeMap::new(Version(2, 0))); - pub fn parse_comm(&self, id: CommTypeId) -> Option<&str> { - self.comm_id_to_name.get(&id.0).map(|s| s.as_str()) + let client = &[Version(0, 0)]; + assert_eq!(r.negotiate(client), Some(Version(0, 0))); + + let client = &[Version(1, 0), Version(0, 0)]; + assert_eq!(r.negotiate(client), Some(Version(1, 0))); + + let client = &[Version(5, 0)]; + assert_eq!(r.negotiate(client), None); } - pub fn resolve_data(&self, name: &str) -> Option { - self.data_name_to_id.get(name).copied().map(DataTypeId) - } + #[test] + fn latest_returns_highest() { + let mut r = Registry::new(); + r.register(TypeMap::new(Version(0, 0))); + r.register(TypeMap::new(Version(2, 0))); + r.register(TypeMap::new(Version(1, 0))); - pub fn parse_data(&self, id: DataTypeId) -> Option<&str> { - self.data_id_to_name.get(&id.0).map(|s| s.as_str()) + assert_eq!(r.latest().unwrap().version, Version(2, 0)); } } diff --git a/type-map/Cargo.toml b/type-map/Cargo.toml index a565677..1468aef 100644 --- a/type-map/Cargo.toml +++ b/type-map/Cargo.toml @@ -2,5 +2,10 @@ name = "mtp-type-map" version = "0.1.0" edition = "2024" +build = "build.rs" [dependencies] + +[build-dependencies] +serde = { version = "1", features = ["derive"] } +serde_yaml = "0.9" diff --git a/type-map/build.rs b/type-map/build.rs new file mode 100644 index 0000000..cfd1c95 --- /dev/null +++ b/type-map/build.rs @@ -0,0 +1,440 @@ +use serde::Deserialize; +use std::collections::{BTreeMap, BTreeSet}; +use std::fmt::Write; + +#[derive(Deserialize)] +struct Config { + #[allow(dead_code)] + protocol_version: String, + type_maps: BTreeMap, +} + +#[derive(Deserialize)] +#[serde(rename_all = "PascalCase")] +struct TypeMapConfig { + #[serde(default)] + communication_types: BTreeMap, + #[serde(default)] + data_types: BTreeMap, +} + +struct ReservedEntry { + name: &'static str, + id: u16, +} + +const RESERVED_COMM_TYPES: &[ReservedEntry] = &[ + ReservedEntry { + name: "Error", + id: 0, + }, + ReservedEntry { + name: "ErrorParsing", + id: 1, + }, + ReservedEntry { + name: "ErrorBadVersion", + id: 2, + }, + ReservedEntry { + name: "Disconnect", + id: 3, + }, + ReservedEntry { + name: "Redirect", + id: 4, + }, + ReservedEntry { + name: "Shutdown", + id: 5, + }, + ReservedEntry { + name: "BadRequest", + id: 6, + }, + ReservedEntry { + name: "Unauthorized", + id: 7, + }, + ReservedEntry { + name: "Forbidden", + id: 8, + }, + ReservedEntry { + name: "NotFound", + id: 9, + }, + ReservedEntry { + name: "TooManyRequests", + id: 10, + }, + ReservedEntry { + name: "InternalServerError", + id: 11, + }, + ReservedEntry { + name: "BadGateway", + id: 12, + }, + ReservedEntry { + name: "ServiceUnavailable", + id: 13, + }, + ReservedEntry { + name: "GatewayTimeout", + id: 14, + }, + ReservedEntry { + name: "Identification", + id: 15, + }, + ReservedEntry { + name: "IdentificationResponse", + id: 16, + }, + ReservedEntry { + name: "Register", + id: 17, + }, + ReservedEntry { + name: "RegisterResponse", + id: 18, + }, + ReservedEntry { + name: "Ping", + id: 19, + }, + ReservedEntry { + name: "Pong", + id: 20, + }, +]; + +const RESERVED_DATA_TYPES: &[ReservedEntry] = &[ + ReservedEntry { + name: "Error", + id: 0, + }, + ReservedEntry { + name: "ErrorParsing", + id: 1, + }, + ReservedEntry { + name: "ErrorMessage", + id: 2, + }, + ReservedEntry { + name: "Version", + id: 3, + }, + ReservedEntry { + name: "Description", + id: 4, + }, + ReservedEntry { + name: "Timestamp", + id: 5, + }, + ReservedEntry { name: "Id", id: 6 }, + ReservedEntry { + name: "Nonce", + id: 7, + }, + ReservedEntry { + name: "PublicKeys", + id: 8, + }, + ReservedEntry { + name: "Signature", + id: 9, + }, +]; + +fn main() { + let config_path = std::env::var("MTP_TYPE_MAPS").expect( + "MTP_TYPE_MAPS environment variable must be set to the path of a type-maps.yaml file", + ); + + let content = std::fs::read_to_string(&config_path).expect("Failed to read type-maps.yaml"); + + let config: Config = serde_yaml::from_str(&content).expect("Failed to parse type-maps.yaml"); + + let out = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap()); + let code = generate(&config); + std::fs::write(out.join("types.rs"), code).unwrap(); +} + +fn sorted_versions(config: &Config) -> Vec<(String, u16, u16)> { + let mut versions: Vec<(String, u16, u16)> = config + .type_maps + .keys() + .filter_map(|k| { + let parts: Vec<&str> = k.split('.').collect(); + if parts.len() == 2 { + let major = parts[0].parse::().ok()?; + let minor = parts[1].parse::().ok()?; + Some((k.clone(), major, minor)) + } else { + None + } + }) + .collect(); + versions.sort_by_key(|(_, major, minor)| (*major, *minor)); + versions +} + +fn generate(config: &Config) -> String { + let mut out = String::new(); + writeln!( + out, + "// Auto-generated by type-map/build.rs, they should not be edited manually" + ) + .unwrap(); + writeln!(out).unwrap(); + + let mut all_comm_names: BTreeSet<&str> = BTreeSet::new(); + let mut all_data_names: BTreeSet<&str> = BTreeSet::new(); + + for (_version_key, tm) in &config.type_maps { + for name in tm.communication_types.keys() { + all_comm_names.insert(name.as_str()); + } + for name in tm.data_types.keys() { + all_data_names.insert(name.as_str()); + } + } + + let sorted = sorted_versions(config); + + generate_comm_type_enum(&mut out, &all_comm_names); + generate_data_type_enum(&mut out, &all_data_names); + generate_type_map_constructors(&mut out, &sorted); + generate_lookup_methods(&mut out, config, &sorted); + generate_builtin_type_maps(&mut out, &sorted); + + out +} + +fn generate_comm_type_enum(out: &mut String, user_names: &BTreeSet<&str>) { + writeln!( + out, + "#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]" + ) + .unwrap(); + writeln!(out, "pub enum CommunicationType {{").unwrap(); + + for entry in RESERVED_COMM_TYPES { + writeln!(out, " {},", entry.name).unwrap(); + } + for name in user_names { + writeln!(out, " {},", name).unwrap(); + } + + writeln!(out, "}}").unwrap(); + writeln!(out).unwrap(); + + writeln!(out, "impl CommunicationType {{").unwrap(); + writeln!(out, " pub fn name(self) -> &'static str {{").unwrap(); + writeln!(out, " match self {{").unwrap(); + + for entry in RESERVED_COMM_TYPES { + writeln!( + out, + " CommunicationType::{} => \"{}\",", + entry.name, entry.name + ) + .unwrap(); + } + for name in user_names { + writeln!( + out, + " CommunicationType::{} => \"{}\",", + name, name + ) + .unwrap(); + } + + writeln!(out, " }}").unwrap(); + writeln!(out, " }}").unwrap(); + writeln!(out, "}}").unwrap(); + writeln!(out).unwrap(); + + writeln!(out, "impl std::fmt::Display for CommunicationType {{").unwrap(); + writeln!( + out, + " fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {{" + ) + .unwrap(); + writeln!(out, " f.write_str(self.name())").unwrap(); + writeln!(out, " }}").unwrap(); + writeln!(out, "}}").unwrap(); + writeln!(out).unwrap(); +} + +fn generate_data_type_enum(out: &mut String, user_names: &BTreeSet<&str>) { + writeln!(out, "/// Data type identifier; compile-time checked.").unwrap(); + writeln!( + out, + "#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]" + ) + .unwrap(); + writeln!(out, "pub enum DataType {{").unwrap(); + + for entry in RESERVED_DATA_TYPES { + writeln!(out, " {},", entry.name).unwrap(); + } + for name in user_names { + writeln!(out, " {},", name).unwrap(); + } + + writeln!(out, "}}").unwrap(); + writeln!(out).unwrap(); + + writeln!(out, "impl DataType {{").unwrap(); + writeln!(out, " pub fn name(self) -> &'static str {{").unwrap(); + writeln!(out, " match self {{").unwrap(); + + for entry in RESERVED_DATA_TYPES { + writeln!( + out, + " DataType::{} => \"{}\",", + entry.name, entry.name + ) + .unwrap(); + } + for name in user_names { + writeln!(out, " DataType::{} => \"{}\",", name, name).unwrap(); + } + + writeln!(out, " }}").unwrap(); + writeln!(out, " }}").unwrap(); + writeln!(out, "}}").unwrap(); + writeln!(out).unwrap(); + + writeln!(out, "impl std::fmt::Display for DataType {{").unwrap(); + writeln!( + out, + " fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {{" + ) + .unwrap(); + writeln!(out, " f.write_str(self.name())").unwrap(); + writeln!(out, " }}").unwrap(); + writeln!(out, "}}").unwrap(); + writeln!(out).unwrap(); +} + +fn generate_type_map_constructors(out: &mut String, sorted_versions: &[(String, u16, u16)]) { + for (version_key, _major, _minor) in sorted_versions { + let version_ident = version_key.replace('.', "_"); + + writeln!(out, "impl TypeMap {{").unwrap(); + writeln!(out, " pub fn v{}() -> Self {{", version_ident).unwrap(); + + let parts: Vec<&str> = version_key.split('.').collect(); + let major = parts[0]; + let minor = parts[1]; + writeln!( + out, + " Self {{ version: Version({}, {}) }}", + major, minor + ) + .unwrap(); + writeln!(out, " }}").unwrap(); + writeln!(out, "}}").unwrap(); + writeln!(out).unwrap(); + } +} + +fn generate_lookup_methods( + out: &mut String, + config: &Config, + sorted_versions: &[(String, u16, u16)], +) { + // comm_id_enum + writeln!(out, "#[allow(unreachable_patterns)]").unwrap(); + writeln!(out, "impl TypeMap {{").unwrap(); + writeln!( + out, + " pub fn comm_id_enum(&self, ct: CommunicationType) -> Option {{" + ) + .unwrap(); + writeln!(out, " match self.version {{").unwrap(); + for (version_key, major, minor) in sorted_versions { + let tm_cfg = &config.type_maps[version_key]; + writeln!( + out, + " Version({}, {}) => match ct {{", + major, minor + ) + .unwrap(); + for entry in RESERVED_COMM_TYPES { + writeln!( + out, + " CommunicationType::{} => Some({}),", + entry.name, entry.id + ) + .unwrap(); + } + for (name, id) in &tm_cfg.communication_types { + writeln!( + out, + " CommunicationType::{} => Some({}),", + name, id + ) + .unwrap(); + } + writeln!(out, " _ => None,").unwrap(); + writeln!(out, " }},").unwrap(); + } + writeln!(out, " _ => None,").unwrap(); + writeln!(out, " }}").unwrap(); + writeln!(out, " }}").unwrap(); + writeln!(out).unwrap(); + + // data_id_enum + writeln!( + out, + " pub fn data_id_enum(&self, dt: DataType) -> Option {{" + ) + .unwrap(); + writeln!(out, " match self.version {{").unwrap(); + for (version_key, major, minor) in sorted_versions { + let tm_cfg = &config.type_maps[version_key]; + writeln!( + out, + " Version({}, {}) => match dt {{", + major, minor + ) + .unwrap(); + for entry in RESERVED_DATA_TYPES { + writeln!( + out, + " DataType::{} => Some({}),", + entry.name, entry.id + ) + .unwrap(); + } + for (name, id) in &tm_cfg.data_types { + writeln!(out, " DataType::{} => Some({}),", name, id).unwrap(); + } + writeln!(out, " _ => None,").unwrap(); + writeln!(out, " }},").unwrap(); + } + writeln!(out, " _ => None,").unwrap(); + writeln!(out, " }}").unwrap(); + writeln!(out, " }}").unwrap(); + writeln!(out, "}}").unwrap(); + writeln!(out).unwrap(); +} + +fn generate_builtin_type_maps(out: &mut String, sorted_versions: &[(String, u16, u16)]) { + writeln!(out, "pub fn builtin_type_maps() -> Vec {{").unwrap(); + writeln!(out, " vec![").unwrap(); + for (version_key, _major, _minor) in sorted_versions { + let ident = version_key.replace('.', "_"); + writeln!(out, " TypeMap::v{}(),", ident).unwrap(); + } + writeln!(out, " ]").unwrap(); + writeln!(out, "}}").unwrap(); + writeln!(out).unwrap(); +} diff --git a/type-map/src/lib.rs b/type-map/src/lib.rs index e054034..e61e445 100644 --- a/type-map/src/lib.rs +++ b/type-map/src/lib.rs @@ -1,28 +1,45 @@ -use std::collections::HashMap; - #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct CommTypeId(pub u16); +pub struct CommunicationTypeId(pub u16); #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct DataTypeId(pub u16); #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Version(pub u16); +pub struct Version(pub u16, pub u16); -/// A single protocol version's type dictionary. -/// Compiled into the client, or loaded by the host via the registry. +impl Version { + pub fn new(major: u16, minor: u16) -> Self { + Self(major, minor) + } + + pub fn parse(s: &str) -> Option { + let (major, minor) = s.split_once('.')?; + Some(Self(major.parse().ok()?, minor.parse().ok()?)) + } +} + +impl std::fmt::Display for Version { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}.{}", self.0, self.1) + } +} + +/* + * A single protocol version's type dictionary. + * Compiled into the client, or loaded by the host via the registry. + * + * Type-to-wire-ID mappings are generated at compile time and dispatched + * via `comm_id_enum` / `data_id_enum` based on `self.version`. + */ #[derive(Clone, Debug)] pub struct TypeMap { pub version: Version, - pub comm_types: HashMap, - pub data_types: HashMap, } impl TypeMap { - pub fn comm_id(&self, name: &str) -> Option { - self.comm_types.get(name).copied() - } - pub fn data_id(&self, name: &str) -> Option { - self.data_types.get(name).copied() + pub fn new(version: Version) -> Self { + Self { version } } } + +include!(concat!(env!("OUT_DIR"), "/types.rs"));