Types Creation

This commit is contained in:
Alex Emmet 2026-06-21 22:57:50 +02:00
commit 8337fa3d8f
22 changed files with 1141 additions and 202 deletions

136
CONNECTOR.md Normal file
View file

@ -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.

View file

@ -3,26 +3,22 @@
**MTP** is a **m**odular **t**ransport **p**rotocol by Methanium. **MTP** is a **m**odular **t**ransport **p**rotocol by Methanium.
## Crates ## Crates
- Codec
- Transport
- Common - Common
- Crypto - Crypto
- TypeMap - TypeMap
- Registry - Registry
- Codec
- Transport
- Host - Host
- Client - 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 ## Codec
The Codec crate handles encoding and decoding of MTP packets using Communication Types and Data Types from the Registry. 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 - Container
- Encrypted Container - Encrypted Container (requires `crypto` feature)
- Signed Integer - Signed Integer
- Unsigned Integer - Unsigned Integer
- Boolean - Boolean
@ -31,23 +27,22 @@ The Codec has these Data Values:
- Array - Array
- Binary (List of Bytes) - 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. The Codec uses the Crypto crate to encrypt and decrypt Encrypted Containers.
---
## Transport ## Transport
The Transport crate wraps QUIC.
## Host & Client The Transport crate wraps QUIC using `wtransport`. It provides `Sender`/`Receiver` for bidirectional message passing over QUIC streams.
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.
## Common ## 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 ## Crypto Stack
| Crate | Audited? | Notes | | Crate | Audited? | Notes |
| ---------------- | -------- | ------------------------------------------------ | | ---------------- | -------- | ------------------------------------------------ |
| ml-dsa | No | NIST vectors pass; regression bug fixed Jan 2026 | | ml-dsa | No | NIST vectors pass; regression bug fixed Jan 2026 |

16
TRANSPORT.md Normal file
View file

@ -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?;
```

89
TYPE-MAP.md Normal file
View file

@ -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 }
```

View file

@ -4,3 +4,8 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
mtp-common = { path = "../common" }
mtp-type-map = { path = "../type-map" }
mtp-codec = { path = "../codec" }
mtp-transport = { path = "../transport" }
tokio = { version = "1", features = ["full"] }

View file

@ -1,5 +1,65 @@
pub fn add(left: u64, right: u64) -> u64 { use mtp_codec::{CommunicationValue, DataTypeId, DataValue, Version};
left + right 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<Vec<u8>>,
}
// 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<MTPConnection, CommunicationError> {
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)] #[cfg(test)]
@ -7,8 +67,7 @@ mod tests {
use super::*; use super::*;
#[test] #[test]
fn it_works() { fn protocol_version_is_compiled_in() {
let result = add(2, 2); assert_eq!(PROTOCOL_VERSION, Version(2, 0));
assert_eq!(result, 4);
} }
} }

View file

@ -7,13 +7,12 @@ edition = "2024"
mtp-type-map = { path = "../type-map" } mtp-type-map = { path = "../type-map" }
mtp-common = { path = "../common" } mtp-common = { path = "../common" }
mtp-crypto = { path = "../crypto", optional = true } mtp-crypto = { path = "../crypto", optional = true }
registry = { path = "../registry", optional = true }
base64 = "*" base64 = "*"
byteorder = "*" byteorder = "*"
rand = { version = "*", features = ["std", "std_rng"] } rand = { version = "*", features = ["std", "std_rng"] }
[features] [features]
default = [] default = []
# Enables the Registry module and versioned codec wrapper registry = ["dep:registry"]
registry = []
# Enables EncryptedContainer support via mtp-crypto
crypto = ["dep:mtp-crypto"] crypto = ["dep:mtp-crypto"]

View file

@ -1,25 +0,0 @@
// Communication types used in the MTP protocol.
// The numeric values are part of the onwire 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,
}
}
}

View file

