commit 0bbcab572744f8c0fccc66070acd184373cc00f8 Author: Alex Emmet <111742636+Alex-Emmet@users.noreply.github.com> Date: Sat Jun 20 01:55:31 2026 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c1b18f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +**target/ +**Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..78c22f3 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[workspace] +members = [ + "common", + "crypto", + "registry", + "type-map", + "codec", + "transport", + "host", + "client", +] +resolver = "3" diff --git a/README.md b/README.md new file mode 100644 index 0000000..4cb4408 --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# **M**ethanium **T**ransport **P**rotocol + +**MTP** is a **m**odular **t**ransport **p**rotocol by Methanium. + +## Crates +- Common +- Crypto +- TypeMap +- Registry +- Codec +- Transport +- Host +- Client + +## Registry & TypeMaps +A TypeMap maps Communication-Types to a byte, and Data-Types to 2 bytes. +A Communication- or Data-Type is a String to be used Plain in the code, while the Byte is transmitted. + +The Registries map a TypeMap to a specific version of MTP. + +## Codec +The Codec crate handles encoding and decoding of MTP packets using Communication Types and Data Types from the Registry. +The Codec has these Data Values: +- Container +- Encrypted Container +- Signed Integer +- Unsigned Integer +- Boolean +- Signed Float +- String +- Array +- Binary (List of Bytes) + +The Codec uses the Crypto crate to encrypt and decrypt Encrypted Containers. + +## Transport +The Transport crate wraps QUIC. + +## Host & Client +The host creates a quic Server that listens for incoming connections. +The host also manages the registry and versions to allow for backwards compatibility with clients. + +The client connects to a quic Server and uses a registry to communicate with the host. + +The Client may be Outdated while the Host is updated. + +## Common +Common Handles Logging and Error Handling. Personal information reported to Common will be anonmized. + +## Crypto Stack +| Crate | Audited? | Notes | +| ---------------- | -------- | ------------------------------------------------ | +| ml-dsa | No | NIST vectors pass; regression bug fixed Jan 2026 | +| ed25519-dalek | Yes | Used by Signal, Diem | +| chacha20poly1305 | Yes | NCC Group audit, Dec 2019 | +| aes-gcm | Yes | NCC Group audit, Dec 2019 | +| hkdf | No | Simple construction; well-reviewed | +| sha2 | No | Standard construction; widely reviewed | +| zeroize | No | Simple; widely used | +| mlkem-tls | No | mlkem-rs backend unaudited | diff --git a/client/Cargo.toml b/client/Cargo.toml new file mode 100644 index 0000000..09c50c3 --- /dev/null +++ b/client/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "client" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/client/src/lib.rs b/client/src/lib.rs new file mode 100644 index 0000000..b93cf3f --- /dev/null +++ b/client/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/codec/Cargo.toml b/codec/Cargo.toml new file mode 100644 index 0000000..652fbe6 --- /dev/null +++ b/codec/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "mtp-codec" +version = "0.1.0" +edition = "2024" + +[dependencies] +type-map = { path = "../type-map" } +common = { path = "../common" } +mtp-crypto = { path = "../crypto", optional = true } +base64 = "*" +byteorder = "*" +rand = { version = "*", features = ["std", "std_rng"] } + +[features] +default = [] +# Enables the Registry module and versioned codec wrapper +registry = [] +# Enables EncryptedContainer support via mtp-crypto +crypto = ["dep:mtp-crypto"] diff --git a/codec/src/communication_types.rs b/codec/src/communication_types.rs new file mode 100644 index 0000000..16a4f1d --- /dev/null +++ b/codec/src/communication_types.rs @@ -0,0 +1,25 @@ +// 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 = 0x01, + message_send = 0x02, + update = 0x03, +} + +impl CommunicationType { + pub fn as_number(self) -> u8 { + self as u8 + } + + pub fn from_number(n: u8) -> Self { + match n { + 0x01 => CommunicationType::ping, + 0x02 => CommunicationType::message_send, + 0x03 => CommunicationType::update, + _ => CommunicationType::ping, + } + } +} diff --git a/codec/src/communication_value.rs b/codec/src/communication_value.rs new file mode 100644 index 0000000..354efc2 --- /dev/null +++ b/codec/src/communication_value.rs @@ -0,0 +1,300 @@ +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use std::collections::BTreeMap; +use std::io::{Cursor, Read}; + +use crate::data_value::DataValue; +use crate::rand_u32; +use type_map::{CommTypeId, DataTypeId}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CommunicationValue { + id: u32, + comm_type: CommTypeId, + sender: u64, + receiver: u64, + data: BTreeMap, +} + +#[allow(dead_code)] +impl CommunicationValue { + pub fn new(comm_type: CommTypeId) -> Self { + Self { + id: rand_u32(), + comm_type, + sender: 0, + receiver: 0, + data: BTreeMap::new(), + } + } + + pub fn with_id(mut self, p0: u32) -> Self { + self.id = p0; + self + } + + pub fn get_id(&self) -> u32 { + self.id + } + + pub fn with_sender(mut self, sender: u64) -> Self { + self.sender = sender; + self + } + + pub fn get_sender(&self) -> u64 { + self.sender + } + + pub fn with_receiver(mut self, receiver: u64) -> Self { + self.receiver = receiver; + self + } + + pub fn get_receiver(&self) -> u64 { + self.receiver + } + + pub fn get_type(&self) -> CommTypeId { + self.comm_type + } + + pub fn is_type(&self, p0: CommTypeId) -> bool { + self.comm_type == p0 + } + + pub fn add_data(mut self, data: DataTypeId, value: DataValue) -> Self { + self.data.insert(data, value); + self + } + + pub fn get_data(&self, data_type: DataTypeId) -> &DataValue { + self.data.get(&data_type).unwrap_or(&DataValue::Null) + } + + pub fn get_data_container(&self) -> &BTreeMap { + &self.data + } +} + +impl CommunicationValue { + /* + * Frame format (strict new format): + * [4 bytes u32 total_length] // number of bytes after this field + * [1 byte communication_type] + * [1 byte flags] + * [optional 4 bytes id] // if flags bit2 set + * [optional 6 bytes sender] // if flags bit0 set + * [optional 6 bytes receiver]// if flags bit1 set + * [data container bytes...] + * + * Flags: + * bit0 => has sender + * bit1 => has receiver + * bit2 => has id + */ + pub fn to_bytes(&self) -> Vec { + let mut payload = Vec::new(); + + let has_sender = self.sender != 0; + let has_receiver = self.receiver != 0; + let has_id = self.id != 0; + + let mut flags: u8 = 0; + if has_sender { + flags |= 0b0000_0001; + } + if has_receiver { + flags |= 0b0000_0010; + } + if has_id { + flags |= 0b0000_0100; + } + + payload.push(self.comm_type.0); + payload.push(flags); + + if has_id { + let _ = payload.write_u32::(self.id); + } + + if has_sender { + let sender_be = self.sender.to_be_bytes(); + payload.extend_from_slice(&sender_be[2..]); // 6 bytes + } + + if has_receiver { + let receiver_be = self.receiver.to_be_bytes(); + payload.extend_from_slice(&receiver_be[2..]); // 6 bytes + } + + let container_value = DataValue::container_from_map(&self.data); + let container_bytes = container_value.to_bytes(); + payload.extend_from_slice(&container_bytes); + + let mut frame = Vec::with_capacity(4 + payload.len()); + let _ = frame.write_u32::(payload.len() as u32); + frame.extend_from_slice(&payload); + + frame + } + + pub fn from_bytes(bytes: &[u8]) -> Option { + let mut cursor = Cursor::new(bytes); + + let total_len = cursor.read_u32::().ok()? as usize; + if bytes.len() < 4 + total_len { + return None; + } + + let frame_end = 4 + total_len; + + let comm_type_num = cursor.read_u8().ok()?; + let comm_type = CommTypeId(comm_type_num); + + let flags = cursor.read_u8().ok()?; + let has_sender = (flags & 0b0000_0001) != 0; + let has_receiver = (flags & 0b0000_0010) != 0; + let has_id = (flags & 0b0000_0100) != 0; + + let id = if has_id { + cursor.read_u32::().ok()? + } else { + 0 + }; + + let sender = if has_sender { + let mut buf = [0u8; 8]; + cursor.read_exact(&mut buf[2..]).ok()?; + u64::from_be_bytes(buf) + } else { + 0 + }; + + let receiver = if has_receiver { + let mut buf = [0u8; 8]; + cursor.read_exact(&mut buf[2..]).ok()?; + u64::from_be_bytes(buf) + } else { + 0 + }; + + let pos = cursor.position() as usize; + if pos > frame_end { + return None; + } + + let data_bytes = &bytes[pos..frame_end]; + let data_value = DataValue::from_bytes(data_bytes)?; + let data = data_value.as_map()?; + + Some(Self { + id, + comm_type, + sender, + receiver, + data, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::data_value::DataValue; + use type_map::{CommTypeId, DataTypeId}; + + fn roundtrip(cv: CommunicationValue) -> CommunicationValue { + let bytes = cv.to_bytes(); + let decoded = CommunicationValue::from_bytes(&bytes).expect("failed to deserialize"); + let bytes2 = decoded.to_bytes(); + assert_eq!(bytes, bytes2); + decoded + } + + #[test] + fn test_flags_and_order_without_optional() { + let cv = CommunicationValue::new(CommTypeId(1)).with_id(0); + let bytes = cv.to_bytes(); + + // [u32 len][type][flags]... + assert!(bytes.len() >= 6); + let mut c = Cursor::new(bytes.as_slice()); + let total_len = c.read_u32::().expect("read len"); + assert_eq!(total_len as usize + 4, bytes.len()); + + let typ = c.read_u8().expect("read type"); + assert_eq!(typ, 1); + + let flags = c.read_u8().expect("read flags"); + assert_eq!(flags & 0b0000_0111, 0); + } + + #[test] + fn test_flags_and_order_with_all_optional() { + let cv = CommunicationValue::new(CommTypeId(2)) + .with_id(0xAABBCCDD) + .with_sender(0x0000_1122_3344_5566) + .with_receiver(0x0000_6677_8899_AABB); + + let bytes = cv.to_bytes(); + let mut c = Cursor::new(bytes.as_slice()); + + let total_len = c.read_u32::().expect("len"); + assert_eq!(total_len as usize + 4, bytes.len()); + + let typ = c.read_u8().expect("type"); + assert_eq!(typ, 2); + + let flags = c.read_u8().expect("flags"); + assert_eq!(flags & 0b0000_0111, 0b0000_0111); + + let id = c.read_u32::().expect("id"); + assert_eq!(id, 0xAABBCCDD); + + let mut sender6 = [0u8; 6]; + c.read_exact(&mut sender6).expect("sender"); + assert_eq!(sender6, [0x11, 0x22, 0x33, 0x44, 0x55, 0x66]); + + let mut receiver6 = [0u8; 6]; + c.read_exact(&mut receiver6).expect("receiver"); + assert_eq!(receiver6, [0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB]); + } + + #[test] + fn test_roundtrip_complex() { + let cv = CommunicationValue::new(CommTypeId(3)) + .with_id(1234) + .with_sender(111) + .with_receiver(222) + .add_data(DataTypeId(1), DataValue::Str("alice".to_string())) + .add_data(DataTypeId(2), DataValue::SignedNumber(42)) + .add_data(DataTypeId(3), DataValue::BoolTrue) + .add_data( + DataTypeId(4), + DataValue::Array(vec![DataValue::SignedNumber(1), DataValue::SignedNumber(2)]), + ); + + let decoded = roundtrip(cv.clone()); + + assert_eq!(decoded.get_id(), 1234); + assert_eq!(decoded.get_sender(), 111); + assert_eq!(decoded.get_receiver(), 222); + assert_eq!(decoded.get_type(), CommTypeId(3)); + assert_eq!( + decoded.get_data(DataTypeId(1)), + &DataValue::Str("alice".to_string()) + ); + assert_eq!( + decoded.get_data(DataTypeId(2)), + &DataValue::SignedNumber(42) + ); + } + + #[test] + fn test_corrupted_length_returns_none() { + let mut bad = vec![0u8; 8]; + // total_length claims more than available + bad[0..4].copy_from_slice(&(1000u32.to_be_bytes())); + assert!(CommunicationValue::from_bytes(&bad).is_none()); + } +} diff --git a/codec/src/data_value.rs b/codec/src/data_value.rs new file mode 100644 index 0000000..f283dd2 --- /dev/null +++ b/codec/src/data_value.rs @@ -0,0 +1,663 @@ +use base64::Engine; +use base64::engine::general_purpose; +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use std::collections::BTreeMap; +use std::hash::{Hash, Hasher}; +use std::io::Cursor; + +use type_map::DataTypeId; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DataKind { + Bool, + + SignedNumber, + UnsignedNumber, + Float, + + Str, + Bytes, + Array(Box), + + Container, + + #[cfg(feature = "crypto")] + EncryptedContainer, + + Null, +} + +#[derive(Debug, Clone, Eq)] +pub enum DataValue { + BoolTrue, + BoolFalse, + Bool(bool), + + SignedNumber(i128), + UnsignedNumber(u128), + Float(u8, u32), + + Str(String), + Bytes(Vec), + Array(Vec), + + Container(Vec<(DataTypeId, DataValue)>), + + #[cfg(feature = "crypto")] + EncryptedContainer(Vec<(DataTypeId, DataValue)>), + + Null, +} + +impl DataValue { + /* + * Container format: + * [2 bytes u16 entry_count] // number of key-value pairs + * [1 byte kind] // DataValue kind marker + * [if kind == BOOL_TRUE or BOOL_FALSE:] + * [1 byte key] // DataTypes discriminant + * [else:] + * [2 bytes u16 payload_len] // length of the value payload + * [1 byte key] // DataTypes discriminant + * [payload_len bytes payload] // value data (interpreted based on kind) + * + * Kind markers: + * 0x01 => BoolTrue + * 0x02 => BoolFalse + * 0x03 => Number (i64, 8 bytes big-endian) + * 0x04 => Str (UTF-8 bytes) + * 0x05 => Array (nested container format) + * 0x06 => Container (nested container format) + * 0x07 => Null + */ + const KIND_BOOL_TRUE: u8 = 0x01; + const KIND_BOOL_FALSE: u8 = 0x02; + + const KIND_SIGNED_NUMBER: u8 = 0x03; + const KIND_UNSIGNED_NUMBER: u8 = 0x04; + const KIND_FLOAT: u8 = 0x05; + + const KIND_STR: u8 = 0x06; + const KIND_BYTES: u8 = 0x07; + const KIND_ARRAY: u8 = 0x08; + + const KIND_CONTAINER: u8 = 0x09; + #[cfg(feature = "crypto")] + const KIND_ENCRYPTED_CONTAINER: u8 = 0x0A; + + const KIND_NULL: u8 = 0x0B; + + pub fn container_from_map(map: &BTreeMap) -> DataValue { + let mut container = Vec::new(); + for (key, value) in map { + container.push((key.clone(), value.clone())); + } + DataValue::Container(container) + } + + pub fn kind(&self) -> DataKind { + match self { + DataValue::Bool(_) | DataValue::BoolTrue | DataValue::BoolFalse => DataKind::Bool, + DataValue::SignedNumber(_) => DataKind::SignedNumber, + DataValue::UnsignedNumber(_) => DataKind::UnsignedNumber, + DataValue::Float(_, _) => DataKind::Float, + DataValue::Str(_) => DataKind::Str, + DataValue::Array(a) => { + if let Some(first) = a.first() { + DataKind::Array(Box::new(first.kind())) + } else { + DataKind::Array(Box::new(DataKind::Null)) + } + } + DataValue::Bytes(_) => DataKind::Bytes, + DataValue::Container(_) => DataKind::Container, + #[cfg(feature = "crypto")] + DataValue::EncryptedContainer(_) => DataKind::EncryptedContainer, + DataValue::Null => DataKind::Null, + } + } + + pub fn as_bool(&self) -> Option { + match self { + DataValue::BoolTrue => Some(true), + DataValue::BoolFalse => Some(false), + DataValue::Bool(v) => Some(*v), + _ => None, + } + } + + pub fn as_str(&self) -> Option<&str> { + match self { + DataValue::Str(s) => Some(s), + _ => None, + } + } + + pub fn as_string(&self) -> Option { + self.as_str().map(|s| s.to_string()) + } + + pub fn as_signed_number(&self) -> Option { + match self { + DataValue::SignedNumber(n) => Some(*n), + _ => None, + } + } + + pub fn as_unsigned_number(&self) -> Option { + match self { + DataValue::UnsignedNumber(n) => Some(*n), + _ => None, + } + } + + pub fn as_float(&self) -> Option<(u8, u32)> { + match self { + DataValue::Float(a, b) => Some((*a, *b)), + _ => None, + } + } + + pub fn as_array(&self) -> Option> { + match self { + DataValue::Array(a) => Some(a.clone()), + _ => None, + } + } + + pub fn as_bytes(&self) -> Option> { + match self { + DataValue::Bytes(b) => Some(b.clone()), + _ => None, + } + } + + pub fn as_container(&self) -> Option> { + match self { + DataValue::Container(c) => Some(c.clone()), + _ => None, + } + } + + #[cfg(feature = "crypto")] + pub fn as_encrypted_container(&self) -> Option> { + match self { + DataValue::EncryptedContainer(c) => Some(c.clone()), + _ => None, + } + } + + pub fn as_map(&self) -> Option> { + match self { + DataValue::Container(c) => { + let mut out = BTreeMap::new(); + for (k, v) in c { + out.insert(k.clone(), v.clone()); + } + Some(out) + } + _ => None, + } + } + + pub fn to_bytes(&self) -> Vec { + match self { + DataValue::Container(entries) => Self::encode_container(entries), + DataValue::Array(arr) => Self::encode_array(arr), + _ => { + let mut out = Vec::new(); + if Self::write_value_payload(&mut out, self).is_none() { + return Vec::new(); + } + out + } + } + } + + pub fn from_bytes(bytes: &[u8]) -> Option { + let mut cursor = Cursor::new(bytes); + let value = Self::read_value(&mut cursor, true)?; + if cursor.position() as usize != bytes.len() { + return None; + } + Some(value) + } + + pub fn to_base64(&self) -> String { + general_purpose::STANDARD.encode(self.to_bytes()) + } + + pub fn from_base64(base64_str: &str) -> Option { + let bytes = general_purpose::STANDARD.decode(base64_str).ok()?; + Self::from_bytes(&bytes) + } + + fn encode_container(entries: &[(DataTypeId, DataValue)]) -> Vec { + let mut out = Vec::new(); + if out + .write_u16::(u16::try_from(entries.len()).ok().unwrap_or(0)) + .is_err() + { + return Vec::new(); + } + + for (key, value) in entries { + if !Self::write_container_entry(&mut out, key.clone(), value) { + return Vec::new(); + } + } + out + } + + fn write_container_entry(buf: &mut Vec, key: DataTypeId, value: &DataValue) -> bool { + let kind = Self::kind_marker(value); + buf.push(kind); + + if kind == Self::KIND_BOOL_TRUE || kind == Self::KIND_BOOL_FALSE { + let key = u8::try_from(key.0).unwrap_or(0); + buf.push(key); + return true; + } + + let mut payload = Vec::new(); + if Self::write_value_payload(&mut payload, value).is_none() { + return false; + } + + let len_u16 = match u16::try_from(payload.len()) { + Ok(v) => v, + Err(_) => return false, + }; + + if buf.write_u16::(len_u16).is_err() { + return false; + } + let key = u8::try_from(key.0).unwrap_or(0); + buf.push(key); + buf.extend_from_slice(&payload); + true + } + + fn encode_array(arr: &[DataValue]) -> Vec { + let mut out = Vec::new(); + if out + .write_u16::(u16::try_from(arr.len()).ok().unwrap_or(0)) + .is_err() + { + return Vec::new(); + } + + for value in arr { + if !Self::write_array_entry(&mut out, value) { + return Vec::new(); + } + } + + out + } + + fn write_array_entry(buf: &mut Vec, value: &DataValue) -> bool { + let kind = Self::kind_marker(value); + buf.push(kind); + + if kind == Self::KIND_BOOL_TRUE || kind == Self::KIND_BOOL_FALSE { + return true; + } + + let mut payload = Vec::new(); + if Self::write_value_payload(&mut payload, value).is_none() { + return false; + } + + let len_u16 = match u16::try_from(payload.len()) { + Ok(v) => v, + Err(_) => return false, + }; + + if buf.write_u16::(len_u16).is_err() { + return false; + } + buf.extend_from_slice(&payload); + true + } + + fn write_value_payload(buf: &mut Vec, value: &DataValue) -> Option<()> { + match value { + DataValue::BoolTrue => Some(()), + DataValue::BoolFalse => Some(()), + DataValue::Bool(v) => { + if *v { + Some(()) + } else { + Some(()) + } + } + DataValue::SignedNumber(n) => { + buf.write_i128::(*n).ok()?; + Some(()) + } + DataValue::UnsignedNumber(n) => { + buf.write_u128::(*n).ok()?; + Some(()) + } + DataValue::Float(a, b) => { + buf.write_u8(*a).ok()?; + buf.write_u32::(*b).ok()?; + Some(()) + } + DataValue::Str(s) => { + buf.extend_from_slice(s.as_bytes()); + Some(()) + } + DataValue::Array(arr) => { + let bytes = Self::encode_array(arr); + buf.extend_from_slice(&bytes); + Some(()) + } + DataValue::Bytes(b) => { + buf.extend_from_slice(b); + Some(()) + } + DataValue::Container(entries) => { + let bytes = Self::encode_container(entries); + buf.extend_from_slice(&bytes); + Some(()) + } + #[cfg(feature = "crypto")] + DataValue::EncryptedContainer(entries) => { + let bytes = Self::encode_container(entries); + buf.extend_from_slice(&bytes); + Some(()) + } + + DataValue::Null => Some(()), + } + } + + fn read_value(cursor: &mut Cursor<&[u8]>, top_level: bool) -> Option { + if top_level { + let start = cursor.position() as usize; + let remaining = cursor.get_ref().len().checked_sub(start)?; + if remaining < 2 { + return None; + } + + let snapshot = cursor.clone(); + if let Some(container) = Self::try_read_container(cursor) { + return Some(container); + } + *cursor = snapshot; + + let array = Self::read_array(cursor)?; + return Some(array); + } + + let kind = cursor.read_u8().ok()?; + Self::read_value_by_kind(cursor, kind, None) + } + + fn try_read_container(cursor: &mut Cursor<&[u8]>) -> Option { + let count = cursor.read_u16::().ok()? as usize; + let mut entries = Vec::with_capacity(count); + + for _ in 0..count { + let kind = cursor.read_u8().ok()?; + + if kind == Self::KIND_BOOL_TRUE || kind == Self::KIND_BOOL_FALSE { + let key = DataTypeId(cursor.read_u16::().ok()?); + let value = if kind == Self::KIND_BOOL_TRUE { + DataValue::BoolTrue + } else { + DataValue::BoolFalse + }; + entries.push((key, value)); + continue; + } + + let len = cursor.read_u16::().ok()? as usize; + let key = DataTypeId(cursor.read_u16::().ok()?); + + let start = cursor.position() as usize; + let end = start.checked_add(len)?; + if end > cursor.get_ref().len() { + return None; + } + + let payload = &cursor.get_ref()[start..end]; + let mut inner = Cursor::new(payload); + let value = Self::read_value_by_kind(&mut inner, kind, Some(len))?; + if inner.position() as usize != len { + return None; + } + + cursor.set_position(end as u64); + entries.push((key, value)); + } + + Some(DataValue::Container(entries)) + } + + fn read_array(cursor: &mut Cursor<&[u8]>) -> Option { + let count = cursor.read_u16::().ok()? as usize; + let mut out = Vec::with_capacity(count); + + for _ in 0..count { + let kind = cursor.read_u8().ok()?; + + if kind == Self::KIND_BOOL_TRUE { + out.push(DataValue::BoolTrue); + continue; + } + if kind == Self::KIND_BOOL_FALSE { + out.push(DataValue::BoolFalse); + continue; + } + + let len = cursor.read_u16::().ok()? as usize; + let start = cursor.position() as usize; + let end = start.checked_add(len)?; + if end > cursor.get_ref().len() { + return None; + } + + let payload = &cursor.get_ref()[start..end]; + let mut inner = Cursor::new(payload); + let value = Self::read_value_by_kind(&mut inner, kind, Some(len))?; + if inner.position() as usize != len { + return None; + } + + cursor.set_position(end as u64); + out.push(value); + } + + Some(DataValue::Array(out)) + } + + fn read_value_by_kind( + cursor: &mut Cursor<&[u8]>, + kind: u8, + payload_len: Option, + ) -> Option { + match kind { + Self::KIND_BOOL_TRUE => Some(DataValue::BoolTrue), + Self::KIND_BOOL_FALSE => Some(DataValue::BoolFalse), + Self::KIND_SIGNED_NUMBER => Some(DataValue::SignedNumber( + cursor.read_i128::().ok()?, + )), + Self::KIND_UNSIGNED_NUMBER => Some(DataValue::UnsignedNumber( + cursor.read_u128::().ok()?, + )), + Self::KIND_FLOAT => { + let a = cursor.read_u8().ok()?; + let b = cursor.read_u32::().ok()?; + Some(DataValue::Float(a, b)) + } + Self::KIND_STR => { + let len = payload_len?; + let start = cursor.position() as usize; + let end = start.checked_add(len)?; + if end > cursor.get_ref().len() { + return None; + } + let s = std::str::from_utf8(&cursor.get_ref()[start..end]) + .ok()? + .to_string(); + cursor.set_position(end as u64); + Some(DataValue::Str(s)) + } + Self::KIND_ARRAY => { + let start = cursor.position() as usize; + let len = payload_len?; + let end = start.checked_add(len)?; + if end > cursor.get_ref().len() { + return None; + } + let mut inner = Cursor::new(&cursor.get_ref()[start..end]); + let arr = Self::read_array(&mut inner)?; + if inner.position() as usize != len { + return None; + } + cursor.set_position(end as u64); + Some(arr) + } + Self::KIND_CONTAINER => { + let start = cursor.position() as usize; + let len = payload_len?; + let end = start.checked_add(len)?; + if end > cursor.get_ref().len() { + return None; + } + let mut inner = Cursor::new(&cursor.get_ref()[start..end]); + let c = Self::try_read_container(&mut inner)?; + if inner.position() as usize != len { + return None; + } + cursor.set_position(end as u64); + Some(c) + } + #[cfg(feature = "crypto")] + Self::KIND_ENCRYPTED_CONTAINER => { + let start = cursor.position() as usize; + let len = payload_len?; + let end = start.checked_add(len)?; + if end > cursor.get_ref().len() { + return None; + } + let mut inner = Cursor::new(&cursor.get_ref()[start..end]); + let c = Self::try_read_container(&mut inner)?; + if inner.position() as usize != len { + return None; + } + cursor.set_position(end as u64); + if let DataValue::Container(entries) = c { + Some(DataValue::EncryptedContainer(entries)) + } else { + None + } + } + Self::KIND_NULL => Some(DataValue::Null), + _ => None, + } + } + + fn kind_marker(value: &DataValue) -> u8 { + match value { + DataValue::BoolTrue => Self::KIND_BOOL_TRUE, + DataValue::BoolFalse => Self::KIND_BOOL_FALSE, + DataValue::Bool(v) => { + if *v { + Self::KIND_BOOL_TRUE + } else { + Self::KIND_BOOL_FALSE + } + } + DataValue::SignedNumber(_) => Self::KIND_SIGNED_NUMBER, + DataValue::UnsignedNumber(_) => Self::KIND_UNSIGNED_NUMBER, + DataValue::Float(_, _) => Self::KIND_FLOAT, + DataValue::Str(_) => Self::KIND_STR, + DataValue::Array(_) => Self::KIND_ARRAY, + DataValue::Bytes(_) => Self::KIND_BYTES, + DataValue::Container(_) => Self::KIND_CONTAINER, + #[cfg(feature = "crypto")] + DataValue::EncryptedContainer(_) => Self::KIND_ENCRYPTED_CONTAINER, + DataValue::Null => Self::KIND_NULL, + } + } +} + +impl PartialEq for DataValue { + fn eq(&self, other: &Self) -> bool { + use DataValue::*; + match (self, other) { + (BoolTrue, BoolTrue) | (BoolFalse, BoolFalse) => true, + (BoolTrue, Bool(true)) | (Bool(true), BoolTrue) => true, + (BoolFalse, Bool(false)) | (Bool(false), BoolFalse) => true, + (Bool(a), Bool(b)) => a == b, + (SignedNumber(a), SignedNumber(b)) => a == b, + (UnsignedNumber(a), UnsignedNumber(b)) => a == b, + (Float(a, b), Float(c, d)) => a == c && b == d, + (Str(a), Str(b)) => a == b, + (Array(a), Array(b)) => a == b, + (Bytes(a), Bytes(b)) => a == b, + (Container(a), Container(b)) => a == b, + #[cfg(feature = "crypto")] + (EncryptedContainer(a), EncryptedContainer(b)) => a == b, + (Null, Null) => true, + _ => false, + } + } +} + +impl Hash for DataValue { + fn hash(&self, state: &mut H) { + use DataValue::*; + match self { + BoolTrue | Bool(true) => { + 0u8.hash(state); + true.hash(state); + } + BoolFalse | Bool(false) => { + 0u8.hash(state); + false.hash(state); + } + SignedNumber(n) => { + 1u8.hash(state); + n.hash(state); + } + UnsignedNumber(n) => { + 2u8.hash(state); + n.hash(state); + } + Float(n, m) => { + 3u8.hash(state); + n.hash(state); + m.hash(state); + } + Str(s) => { + 2u8.hash(state); + s.hash(state); + } + Array(a) => { + 3u8.hash(state); + a.hash(state); + } + Bytes(a) => { + 4u8.hash(state); + a.hash(state); + } + Container(c) => { + 5u8.hash(state); + c.hash(state); + } + #[cfg(feature = "crypto")] + EncryptedContainer(c) => { + 6u8.hash(state); + c.hash(state); + } + Null => { + 6u8.hash(state); + } + } + } +} diff --git a/codec/src/lib.rs b/codec/src/lib.rs new file mode 100644 index 0000000..071dbe6 --- /dev/null +++ b/codec/src/lib.rs @@ -0,0 +1,26 @@ +pub mod communication_types; +pub mod communication_value; +pub mod data_value; +pub mod util; + +pub use communication_value::CommunicationValue; +pub use data_value::{DataKind, DataValue}; + +pub use util::rand_u32; + +pub use type_map::{CommTypeId, DataTypeId, TypeMap, Version}; + +use common::CodecError; + +pub fn encode(_value: &DataValue, _typemap: &TypeMap) -> Result, CodecError> { + // write header using typemap.data_id(), serialize value + todo!() +} + +pub fn decode(_bytes: &[u8], _typemap: &TypeMap) -> Result { + // read header, look up type names from typemap, build DataValue + todo!() +} + +#[cfg(feature = "registry")] +pub mod registry; diff --git a/codec/src/registry.rs b/codec/src/registry.rs new file mode 100644 index 0000000..d7f5ae9 --- /dev/null +++ b/codec/src/registry.rs @@ -0,0 +1,67 @@ +use crate::{CodecError, DataValue, decode, encode}; +use std::collections::BTreeMap; +use type_map::{TypeMap, Version}; + +#[derive(Clone, Debug)] +pub struct Registry { + versions: BTreeMap, +} + +impl Registry { + pub fn new() -> Self { + Self { + versions: BTreeMap::new(), + } + } + + pub fn register(&mut self, typemap: TypeMap) { + self.versions.insert(typemap.version.clone(), typemap); + } + + pub fn get(&self, version: Version) -> Option<&TypeMap> { + self.versions.get(&version) + } + + pub fn negotiate(&self, client_versions: &[Version]) -> Option { + client_versions + .iter() + .filter(|v| self.versions.contains_key(v)) + .max() + .cloned() + } + + pub fn latest(&self) -> Option<&TypeMap> { + self.versions.last_key_value().map(|(_, v)| v) + } +} + +#[derive(Clone, Debug)] +pub struct VersionedCodec { + registry: Registry, +} + +impl VersionedCodec { + pub fn new(registry: Registry) -> Self { + Self { registry } + } + + pub fn encode(&self, value: &DataValue, version: Version) -> Result, CodecError> { + let typemap = self + .registry + .get(version) + .ok_or(CodecError::UnknownVersion)?; + encode(value, typemap) + } + + pub fn decode(&self, bytes: &[u8], version: Version) -> Result { + let typemap = self + .registry + .get(version) + .ok_or(CodecError::UnknownVersion)?; + decode(bytes, typemap) + } + + pub fn negotiate(&self, client_versions: &[Version]) -> Option { + self.registry.negotiate(client_versions) + } +} diff --git a/codec/src/util.rs b/codec/src/util.rs new file mode 100644 index 0000000..db90014 --- /dev/null +++ b/codec/src/util.rs @@ -0,0 +1,9 @@ +use rand::random; + +pub fn rand_u32() -> u32 { + random::() +} + +pub fn rand_u64() -> u64 { + random::() +} diff --git a/common/Cargo.toml b/common/Cargo.toml new file mode 100644 index 0000000..fa02773 --- /dev/null +++ b/common/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "common" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/common/src/lib.rs b/common/src/lib.rs new file mode 100644 index 0000000..0b3c97b --- /dev/null +++ b/common/src/lib.rs @@ -0,0 +1,12 @@ +pub enum RegistryError { + ReservedCommId(u8, String), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum CodecError { + UnknownVersion, + UnknownCommunicationType(String), + UnknownDataType(String), + ReservedCommunicationType(u8), + InvalidEncoding, +} diff --git a/crypto/Cargo.toml b/crypto/Cargo.toml new file mode 100644 index 0000000..d882c57 --- /dev/null +++ b/crypto/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "mtp-crypto" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/crypto/src/lib.rs b/crypto/src/lib.rs new file mode 100644 index 0000000..b93cf3f --- /dev/null +++ b/crypto/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/host/Cargo.toml b/host/Cargo.toml new file mode 100644 index 0000000..b35bfb6 --- /dev/null +++ b/host/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "host" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/host/src/lib.rs b/host/src/lib.rs new file mode 100644 index 0000000..b93cf3f --- /dev/null +++ b/host/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/registry/Cargo.toml b/registry/Cargo.toml new file mode 100644 index 0000000..20e847b --- /dev/null +++ b/registry/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "registry" +version = "0.1.0" +edition = "2024" + +[dependencies] +common = { path = "../common" } diff --git a/registry/src/lib.rs b/registry/src/lib.rs new file mode 100644 index 0000000..aef951f --- /dev/null +++ b/registry/src/lib.rs @@ -0,0 +1,94 @@ +use common::RegistryError; +use std::collections::HashMap; + +#[repr(transparent)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct CommTypeId(pub u8); + +#[repr(transparent)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct DataTypeId(pub u16); + +/* + * # Reserved Internal Namespace + * These are NEVER assigned by a RegistryConfig. They are fixed + * across all versions for version negotiation & security. + */ + +pub const INTERNAL_COMM_RESERVED: std::ops::Range = 0..16; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum InternalCommType { + VersionNegotiate = 0, + SecurityHandshake = 1, + RegistrySync = 2, + // 3-15 reserved for future internal use +} + +impl InternalCommType { + pub fn as_id(self) -> CommTypeId { + CommTypeId(self as u8) + } +} + +#[derive(Clone, Debug)] +// (major , minor , patch ); +pub struct Version(pub u16, pub u16, pub u16); + +pub struct Registry { + pub version: Version, + comm_name_to_id: HashMap, + comm_id_to_name: HashMap, + data_name_to_id: HashMap, + data_id_to_name: HashMap, +} + +pub struct RegistryConfig { + pub version: Version, + pub communication_types: HashMap, + pub data_types: HashMap, +} + +impl Registry { + pub fn from_config(cfg: RegistryConfig) -> Result { + for (name, &id) in &cfg.communication_types { + if INTERNAL_COMM_RESERVED.contains(&id) { + return Err(RegistryError::ReservedCommId(id, name.clone())); + } + } + + let mut comm_id_to_name = HashMap::with_capacity(cfg.communication_types.len()); + for (name, id) in &cfg.communication_types { + comm_id_to_name.insert(*id, name.clone()); + } + + let mut data_id_to_name = HashMap::with_capacity(cfg.data_types.len()); + for (name, id) in &cfg.data_types { + data_id_to_name.insert(*id, name.clone()); + } + + Ok(Self { + version: cfg.version, + comm_name_to_id: cfg.communication_types, + comm_id_to_name, + data_name_to_id: cfg.data_types, + data_id_to_name, + }) + } + + pub fn resolve_comm(&self, name: &str) -> Option { + self.comm_name_to_id.get(name).copied().map(CommTypeId) + } + + pub fn parse_comm(&self, id: CommTypeId) -> Option<&str> { + self.comm_id_to_name.get(&id.0).map(|s| s.as_str()) + } + + pub fn resolve_data(&self, name: &str) -> Option { + self.data_name_to_id.get(name).copied().map(DataTypeId) + } + + pub fn parse_data(&self, id: DataTypeId) -> Option<&str> { + self.data_id_to_name.get(&id.0).map(|s| s.as_str()) + } +} diff --git a/transport/Cargo.toml b/transport/Cargo.toml new file mode 100644 index 0000000..4ac083f --- /dev/null +++ b/transport/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "transport" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/transport/src/lib.rs b/transport/src/lib.rs new file mode 100644 index 0000000..b93cf3f --- /dev/null +++ b/transport/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/type-map/Cargo.toml b/type-map/Cargo.toml new file mode 100644 index 0000000..66f916d --- /dev/null +++ b/type-map/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "type-map" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/type-map/src/lib.rs b/type-map/src/lib.rs new file mode 100644 index 0000000..ddba981 --- /dev/null +++ b/type-map/src/lib.rs @@ -0,0 +1,28 @@ +use std::collections::HashMap; + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct CommTypeId(pub u8); + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct DataTypeId(pub u16); + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Version(pub u16); + +/// A single protocol version's type dictionary. +/// Compiled into the client, or loaded by the host via the registry. +#[derive(Clone, Debug)] +pub struct TypeMap { + pub version: Version, + pub comm_types: HashMap, + pub data_types: HashMap, +} + +impl TypeMap { + pub fn comm_id(&self, name: &str) -> Option { + self.comm_types.get(name).copied() + } + pub fn data_id(&self, name: &str) -> Option { + self.data_types.get(name).copied() + } +}