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

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"
[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 {
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<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)]
@ -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));
}
}

View file

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

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::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<DataTypeId, DataValue>,
@ -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::<BigEndian>().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())

View file

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

View file

@ -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<Vec<u8>, CodecError> {
// write header using typemap.data_id(), serialize value
todo!()
}
pub fn decode(_bytes: &[u8], _typemap: &TypeMap) -> Result<DataValue, CodecError> {
// read header, look up type names from typemap, build DataValue
todo!()
}

View file

@ -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<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)
}
}
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<Vec<u8>, 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<DataValue, CodecError> {
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<Version> {
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)
}
/// 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,

View file

@ -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<u8> {
let fields: &[&[u8]] = &[
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"
[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 {
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<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)]
@ -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());
}
}

View file

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

View file

@ -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<u16> = 0..32;
pub const INTERNAL_COMM_RESERVED: std::ops::Range<u16> = 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<u16> = 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<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>,
versions: BTreeMap<Version, TypeMap>,
}
impl Registry {
pub fn from_config(cfg: RegistryConfig) -> Result<Self, RegistryError> {
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<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());
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<CommTypeId> {
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<DataTypeId> {
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));
}
}

View file

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

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)]
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<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)]
pub struct TypeMap {
pub version: Version,
pub comm_types: HashMap<String, u16>,
pub data_types: HashMap<String, u16>,
}
impl TypeMap {
pub fn comm_id(&self, name: &str) -> Option<u16> {
self.comm_types.get(name).copied()
}
pub fn data_id(&self, name: &str) -> Option<u16> {
self.data_types.get(name).copied()
pub fn new(version: Version) -> Self {
Self { version }
}
}
include!(concat!(env!("OUT_DIR"), "/types.rs"));