@ -4,12 +4,12 @@ use std::io::{Cursor, Read};
use crate::data_value::DataValue; use crate::data_value::DataValue;
use crate::rand_u32; use crate::rand_u32;
use mtp_type_map::{CommTypeId, DataTypeId}; use mtp_type_map::{CommunicationTypeId, DataTypeId};
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommunicationValue { pub struct CommunicationValue {
id: u32, id: u32,
comm_type: CommTypeId, comm_type: CommunicationTypeId,
sender: u64, sender: u64,
receiver: u64, receiver: u64,
data: BTreeMap<DataTypeId, DataValue>, data: BTreeMap<DataTypeId, DataValue>,
@ -17,7 +17,7 @@ pub struct CommunicationValue {
#[allow(dead_code)] #[allow(dead_code)]
impl CommunicationValue { impl CommunicationValue {
pub fn new(comm_type: CommTypeId) -> Self { pub fn new(comm_type: CommunicationTypeId) -> Self {
Self { Self {
id: rand_u32(), id: rand_u32(),
comm_type, comm_type,
@ -54,11 +54,11 @@ impl CommunicationValue {
self.receiver self.receiver
} }
pub fn get_type(&self) -> CommTypeId { pub fn get_type(&self) -> CommunicationTypeId {
self.comm_type self.comm_type
} }
pub fn is_type(&self, p0: CommTypeId) -> bool { pub fn is_type(&self, p0: CommunicationTypeId) -> bool {
self.comm_type == p0 self.comm_type == p0
} }
@ -149,7 +149,7 @@ impl CommunicationValue {
let frame_end = 4 + total_len; let frame_end = 4 + total_len;
let comm_type_num = cursor.read_u16::<BigEndian>().ok()?; let comm_type_num = cursor.read_u16::<BigEndian>().ok()?;
let comm_type = CommTypeId(comm_type_num); let comm_type = CommunicationTypeId(comm_type_num);
let flags = cursor.read_u8().ok()?; let flags = cursor.read_u8().ok()?;
let has_sender = (flags & 0b0000_0001) != 0; let has_sender = (flags & 0b0000_0001) != 0;
@ -201,7 +201,7 @@ impl CommunicationValue {
mod tests { mod tests {
use super::*; use super::*;
use crate::data_value::DataValue; use crate::data_value::DataValue;
use mtp_type_map::{CommTypeId, DataTypeId}; use mtp_type_map::{CommunicationTypeId, DataTypeId};
fn roundtrip(cv: CommunicationValue) -> CommunicationValue { fn roundtrip(cv: CommunicationValue) -> CommunicationValue {
let bytes = cv.to_bytes(); let bytes = cv.to_bytes();
@ -213,7 +213,7 @@ mod tests {
#[test] #[test]
fn test_flags_and_order_without_optional() { 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(); let bytes = cv.to_bytes();
// [u32 len][u16 type][flags]... // [u32 len][u16 type][flags]...
@ -231,7 +231,7 @@ mod tests {
#[test] #[test]
fn test_flags_and_order_with_all_optional() { fn test_flags_and_order_with_all_optional() {
let cv = CommunicationValue::new(CommTypeId(2)) let cv = CommunicationValue::new(CommunicationTypeId(2))
.with_id(0xAABBCCDD) .with_id(0xAABBCCDD)
.with_sender(0x0000_1122_3344_5566) .with_sender(0x0000_1122_3344_5566)
.with_receiver(0x0000_6677_8899_AABB); .with_receiver(0x0000_6677_8899_AABB);
@ -262,7 +262,7 @@ mod tests {
#[test] #[test]
fn test_roundtrip_complex() { fn test_roundtrip_complex() {
let cv = CommunicationValue::new(CommTypeId(3)) let cv = CommunicationValue::new(CommunicationTypeId(3))
.with_id(1234) .with_id(1234)
.with_sender(111) .with_sender(111)
.with_receiver(222) .with_receiver(222)
@ -279,7 +279,7 @@ mod tests {
assert_eq!(decoded.get_id(), 1234); assert_eq!(decoded.get_id(), 1234);
assert_eq!(decoded.get_sender(), 111); assert_eq!(decoded.get_sender(), 111);
assert_eq!(decoded.get_receiver(), 222); assert_eq!(decoded.get_receiver(), 222);
assert_eq!(decoded.get_type(), CommTypeId(3)); assert_eq!(decoded.get_type(), CommunicationTypeId(3));
assert_eq!( assert_eq!(
decoded.get_data(DataTypeId(1)), decoded.get_data(DataTypeId(1)),
&DataValue::Str("alice".to_string()) &DataValue::Str("alice".to_string())

View file

@ -204,9 +204,11 @@ impl DataValue {
} }
} }
/// Decrypt an `EncryptedContainer` in-place, replacing it with the /*
/// deserialized `Container`. Returns `None` if decryption or * Decrypt an `EncryptedContainer` in-place, replacing it with the
/// deserialization fails. * deserialized `Container`. Returns `None` if decryption or
* deserialization fails.
*/
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
pub fn decrypt_into_container( pub fn decrypt_into_container(
&mut self, &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")] #[cfg(feature = "crypto")]
pub fn encrypt_container( pub fn encrypt_container(
&mut self, &mut self,

View file

@ -1,4 +1,3 @@
pub mod communication_types;
pub mod communication_value; pub mod communication_value;
pub mod data_value; pub mod data_value;
pub mod util; pub mod util;
@ -8,17 +7,17 @@ pub use data_value::{DataKind, DataValue};
pub use util::rand_u32; 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; use mtp_common::CodecError;
pub fn encode(_value: &DataValue, _typemap: &TypeMap) -> Result<Vec<u8>, CodecError> { pub fn encode(_value: &DataValue, _typemap: &TypeMap) -> Result<Vec<u8>, CodecError> {
// write header using typemap.data_id(), serialize value
todo!() todo!()
} }
pub fn decode(_bytes: &[u8], _typemap: &TypeMap) -> Result<DataValue, CodecError> { pub fn decode(_bytes: &[u8], _typemap: &TypeMap) -> Result<DataValue, CodecError> {
// read header, look up type names from typemap, build DataValue
todo!() todo!()
} }

View file

@ -1,40 +1,12 @@
pub use registry::Registry;
use crate::{CodecError, DataValue, decode, encode}; use crate::{CodecError, DataValue, decode, encode};
use std::collections::BTreeMap; use mtp_type_map::Version;
use type_map::{TypeMap, Version};
#[derive(Clone, Debug)]
pub struct Registry {
versions: BTreeMap<Version, TypeMap>,
}
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<Version> {
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)
}
}
/*
* A version-aware codec that uses a multi-version registry to resolve
* the correct TypeMap for encoding and decoding operations.
*/
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct VersionedCodec { pub struct VersionedCodec {
registry: Registry, registry: Registry,
@ -48,7 +20,7 @@ impl VersionedCodec {
pub fn encode(&self, value: &DataValue, version: Version) -> Result<Vec<u8>, CodecError> { pub fn encode(&self, value: &DataValue, version: Version) -> Result<Vec<u8>, CodecError> {
let typemap = self let typemap = self
.registry .registry
.get(version) .get(&version)
.ok_or(CodecError::UnknownVersion)?; .ok_or(CodecError::UnknownVersion)?;
encode(value, typemap) encode(value, typemap)
} }
@ -56,7 +28,7 @@ impl VersionedCodec {
pub fn decode(&self, bytes: &[u8], version: Version) -> Result<DataValue, CodecError> { pub fn decode(&self, bytes: &[u8], version: Version) -> Result<DataValue, CodecError> {
let typemap = self let typemap = self
.registry .registry
.get(version) .get(&version)
.ok_or(CodecError::UnknownVersion)?; .ok_or(CodecError::UnknownVersion)?;
decode(bytes, typemap) decode(bytes, typemap)
} }
@ -64,4 +36,8 @@ impl VersionedCodec {
pub fn negotiate(&self, client_versions: &[Version]) -> Option<Version> { pub fn negotiate(&self, client_versions: &[Version]) -> Option<Version> {
self.registry.negotiate(client_versions) self.registry.negotiate(client_versions)
} }
pub fn registry(&self) -> &Registry {
&self.registry
}
} }

View file

@ -198,7 +198,6 @@ pub fn decrypt_multi(
Err(CryptoError::DecryptionFailed) Err(CryptoError::DecryptionFailed)
} }
/// Verify an Ed25519 signature against a public key.
#[cfg(feature = "ed25519-dalek")] #[cfg(feature = "ed25519-dalek")]
pub fn verify_ed25519_sig( pub fn verify_ed25519_sig(
public_key: &crate::keypair::SignaturePublicKey, public_key: &crate::keypair::SignaturePublicKey,
@ -208,7 +207,6 @@ pub fn verify_ed25519_sig(
crate::sign::verify_ed25519(public_key, msg, signature) crate::sign::verify_ed25519(public_key, msg, signature)
} }
/// Verify an ML-DSA signature against a public key.
#[cfg(feature = "ml-dsa")] #[cfg(feature = "ml-dsa")]
pub fn verify_ml_dsa_sig( pub fn verify_ml_dsa_sig(
public_key: &crate::keypair::SignaturePqPublicKey, public_key: &crate::keypair::SignaturePqPublicKey,

View file

@ -351,10 +351,12 @@ impl Keyring {
} }
} }
/// Serialize the full keyring (all six keys) into a byte vector. /*
/// * 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. * 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<u8> { pub fn to_bytes(&self) -> Vec<u8> {
let fields: &[&[u8]] = &[ let fields: &[&[u8]] = &[
self.kem_public_key.as_bytes(), self.kem_public_key.as_bytes(),

65
example-type-maps.yaml Normal file
View file

@ -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

View file

@ -4,3 +4,9 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [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"] }

View file

@ -1,5 +1,108 @@
pub fn add(left: u64, right: u64) -> u64 { use mtp_codec::{
left + right 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<u8>,
pub tls_key: Vec<u8>,
}
// 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<Self, CommunicationError> {
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<MTPConnection> {
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<Version> {
/* 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)] #[cfg(test)]
@ -7,8 +110,23 @@ mod tests {
use super::*; use super::*;
#[test] #[test]
fn it_works() { fn version_extraction() {
let result = add(2, 2); let msg = CommunicationValue::new(mtp_codec::CommunicationTypeId(15))
assert_eq!(result, 4); .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());
} }
} }

View file

@ -5,3 +5,4 @@ edition = "2024"
[dependencies] [dependencies]
mtp-common = { path = "../common" } mtp-common = { path = "../common" }
mtp-type-map = { path = "../type-map" }

View file

@ -1,94 +1,128 @@
use mtp_common::RegistryError; use std::collections::BTreeMap;
use std::collections::HashMap;
#[repr(transparent)] pub use mtp_type_map::{
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] CommunicationType, CommunicationTypeId, DataType, DataTypeId, TypeMap, Version,
pub struct CommTypeId(pub u16); };
#[repr(transparent)]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct DataTypeId(pub u16);
/* /*
* # Reserved Internal Namespace * Reserved communication-type namespace (0..32).
* These are NEVER assigned by a RegistryConfig. They are fixed * These are fixed across all protocol versions for version negotiation
* across all versions for version negotiation & security. * and security. They are never assigned by user configuration.
*/ */
pub const INTERNAL_COMM_RESERVED: std::ops::Range<u16> = 0..32;
pub const INTERNAL_COMM_RESERVED: std::ops::Range<u16> = 0..16; /* Reserved data-type namespace (0..32). */
pub const INTERNAL_DATA_RESERVED: std::ops::Range<u16> = 0..32;
#[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)
}
}
/*
* 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)] #[derive(Clone, Debug)]
// (major , minor , patch );
pub struct Version(pub u16, pub u16, pub u16);
pub struct Registry { pub struct Registry {
pub version: Version, versions: BTreeMap<Version, TypeMap>,
comm_name_to_id: HashMap<String, u16>,
comm_id_to_name: HashMap<u16, String>,
data_name_to_id: HashMap<String, u16>,
data_id_to_name: HashMap<u16, String>,
}
pub struct RegistryConfig {
pub version: Version,
pub communication_types: HashMap<String, u16>,
pub data_types: HashMap<String, u16>,
} }
impl Registry { impl Registry {
pub fn from_config(cfg: RegistryConfig) -> Result<Self, RegistryError> { pub fn new() -> Self {
for (name, &id) in &cfg.communication_types { Self {
if INTERNAL_COMM_RESERVED.contains(&id) { versions: BTreeMap::new(),
return Err(RegistryError::ReservedCommId(id, name.clone()));
}
} }
}
let mut comm_id_to_name = HashMap::with_capacity(cfg.communication_types.len()); // Register a type map, keyed by its version.
for (name, id) in &cfg.communication_types { pub fn register(&mut self, typemap: TypeMap) {
comm_id_to_name.insert(*id, name.clone()); 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<Version> {
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()); impl Default for Registry {
for (name, id) in &cfg.data_types { fn default() -> Self {
data_id_to_name.insert(*id, name.clone()); Self::new()
}
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,
})
} }
}
#[cfg(test)]
mod tests {
use super::*;
pub fn resolve_comm(&self, name: &str) -> Option<CommTypeId> { #[test]
self.comm_name_to_id.get(name).copied().map(CommTypeId) 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> { let client = &[Version(0, 0)];
self.comm_id_to_name.get(&id.0).map(|s| s.as_str()) 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<DataTypeId> { #[test]
self.data_name_to_id.get(name).copied().map(DataTypeId) 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> { assert_eq!(r.latest().unwrap().version, Version(2, 0));
self.data_id_to_name.get(&id.0).map(|s| s.as_str())
} }
} }

View file

@ -2,5 +2,10 @@
name = "mtp-type-map" name = "mtp-type-map"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
build = "build.rs"
[dependencies] [dependencies]
[build-dependencies]
serde = { version = "1", features = ["derive"] }
serde_yaml = "0.9"

440
type-map/build.rs Normal file
View file

@ -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<String, TypeMapConfig>,
}
#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
struct TypeMapConfig {
#[serde(default)]
communication_types: BTreeMap<String, u16>,
#[serde(default)]
data_types: BTreeMap<String, u16>,
}
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::<u16>().ok()?;
let minor = parts[1].parse::<u16>().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<u16> {{"
)
.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<u16> {{"
)
.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<TypeMap> {{").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();
}

View file

@ -1,28 +1,45 @@
use std::collections::HashMap;
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] #[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)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct DataTypeId(pub u16); pub struct DataTypeId(pub u16);
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[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. impl Version {
/// Compiled into the client, or loaded by the host via the registry. pub fn new(major: u16, minor: u16) -> Self {
Self(major, minor)
}
pub fn parse(s: &str) -> Option<Self> {
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)] #[derive(Clone, Debug)]
pub struct TypeMap { pub struct TypeMap {
pub version: Version, pub version: Version,
pub comm_types: HashMap<String, u16>,
pub data_types: HashMap<String, u16>,
} }
impl TypeMap { impl TypeMap {
pub fn comm_id(&self, name: &str) -> Option<u16> { pub fn new(version: Version) -> Self {
self.comm_types.get(name).copied() Self { version }
}
pub fn data_id(&self, name: &str) -> Option<u16> {
self.data_types.get(name).copied()
} }
} }
include!(concat!(env!("OUT_DIR"), "/types.rs"));