Types Creation
This commit is contained in:
parent
94f2280570
commit
8337fa3d8f
22 changed files with 1141 additions and 202 deletions
136
CONNECTOR.md
Normal file
136
CONNECTOR.md
Normal 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.
|
||||||
33
README.md
33
README.md
|
|
@ -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
16
TRANSPORT.md
Normal 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
89
TYPE-MAP.md
Normal 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 }
|
||||||
|
```
|
||||||
|
|
@ -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"] }
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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())
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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!()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
65
example-type-maps.yaml
Normal 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
|
||||||
|
|
@ -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"] }
|
||||||
|
|
|
||||||
128
host/src/lib.rs
128
host/src/lib.rs
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,3 +5,4 @@ edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
mtp-common = { path = "../common" }
|
mtp-common = { path = "../common" }
|
||||||
|
mtp-type-map = { path = "../type-map" }
|
||||||
|
|
|
||||||
|
|
@ -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())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
440
type-map/build.rs
Normal 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();
|
||||||
|
}
|
||||||
|
|
@ -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"));
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue