diff --git a/TRANSPORT.md b/TRANSPORT.md deleted file mode 100644 index c9599ec..0000000 --- a/TRANSPORT.md +++ /dev/null @@ -1,16 +0,0 @@ -# Transport - -The Transport crate wraps QUIC using `wtransport`. It provides `Sender`/`Receiver` for bidirectional message passing over QUIC streams. - -```rust -use mtp_transport::{connect, Policy}; - -// Connect as a client -let (sender, receiver) = connect("https://example.com", None, Policy::default()).await?; - -// Send messages -sender.send(&communication_value).await?; - -// Receive messages -let msg = receiver.receive().await?; -``` diff --git a/client/Cargo.toml b/client/Cargo.toml index c42f2d7..ee1bbb5 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -5,7 +5,6 @@ edition = "2024" [dependencies] mtp-common = { path = "../common" } -mtp-type-map = { path = "../type-map" } mtp-codec = { path = "../codec" } mtp-transport = { path = "../transport" } tokio = { version = "1", features = ["full"] } diff --git a/client/src/lib.rs b/client/src/lib.rs index b212dca..6c87d46 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -1,17 +1,7 @@ -use mtp_codec::{CommunicationValue, DataTypeId, DataValue, Version}; +use mtp_codec::{CommunicationValue, DataTypeId, DataValue, Version, PROTOCOL_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 use mtp_type_map::PROTOCOL_VERSION; - pub struct ClientConfig { pub url: String, pub server_cert: Option>, @@ -42,7 +32,7 @@ impl MTPClient { // Build the initial identification message with the protocol version. let version_str = format!("{}", PROTOCOL_VERSION); - let ident = CommunicationValue::new(mtp_codec::CommunicationTypeId(15)) + let ident = CommunicationValue::new(mtp_codec::CommunicationType::Identification) .add_data(DataTypeId(3), DataValue::Str(version_str)) .add_data(DataTypeId(6), DataValue::UnsignedNumber(client_id.into())); diff --git a/codec/Cargo.toml b/codec/Cargo.toml index 88ca803..4b75c72 100644 --- a/codec/Cargo.toml +++ b/codec/Cargo.toml @@ -14,5 +14,5 @@ rand = { version = "*", features = ["std", "std_rng"] } [features] default = [] -registry = ["dep:registry"] +registry = ["dep:registry", "mtp-type-map/multi-version"] crypto = ["dep:mtp-crypto"] diff --git a/codec/src/communication_value.rs b/codec/src/communication_value.rs index 51b4f0f..68bcaf3 100644 --- a/codec/src/communication_value.rs +++ b/codec/src/communication_value.rs @@ -1,10 +1,14 @@ use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use std::collections::BTreeMap; +use std::fmt; use std::io::{Cursor, Read}; use crate::data_value::DataValue; use crate::rand_u32; -use mtp_type_map::{CommunicationType, CommunicationTypeId, DataType, DataTypeId, TypeMap}; +use mtp_type_map::{ + communication_type_name, data_type_name, CommunicationType, CommunicationTypeId, DataType, + DataTypeId, TypeMap, PROTOCOL_VERSION, +}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct CommunicationValue { @@ -18,17 +22,20 @@ pub struct CommunicationValue { #[allow(dead_code)] impl CommunicationValue { - pub fn new(comm_type: CommunicationTypeId) -> Self { + pub fn new(comm_type: CommunicationType) -> Self { + let tm = TypeMap::new(PROTOCOL_VERSION); + let id = comm_type.to_id(&tm); Self { id: rand_u32(), - comm_type, + comm_type: id, sender: 0, receiver: 0, data: BTreeMap::new(), - type_map: None, + type_map: Some(tm), } } + #[cfg(feature = "registry")] pub fn from_comm(comm_type: CommunicationType, tm: &TypeMap) -> Self { let id = comm_type.to_id(tm); Self { @@ -42,8 +49,16 @@ impl CommunicationValue { } pub fn from_comm_default(comm_type: CommunicationType) -> Self { - let tm = TypeMap::latest(); - Self::from_comm(comm_type, &tm) + let tm = TypeMap::new(PROTOCOL_VERSION); + let id = comm_type.to_id(&tm); + Self { + id: rand_u32(), + comm_type: id, + sender: 0, + receiver: 0, + data: BTreeMap::new(), + type_map: Some(tm), + } } pub fn with_id(mut self, p0: u32) -> Self { @@ -86,16 +101,14 @@ impl CommunicationValue { self } + #[cfg(feature = "registry")] pub fn add_typed(mut self, data: DataType, tm: &TypeMap, value: DataValue) -> Self { self.data.insert(data.to_id(tm), value); self } pub fn add_typed_default(mut self, data: DataType, value: DataValue) -> Self { - let tm = self - .type_map - .clone() - .unwrap_or_else(TypeMap::latest); + let tm = self.type_map.clone().unwrap_or_else(TypeMap::latest); self.data.insert(data.to_id(&tm), value); self } @@ -231,6 +244,79 @@ impl CommunicationValue { } } +fn fmt_data_value(val: &DataValue, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match val { + DataValue::Container(entries) => { + write!(f, "{{")?; + for (i, (key, value)) in entries.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + let name = data_type_name(key.0).unwrap_or("?"); + write!(f, "{}: ", name)?; + fmt_data_value(value, f)?; + } + write!(f, "}}") + } + DataValue::Array(arr) => { + write!(f, "[")?; + for (i, value) in arr.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + fmt_data_value(value, f)?; + } + write!(f, "]") + } + #[cfg(feature = "crypto")] + DataValue::EncryptedContainer(_) => write!(f, "(Secure)"), + DataValue::Bytes(_) => write!(f, "(Binary)"), + other => write!(f, "{}", other), + } +} + +const BOLD_BLUE: &str = "\x1b[1;34m"; +const GREEN: &str = "\x1b[32m"; +const YELLOW: &str = "\x1b[33m"; +const ORANGE: &str = "\x1b[38;5;208m"; +const RESET: &str = "\x1b[0m"; + +impl fmt::Display for CommunicationValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let version = self + .type_map + .as_ref() + .map(|tm| &tm.version) + .unwrap_or(&PROTOCOL_VERSION); + + write!(f, "V{}{}{}", BOLD_BLUE, version, RESET)?; + + if self.id != 0 { + write!(f, ", ID:{}{:X}{}", GREEN, self.id, RESET)?; + } + if self.sender != 0 { + write!(f, ", S:{}{:X}{}", YELLOW, self.sender, RESET)?; + } + if self.receiver != 0 { + write!(f, ", R:{}{:X}{}", ORANGE, self.receiver, RESET)?; + } + + let name = communication_type_name(self.comm_type.0).unwrap_or("?"); + write!(f, ", {}: ", name)?; + + write!(f, "{{")?; + for (i, (key, value)) in self.data.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + let dname = data_type_name(key.0).unwrap_or("?"); + write!(f, "{}: ", dname)?; + fmt_data_value(value, f)?; + } + write!(f, "}}") + } +} + /* ================================ TESTS ================================ */ #[cfg(test)] mod tests { @@ -247,7 +333,7 @@ mod tests { #[test] fn test_flags_and_order_without_optional() { - let cv = CommunicationValue::new(CommunicationTypeId(1)).with_id(0); + let cv = CommunicationValue::from_comm_default(CommunicationType::ErrorParsing).with_id(0); let bytes = cv.to_bytes(); // [u32 len][u16 type][flags]... @@ -265,7 +351,7 @@ mod tests { #[test] fn test_flags_and_order_with_all_optional() { - let cv = CommunicationValue::new(CommunicationTypeId(2)) + let cv = CommunicationValue::from_comm_default(CommunicationType::ErrorBadVersion) .with_id(0xAABBCCDD) .with_sender(0x0000_1122_3344_5566) .with_receiver(0x0000_6677_8899_AABB); @@ -276,10 +362,10 @@ mod tests { let total_len = c.read_u32::().expect("len"); assert_eq!(total_len as usize + 4, bytes.len()); - let typ = c.read_u16::().expect("type"); + let typ = c.read_u16::().expect("read type"); assert_eq!(typ, 2); - let flags = c.read_u8().expect("flags"); + let flags = c.read_u8().expect("read flags"); assert_eq!(flags & 0b0000_0111, 0b0000_0111); let id = c.read_u32::().expect("id"); @@ -296,7 +382,7 @@ mod tests { #[test] fn test_roundtrip_complex() { - let cv = CommunicationValue::new(CommunicationTypeId(3)) + let cv = CommunicationValue::from_comm_default(CommunicationType::Disconnect) .with_id(1234) .with_sender(111) .with_receiver(222) diff --git a/codec/src/data_value.rs b/codec/src/data_value.rs index 0099e8b..af1f361 100644 --- a/codec/src/data_value.rs +++ b/codec/src/data_value.rs @@ -2,6 +2,7 @@ use base64::Engine; use base64::engine::general_purpose; use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use std::collections::BTreeMap; +use std::fmt; use std::hash::{Hash, Hasher}; use std::io::Cursor; @@ -635,6 +636,44 @@ impl DataValue { } } +impl fmt::Display for DataValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + DataValue::BoolTrue => write!(f, "true"), + DataValue::BoolFalse => write!(f, "false"), + DataValue::Bool(v) => write!(f, "{}", v), + DataValue::SignedNumber(n) => write!(f, "{}", n), + DataValue::UnsignedNumber(n) => write!(f, "{}", n), + DataValue::Float(exp, mant) => write!(f, "{}e{}", mant, exp), + DataValue::Str(s) => write!(f, "\"{}\"", s), + DataValue::Container(entries) => { + write!(f, "{{")?; + for (i, (key, value)) in entries.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}: {}", key.0, value)?; + } + write!(f, "}}") + } + DataValue::Array(arr) => { + write!(f, "[")?; + for (i, value) in arr.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}", value)?; + } + write!(f, "]") + } + DataValue::Bytes(_) => write!(f, "(Binary)"), + #[cfg(feature = "crypto")] + DataValue::EncryptedContainer(_) => write!(f, "(Secure)"), + DataValue::Null => write!(f, "null"), + } + } +} + impl PartialEq for DataValue { fn eq(&self, other: &Self) -> bool { use DataValue::*; diff --git a/codec/src/lib.rs b/codec/src/lib.rs index 6757825..de8d858 100644 --- a/codec/src/lib.rs +++ b/codec/src/lib.rs @@ -8,7 +8,8 @@ pub use data_value::{DataKind, DataValue}; pub use util::rand_u32; pub use mtp_type_map::{ - CommunicationType, CommunicationTypeId, DataType, DataTypeId, TypeMap, Version, + communication_type_name, data_type_name, CommunicationType, CommunicationTypeId, DataType, + DataTypeId, PROTOCOL_VERSION, TypeMap, Version, }; use mtp_common::CodecError; diff --git a/example-type-maps.yaml b/example-type-maps.yaml index 8ec285b..0a78ca6 100644 --- a/example-type-maps.yaml +++ b/example-type-maps.yaml @@ -1,5 +1,5 @@ # The version a Client should use -protocol_version: "2.0" +protocol_version: "0.0" # Note that markers 0 to 31 are reserved for default use, manually working with them is not recommended # Fixed CommunicationType markers are: diff --git a/example-useage/.cargo/config.toml b/example-useage/.cargo/config.toml new file mode 100644 index 0000000..d363b83 --- /dev/null +++ b/example-useage/.cargo/config.toml @@ -0,0 +1,2 @@ +[env] +MTP_TYPE_MAPS = { value = "type-maps.yaml", relative = true } diff --git a/example-useage/Cargo.toml b/example-useage/Cargo.toml new file mode 100644 index 0000000..9ec3f02 --- /dev/null +++ b/example-useage/Cargo.toml @@ -0,0 +1,6 @@ +[workspace] +members = [ + "server", + "client", +] +resolver = "3" diff --git a/example-useage/client/Cargo.toml b/example-useage/client/Cargo.toml new file mode 100644 index 0000000..67a0fed --- /dev/null +++ b/example-useage/client/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "mtp-example-client" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "mtp-example-client" +path = "src/main.rs" + +[dependencies] +mtp-client = { path = "../../client" } +mtp-codec = { path = "../../codec" } +tokio = { version = "1", features = ["full"] } diff --git a/example-useage/client/src/main.rs b/example-useage/client/src/main.rs new file mode 100644 index 0000000..d9be8a2 --- /dev/null +++ b/example-useage/client/src/main.rs @@ -0,0 +1,48 @@ +use std::fs; + +use mtp_client::{ClientConfig, MTPClient}; +use mtp_codec::{CommunicationType, CommunicationValue, DataType, DataValue}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let cert_pem = fs::read("server.pem") + .expect("Missing server.pem server generates it"); + + println!("Connecting to 127.0.0.1:8080 ..."); + + let config = ClientConfig { + url: "https://127.0.0.1:8080".into(), + server_cert: Some(cert_pem), + }; + + let conn = MTPClient::connect(config, 1001).await?; + println!("Connected (version {})", conn.version); + + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + + let greeting = CommunicationValue::new(CommunicationType::Ping) + .add_typed_default(DataType::Description, DataValue::Str("Hello MTP!".into())) + .add_typed_default(DataType::Timestamp, DataValue::UnsignedNumber(timestamp as u128)) + .add_typed_default(DataType::Data, DataValue::UnsignedNumber(42)) + .with_sender(1001); + + println!("Sending: {greeting}"); + conn.sender.send(&greeting).await?; + + match conn.receiver.receive().await { + Ok(msg) => { + println!("Received: {msg}"); + } + Err(e) => { + eprintln!("Receive error: {e}"); + } + } + + conn.sender.close(); + println!("\nDone"); + + Ok(()) +} diff --git a/example-useage/server/Cargo.toml b/example-useage/server/Cargo.toml new file mode 100644 index 0000000..6da0426 --- /dev/null +++ b/example-useage/server/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "mtp-example-server" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "mtp-example-server" +path = "src/main.rs" + +[dependencies] +mtp-host = { path = "../../host" } +mtp-codec = { path = "../../codec" } +rcgen = "0.14" +tokio = { version = "1", features = ["full"] } diff --git a/example-useage/server/src/main.rs b/example-useage/server/src/main.rs new file mode 100644 index 0000000..e58eaae --- /dev/null +++ b/example-useage/server/src/main.rs @@ -0,0 +1,90 @@ +use std::fs; + +use mtp_codec::{CommunicationType, CommunicationValue, DataType, DataValue, TypeMap}; +use mtp_host::{HostConfig, MTPHost}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let cert_path = "server.pem"; + let key_path = "server.key"; + + let (cert_pem, key_pem) = if let (Ok(c), Ok(k)) = (fs::read(cert_path), fs::read(key_path)) { + println!("Using existing TLS cert from {cert_path}"); + (c, k) + } else { + println!("Generating self-signed TLS certificate ..."); + + let key_pair = rcgen::KeyPair::generate()?; + let params = rcgen::CertificateParams::new(vec![ + "localhost".into(), + "127.0.0.1".into(), + ])?; + let cert = params.self_signed(&key_pair)?; + + let cert_str = cert.pem(); + let key_str = key_pair.serialize_pem(); + + fs::write(cert_path, cert_str.as_bytes())?; + fs::write(key_path, key_str.as_bytes())?; + println!("Wrote {cert_path} and {key_path}"); + + (cert_str.into_bytes(), key_str.into_bytes()) + }; + + println!("Starting MTP server on port 8080 ..."); + + let config = HostConfig { + ip: "::".into(), + port: 8080, + tls_fullchain: cert_pem, + tls_key: key_pem, + }; + + let mut host = MTPHost::new(config).await?; + println!("Server listening on {}", host.local_addr()); + + while let Some(conn) = host.accept().await { + println!("\n--- New connection accepted (version {}) ---", conn.version); + + let tm: &TypeMap = conn.codec.registry().get(&conn.version).unwrap(); + + match conn.receiver.receive().await { + Ok(msg) => { + println!("Received: {msg}"); + + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + + let response = CommunicationValue::from_comm( + CommunicationType::Pong, + tm, + ) + .add_data( + mtp_codec::DataTypeId(tm.data_id_enum(DataType::Description).unwrap()), + DataValue::Str("Hello from server!".into()), + ) + .add_data( + mtp_codec::DataTypeId(tm.data_id_enum(DataType::Timestamp).unwrap()), + DataValue::UnsignedNumber(timestamp as u128), + ) + .add_data( + mtp_codec::DataTypeId(tm.data_id_enum(DataType::Data).unwrap()), + DataValue::Str("custom payload".into()), + ); + + println!("Sending: {response}"); + conn.sender.send(&response).await?; + } + Err(e) => { + eprintln!("Receive error: {e}"); + } + } + + conn.sender.close(); + println!("Connection closed\n"); + } + + Ok(()) +} diff --git a/example-useage/type-maps.yaml b/example-useage/type-maps.yaml new file mode 100644 index 0000000..863d7f5 --- /dev/null +++ b/example-useage/type-maps.yaml @@ -0,0 +1,14 @@ +protocol_version: "1.0" + +type_maps: + "0.0": + CommunicationTypes: + DataTypes: + "1.0": + CommunicationTypes: + DataTypes: + Data: 32 + "2.0": + CommunicationTypes: + DataTypes: + Data: 34 diff --git a/host/Cargo.toml b/host/Cargo.toml index 861aa64..fd26447 100644 --- a/host/Cargo.toml +++ b/host/Cargo.toml @@ -5,8 +5,6 @@ edition = "2024" [dependencies] mtp-common = { path = "../common" } -mtp-type-map = { path = "../type-map" } mtp-codec = { path = "../codec", features = ["registry"] } mtp-transport = { path = "../transport", features = ["host"] } -registry = { path = "../registry" } tokio = { version = "1", features = ["full"] } diff --git a/host/src/lib.rs b/host/src/lib.rs index f7b6e07..739bef0 100644 --- a/host/src/lib.rs +++ b/host/src/lib.rs @@ -109,7 +109,8 @@ mod tests { #[test] fn version_extraction() { - let msg = CommunicationValue::new(mtp_codec::CommunicationTypeId(15)) + let tm = mtp_codec::TypeMap::latest(); + let msg = CommunicationValue::from_comm(mtp_codec::CommunicationType::Identification, &tm) .add_data(DataTypeId(3), DataValue::Str("2.0".to_string())); let version = extract_version(&msg); assert_eq!(version, Some(Version(2, 0))); @@ -117,13 +118,15 @@ mod tests { #[test] fn version_extraction_returns_none_for_missing() { - let msg = CommunicationValue::new(mtp_codec::CommunicationTypeId(15)); + let tm = mtp_codec::TypeMap::latest(); + let msg = CommunicationValue::from_comm(mtp_codec::CommunicationType::Identification, &tm); assert!(extract_version(&msg).is_none()); } #[test] fn version_extraction_bad_format() { - let msg = CommunicationValue::new(mtp_codec::CommunicationTypeId(15)) + let tm = mtp_codec::TypeMap::latest(); + let msg = CommunicationValue::from_comm(mtp_codec::CommunicationType::Identification, &tm) .add_data(DataTypeId(3), DataValue::UnsignedNumber(42)); assert!(extract_version(&msg).is_none()); } diff --git a/registry/Cargo.toml b/registry/Cargo.toml index 9967888..1188739 100644 --- a/registry/Cargo.toml +++ b/registry/Cargo.toml @@ -5,4 +5,4 @@ edition = "2024" [dependencies] mtp-common = { path = "../common" } -mtp-type-map = { path = "../type-map" } +mtp-type-map = { path = "../type-map", features = ["multi-version"] } diff --git a/type-map/Cargo.toml b/type-map/Cargo.toml index 1468aef..a233cc7 100644 --- a/type-map/Cargo.toml +++ b/type-map/Cargo.toml @@ -4,6 +4,10 @@ version = "0.1.0" edition = "2024" build = "build.rs" +[features] +default = [] +multi-version = [] + [dependencies] [build-dependencies] diff --git a/type-map/build.rs b/type-map/build.rs index c3f47fe..46e7dd2 100644 --- a/type-map/build.rs +++ b/type-map/build.rs @@ -24,134 +24,45 @@ struct ReservedEntry { } 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, - }, + 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: "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, - }, + ReservedEntry { name: "Nonce", id: 7 }, + ReservedEntry { name: "PublicKeys", id: 8 }, + ReservedEntry { name: "Signature", id: 9 }, ]; fn main() { let out = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap()); + let multi_version = std::env::var("CARGO_FEATURE_MULTI_VERSION").is_ok(); let config = match std::env::var("MTP_TYPE_MAPS") { Ok(config_path) => { @@ -170,7 +81,7 @@ fn main() { } }; - let code = generate(&config); + let code = generate(&config, multi_version); std::fs::write(out.join("types.rs"), code).unwrap(); } @@ -193,7 +104,7 @@ fn sorted_versions(config: &Config) -> Vec<(String, u16, u16)> { versions } -fn generate(config: &Config) -> String { +fn generate(config: &Config, multi_version: bool) -> String { let mut out = String::new(); writeln!( out, @@ -202,27 +113,48 @@ fn generate(config: &Config) -> String { .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()); + let (all_comm_names, all_data_names, sorted) = if multi_version { + 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()); + } } - for name in tm.data_types.keys() { - all_data_names.insert(name.as_str()); + let sorted = sorted_versions(config); + (all_comm_names, all_data_names, sorted) + } else { + let mut all_comm_names: BTreeSet<&str> = BTreeSet::new(); + let mut all_data_names: BTreeSet<&str> = BTreeSet::new(); + if let Some(tm) = config.type_maps.get(&config.protocol_version) { + 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); + (all_comm_names, all_data_names, vec![]) + }; generate_protocol_version(&mut out, 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); - generate_enum_conversion_methods(&mut out, &sorted); + generate_latest_method(&mut out, config); + + if multi_version { + generate_versioned_constructors(&mut out, &sorted); + generate_builtin_type_maps(&mut out, &sorted); + generate_lookup_methods(&mut out, config, &sorted); + } else { + generate_single_version_lookup(&mut out, config); + } + + generate_enum_conversion_methods(&mut out); + generate_reverse_lookups(&mut out, config, &sorted, multi_version); out } @@ -249,6 +181,32 @@ fn generate_protocol_version(out: &mut String, config: &Config) { writeln!(out).unwrap(); } +fn generate_latest_method(out: &mut String, config: &Config) { + let (major, minor) = if config.protocol_version.is_empty() { + (0, 0) + } else { + let parts: Vec<&str> = config.protocol_version.split('.').collect(); + if parts.len() == 2 { + let maj = parts[0].parse::().unwrap_or(0); + let min = parts[1].parse::().unwrap_or(0); + (maj, min) + } else { + (0, 0) + } + }; + writeln!(out, "impl TypeMap {{").unwrap(); + writeln!(out, " pub fn latest() -> Self {{").unwrap(); + writeln!( + out, + " Self {{ version: Version({}, {}) }}", + major, minor + ) + .unwrap(); + writeln!(out, " }}").unwrap(); + writeln!(out, "}}").unwrap(); + writeln!(out).unwrap(); +} + fn generate_comm_type_enum(out: &mut String, user_names: &BTreeSet<&str>) { writeln!( out, @@ -357,7 +315,7 @@ fn generate_data_type_enum(out: &mut String, user_names: &BTreeSet<&str>) { writeln!(out).unwrap(); } -fn generate_type_map_constructors(out: &mut String, sorted_versions: &[(String, u16, u16)]) { +fn generate_versioned_constructors(out: &mut String, sorted_versions: &[(String, u16, u16)]) { for (version_key, _major, _minor) in sorted_versions { let version_ident = version_key.replace('.', "_"); @@ -377,22 +335,6 @@ fn generate_type_map_constructors(out: &mut String, sorted_versions: &[(String, writeln!(out, "}}").unwrap(); writeln!(out).unwrap(); } - - let (latest_major, latest_minor) = match sorted_versions.last() { - Some((_key, major, minor)) => (*major, *minor), - None => (0, 0), - }; - writeln!(out, "impl TypeMap {{").unwrap(); - writeln!(out, " pub fn latest() -> Self {{").unwrap(); - writeln!( - out, - " Self {{ version: Version({}, {}) }}", - latest_major, latest_minor - ) - .unwrap(); - writeln!(out, " }}").unwrap(); - writeln!(out, "}}").unwrap(); - writeln!(out).unwrap(); } fn generate_lookup_methods( @@ -400,7 +342,6 @@ fn generate_lookup_methods( config: &Config, sorted_versions: &[(String, u16, u16)], ) { - // comm_id_enum writeln!(out, "#[allow(unreachable_patterns, unused_variables)]").unwrap(); writeln!(out, "impl TypeMap {{").unwrap(); writeln!( @@ -441,7 +382,6 @@ fn generate_lookup_methods( writeln!(out, " }}").unwrap(); writeln!(out).unwrap(); - // data_id_enum writeln!(out, " #[allow(unused_variables)]").unwrap(); writeln!( out, @@ -478,6 +418,81 @@ fn generate_lookup_methods( writeln!(out).unwrap(); } +fn generate_single_version_lookup(out: &mut String, config: &Config) { + let tm_cfg = config.type_maps.get(&config.protocol_version); + + writeln!(out, "#[allow(unreachable_patterns, unused_variables)]").unwrap(); + writeln!(out, "impl TypeMap {{").unwrap(); + writeln!( + out, + " pub fn comm_id_enum(&self, ct: CommunicationType) -> Option {{" + ) + .unwrap(); + writeln!(out, " match self.version {{").unwrap(); + writeln!( + out, + " PROTOCOL_VERSION => match ct {{" + ) + .unwrap(); + for entry in RESERVED_COMM_TYPES { + writeln!( + out, + " CommunicationType::{} => Some({}),", + entry.name, entry.id + ) + .unwrap(); + } + if let Some(tm_cfg) = tm_cfg { + 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(); + + writeln!(out, " #[allow(unused_variables)]").unwrap(); + writeln!( + out, + " pub fn data_id_enum(&self, dt: DataType) -> Option {{" + ) + .unwrap(); + writeln!(out, " match self.version {{").unwrap(); + writeln!( + out, + " PROTOCOL_VERSION => match dt {{" + ) + .unwrap(); + for entry in RESERVED_DATA_TYPES { + writeln!( + out, + " DataType::{} => Some({}),", + entry.name, entry.id + ) + .unwrap(); + } + if let Some(tm_cfg) = tm_cfg { + for (name, id) in &tm_cfg.data_types { + writeln!(out, " DataType::{} => Some({}),", name, id).unwrap(); + } + } + writeln!(out, " _ => None,").unwrap(); + writeln!(out, " }},").unwrap(); + writeln!(out, " _ => None,").unwrap(); + writeln!(out, " }}").unwrap(); + writeln!(out, " }}").unwrap(); + writeln!(out, "}}").unwrap(); + writeln!(out).unwrap(); +} + fn generate_builtin_type_maps(out: &mut String, sorted_versions: &[(String, u16, u16)]) { writeln!(out, "pub fn builtin_type_maps() -> Vec {{").unwrap(); writeln!(out, " vec![").unwrap(); @@ -490,7 +505,70 @@ fn generate_builtin_type_maps(out: &mut String, sorted_versions: &[(String, u16, writeln!(out).unwrap(); } -fn generate_enum_conversion_methods(out: &mut String, _sorted_versions: &[(String, u16, u16)]) { +fn generate_reverse_lookups( + out: &mut String, + config: &Config, + sorted_versions: &[(String, u16, u16)], + multi_version: bool, +) { + let mut id_to_comm: BTreeMap = BTreeMap::new(); + for entry in RESERVED_COMM_TYPES { + id_to_comm.insert(entry.id, entry.name.to_string()); + } + if multi_version { + for (version_key, _major, _minor) in sorted_versions { + if let Some(tm_cfg) = config.type_maps.get(version_key) { + for (name, id) in &tm_cfg.communication_types { + id_to_comm.insert(*id, name.clone()); + } + } + } + } else if let Some(tm_cfg) = config.type_maps.get(&config.protocol_version) { + for (name, id) in &tm_cfg.communication_types { + id_to_comm.insert(*id, name.clone()); + } + } + + writeln!(out, "pub fn communication_type_name(id: u16) -> Option<&'static str> {{").unwrap(); + writeln!(out, " match id {{").unwrap(); + for (id, name) in &id_to_comm { + writeln!(out, " {} => Some(\"{}\"),", id, name).unwrap(); + } + writeln!(out, " _ => None,").unwrap(); + writeln!(out, " }}").unwrap(); + writeln!(out, "}}").unwrap(); + writeln!(out).unwrap(); + + let mut id_to_data: BTreeMap = BTreeMap::new(); + for entry in RESERVED_DATA_TYPES { + id_to_data.insert(entry.id, entry.name.to_string()); + } + if multi_version { + for (version_key, _major, _minor) in sorted_versions { + if let Some(tm_cfg) = config.type_maps.get(version_key) { + for (name, id) in &tm_cfg.data_types { + id_to_data.insert(*id, name.clone()); + } + } + } + } else if let Some(tm_cfg) = config.type_maps.get(&config.protocol_version) { + for (name, id) in &tm_cfg.data_types { + id_to_data.insert(*id, name.clone()); + } + } + + writeln!(out, "pub fn data_type_name(id: u16) -> Option<&'static str> {{").unwrap(); + writeln!(out, " match id {{").unwrap(); + for (id, name) in &id_to_data { + writeln!(out, " {} => Some(\"{}\"),", id, name).unwrap(); + } + writeln!(out, " _ => None,").unwrap(); + writeln!(out, " }}").unwrap(); + writeln!(out, "}}").unwrap(); + writeln!(out).unwrap(); +} + +fn generate_enum_conversion_methods(out: &mut String) { writeln!(out, "impl CommunicationType {{").unwrap(); writeln!(out, " pub fn to_id(self, tm: &TypeMap) -> CommunicationTypeId {{").unwrap(); writeln!(