commit 3c1f63f9cc44f9e71d330e7ed29bf09a377419e8
parent 37b1a1b8c2856ee0886bdae2e22c453ca9ddb766
Author: triesap <tyson@radroots.org>
Date: Sun, 22 Mar 2026 00:32:53 +0000
simplex: add simplex-smp-proto crate
- add `radroots-simplex-smp-proto` to the workspace and lockfile
- implement official queue uri parsing with client and transport version constants
- add version-aware core SMP codecs for current and v9 `NEW` and `IDS` flows plus transmissions
- cover std and no_std builds with deterministic round-trip tests
Diffstat:
8 files changed, 1993 insertions(+), 0 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -2395,6 +2395,13 @@ dependencies = [
]
[[package]]
+name = "radroots-simplex-smp-proto"
+version = "0.1.0-alpha.1"
+dependencies = [
+ "base64 0.22.1",
+]
+
+[[package]]
name = "radroots-sql-core"
version = "0.1.0-alpha.1"
dependencies = [
diff --git a/Cargo.toml b/Cargo.toml
@@ -17,6 +17,7 @@ members = [
"crates/nostr-ndb",
"crates/nostr-runtime",
"crates/runtime",
+ "crates/simplex-smp-proto",
"crates/sql-wasm-bridge",
"crates/sql-wasm-core",
"crates/sql-core",
@@ -57,6 +58,7 @@ radroots-log = { path = "crates/log", version = "0.1.0-alpha.1", default-feature
radroots-net = { path = "crates/net", version = "0.1.0-alpha.1", default-features = false }
radroots-net-core = { path = "crates/net-core", version = "0.1.0-alpha.1", default-features = false }
radroots-nostr-runtime = { path = "crates/nostr-runtime", version = "0.1.0-alpha.1", default-features = false }
+radroots-simplex-smp-proto = { path = "crates/simplex-smp-proto", version = "0.1.0-alpha.1", default-features = false }
radroots-sql-wasm-bridge = { path = "crates/sql-wasm-bridge", version = "0.1.0-alpha.1" }
radroots-sql-wasm-core = { path = "crates/sql-wasm-core", version = "0.1.0-alpha.1", default-features = false }
radroots-sql-core = { path = "crates/sql-core", version = "0.1.0-alpha.1" }
diff --git a/crates/simplex-smp-proto/Cargo.toml b/crates/simplex-smp-proto/Cargo.toml
@@ -0,0 +1,21 @@
+[package]
+name = "radroots-simplex-smp-proto"
+version = "0.1.0-alpha.1"
+edition.workspace = true
+authors = [
+ "Radroots Authors",
+]
+rust-version.workspace = true
+license.workspace = true
+description = "simplex messaging protocol primitives for the radroots sdk"
+repository.workspace = true
+homepage.workspace = true
+documentation = "https://docs.rs/radroots-simplex-smp-proto"
+readme.workspace = true
+
+[features]
+default = ["std"]
+std = []
+
+[dependencies]
+base64 = { version = "0.22", default-features = false, features = ["alloc"] }
diff --git a/crates/simplex-smp-proto/src/error.rs b/crates/simplex-smp-proto/src/error.rs
@@ -0,0 +1,80 @@
+#[cfg(not(feature = "std"))]
+use alloc::string::String;
+use core::fmt;
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum RadrootsSimplexSmpProtoError {
+ UnexpectedEof,
+
+ InvalidTag(String),
+
+ UnsupportedTag(String),
+
+ InvalidUtf8(String),
+
+ InvalidBase64Url { field: &'static str, value: String },
+
+ InvalidVersionRange(String),
+
+ InvalidUri(String),
+
+ InvalidHostList(String),
+
+ InvalidPort(String),
+
+ InvalidShortFieldLength(usize),
+
+ InvalidLargeFieldLength(usize),
+
+ InvalidCorrelationIdLength(usize),
+
+ InvalidNonceLength(usize),
+
+ InvalidMaybeTag(u8),
+
+ InvalidBoolEncoding(u8),
+
+ MissingField(&'static str),
+
+ TrailingBytes,
+
+ UnsupportedTransportVersion(u16),
+}
+
+impl fmt::Display for RadrootsSimplexSmpProtoError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::UnexpectedEof => write!(f, "unexpected end of SMP input"),
+ Self::InvalidTag(tag) => write!(f, "invalid SMP ASCII tag `{tag}`"),
+ Self::UnsupportedTag(tag) => write!(f, "unsupported SMP tag `{tag}`"),
+ Self::InvalidUtf8(error) => write!(f, "invalid UTF-8 in SMP field: {error}"),
+ Self::InvalidBase64Url { field, value } => {
+ write!(f, "invalid base64url value for `{field}`: `{value}`")
+ }
+ Self::InvalidVersionRange(range) => write!(f, "invalid SMP version range `{range}`"),
+ Self::InvalidUri(uri) => write!(f, "invalid SMP URI: {uri}"),
+ Self::InvalidHostList(hosts) => write!(f, "invalid SMP host list `{hosts}`"),
+ Self::InvalidPort(port) => write!(f, "invalid SMP port `{port}`"),
+ Self::InvalidShortFieldLength(length) => {
+ write!(f, "invalid SMP short field length {length}")
+ }
+ Self::InvalidLargeFieldLength(length) => {
+ write!(f, "invalid SMP large field length {length}")
+ }
+ Self::InvalidCorrelationIdLength(length) => {
+ write!(f, "invalid SMP correlation id length {length}")
+ }
+ Self::InvalidNonceLength(length) => write!(f, "invalid SMP nonce length {length}"),
+ Self::InvalidMaybeTag(tag) => write!(f, "invalid SMP maybe tag `{tag}`"),
+ Self::InvalidBoolEncoding(value) => write!(f, "invalid SMP bool encoding `{value}`"),
+ Self::MissingField(field) => write!(f, "missing required SMP field `{field}`"),
+ Self::TrailingBytes => write!(f, "trailing SMP bytes after parse"),
+ Self::UnsupportedTransportVersion(version) => {
+ write!(f, "unsupported SMP transport version {version}")
+ }
+ }
+ }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for RadrootsSimplexSmpProtoError {}
diff --git a/crates/simplex-smp-proto/src/lib.rs b/crates/simplex-smp-proto/src/lib.rs
@@ -0,0 +1,44 @@
+#![cfg_attr(not(feature = "std"), no_std)]
+#![forbid(unsafe_code)]
+
+extern crate alloc;
+
+pub mod error;
+pub mod uri;
+pub mod version;
+pub mod wire;
+
+pub mod prelude {
+ pub use crate::error::RadrootsSimplexSmpProtoError;
+ pub use crate::uri::{
+ RADROOTS_SIMPLEX_SMP_DEFAULT_PORT, RADROOTS_SIMPLEX_SMP_URI_SCHEME,
+ RadrootsSimplexSmpQueueMode, RadrootsSimplexSmpQueueUri, RadrootsSimplexSmpServerAddress,
+ };
+ pub use crate::version::{
+ RADROOTS_SIMPLEX_SMP_BLOCKED_ENTITY_TRANSPORT_VERSION,
+ RADROOTS_SIMPLEX_SMP_CURRENT_CLIENT_VERSION,
+ RADROOTS_SIMPLEX_SMP_CURRENT_TRANSPORT_VERSION,
+ RADROOTS_SIMPLEX_SMP_DELETED_EVENT_TRANSPORT_VERSION,
+ RADROOTS_SIMPLEX_SMP_INITIAL_CLIENT_VERSION,
+ RADROOTS_SIMPLEX_SMP_INITIAL_TRANSPORT_VERSION,
+ RADROOTS_SIMPLEX_SMP_NEW_NOTIFIER_CREDENTIALS_TRANSPORT_VERSION,
+ RADROOTS_SIMPLEX_SMP_SENDER_AUTH_KEY_CLIENT_VERSION,
+ RADROOTS_SIMPLEX_SMP_SENDER_AUTH_KEY_TRANSPORT_VERSION,
+ RADROOTS_SIMPLEX_SMP_SERVER_HOSTNAMES_CLIENT_VERSION,
+ RADROOTS_SIMPLEX_SMP_SERVICE_CERTS_TRANSPORT_VERSION,
+ RADROOTS_SIMPLEX_SMP_SHORT_LINKS_CLIENT_VERSION,
+ RADROOTS_SIMPLEX_SMP_SHORT_LINKS_TRANSPORT_VERSION, RadrootsSimplexSmpVersionRange,
+ };
+ pub use crate::wire::{
+ RadrootsSimplexSmpBrokerMessage, RadrootsSimplexSmpBrokerTransmission,
+ RadrootsSimplexSmpCommand, RadrootsSimplexSmpCommandError,
+ RadrootsSimplexSmpCommandTransmission, RadrootsSimplexSmpContactQueueRequest,
+ RadrootsSimplexSmpCorrelationId, RadrootsSimplexSmpError, RadrootsSimplexSmpMessageFlags,
+ RadrootsSimplexSmpMessagingQueueRequest, RadrootsSimplexSmpNewNotifierCredentials,
+ RadrootsSimplexSmpNewQueueRequest, RadrootsSimplexSmpNotifierIdsResponse,
+ RadrootsSimplexSmpQueueIdsResponse, RadrootsSimplexSmpQueueLinkData,
+ RadrootsSimplexSmpQueueRequestData, RadrootsSimplexSmpReceivedMessage,
+ RadrootsSimplexSmpSendCommand, RadrootsSimplexSmpServerNotifierCredentials,
+ RadrootsSimplexSmpSubscriptionMode,
+ };
+}
diff --git a/crates/simplex-smp-proto/src/uri.rs b/crates/simplex-smp-proto/src/uri.rs
@@ -0,0 +1,346 @@
+use crate::error::RadrootsSimplexSmpProtoError;
+use crate::version::{
+ RADROOTS_SIMPLEX_SMP_SERVER_HOSTNAMES_CLIENT_VERSION,
+ RADROOTS_SIMPLEX_SMP_SHORT_LINKS_CLIENT_VERSION, RadrootsSimplexSmpVersionRange,
+};
+use alloc::string::{String, ToString};
+use alloc::vec::Vec;
+use base64::Engine as _;
+use core::fmt;
+use core::str::FromStr;
+
+pub const RADROOTS_SIMPLEX_SMP_URI_SCHEME: &str = "smp";
+pub const RADROOTS_SIMPLEX_SMP_DEFAULT_PORT: u16 = 5223;
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RadrootsSimplexSmpServerAddress {
+ pub server_identity: String,
+ pub hosts: Vec<String>,
+ pub port: Option<u16>,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum RadrootsSimplexSmpQueueMode {
+ Messaging,
+ Contact,
+}
+
+impl RadrootsSimplexSmpQueueMode {
+ const fn as_query_value(self) -> &'static str {
+ match self {
+ Self::Messaging => "m",
+ Self::Contact => "c",
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RadrootsSimplexSmpQueueUri {
+ pub server: RadrootsSimplexSmpServerAddress,
+ pub sender_id: String,
+ pub version_range: RadrootsSimplexSmpVersionRange,
+ pub recipient_dh_public_key: String,
+ pub queue_mode: Option<RadrootsSimplexSmpQueueMode>,
+}
+
+impl RadrootsSimplexSmpQueueUri {
+ pub const fn sender_can_secure(&self) -> bool {
+ matches!(
+ self.queue_mode,
+ Some(RadrootsSimplexSmpQueueMode::Messaging)
+ )
+ }
+
+ pub fn parse(value: &str) -> Result<Self, RadrootsSimplexSmpProtoError> {
+ let without_scheme = value
+ .strip_prefix("smp://")
+ .ok_or_else(|| RadrootsSimplexSmpProtoError::InvalidUri(value.to_string()))?;
+ let (authority, sender_and_fragment) = without_scheme
+ .split_once('/')
+ .ok_or_else(|| RadrootsSimplexSmpProtoError::InvalidUri(value.to_string()))?;
+ let server = parse_server_address(authority)?;
+ let (sender_id, fragment) = sender_and_fragment
+ .split_once('#')
+ .ok_or_else(|| RadrootsSimplexSmpProtoError::InvalidUri(value.to_string()))?;
+ let sender_id = sender_id.strip_suffix('/').unwrap_or(sender_id).to_string();
+ validate_base64_url("sender_id", &sender_id)?;
+ let (fragment_dh_public_key, query) = parse_fragment_query(fragment, value)?;
+
+ let mut version_range: Option<RadrootsSimplexSmpVersionRange> = None;
+ let mut recipient_dh_public_key: Option<String> = fragment_dh_public_key;
+ let mut queue_mode: Option<RadrootsSimplexSmpQueueMode> = None;
+ let mut extra_hosts: Option<Vec<String>> = None;
+
+ for pair in query.split('&') {
+ if pair.is_empty() {
+ continue;
+ }
+
+ let (key, raw_value) = pair
+ .split_once('=')
+ .ok_or_else(|| RadrootsSimplexSmpProtoError::InvalidUri(value.to_string()))?;
+
+ match key {
+ "v" => {
+ version_range = Some(raw_value.parse()?);
+ }
+ "dh" => {
+ validate_base64_url("recipient_dh_public_key", raw_value)?;
+ if recipient_dh_public_key
+ .replace(raw_value.to_string())
+ .is_some()
+ {
+ return Err(RadrootsSimplexSmpProtoError::InvalidUri(value.to_string()));
+ }
+ }
+ "q" => {
+ let next_mode = match raw_value {
+ "m" => RadrootsSimplexSmpQueueMode::Messaging,
+ "c" => RadrootsSimplexSmpQueueMode::Contact,
+ _ => {
+ return Err(RadrootsSimplexSmpProtoError::InvalidUri(
+ value.to_string(),
+ ));
+ }
+ };
+ if queue_mode.replace(next_mode).is_some() {
+ return Err(RadrootsSimplexSmpProtoError::InvalidUri(value.to_string()));
+ }
+ }
+ "k" if raw_value == "s" => {
+ if queue_mode
+ .replace(RadrootsSimplexSmpQueueMode::Messaging)
+ .is_some()
+ {
+ return Err(RadrootsSimplexSmpProtoError::InvalidUri(value.to_string()));
+ }
+ }
+ "srv" => {
+ if extra_hosts
+ .replace(parse_host_list(raw_value, value)?)
+ .is_some()
+ {
+ return Err(RadrootsSimplexSmpProtoError::InvalidUri(value.to_string()));
+ }
+ }
+ _ => {
+ return Err(RadrootsSimplexSmpProtoError::InvalidUri(value.to_string()));
+ }
+ }
+ }
+
+ let mut server = server;
+ if let Some(hosts) = extra_hosts {
+ server.hosts.extend(hosts);
+ }
+
+ Ok(Self {
+ server,
+ sender_id,
+ version_range: version_range
+ .ok_or(RadrootsSimplexSmpProtoError::MissingField("version_range"))?,
+ recipient_dh_public_key: recipient_dh_public_key.ok_or(
+ RadrootsSimplexSmpProtoError::MissingField("recipient_dh_public_key"),
+ )?,
+ queue_mode,
+ })
+ }
+}
+
+impl fmt::Display for RadrootsSimplexSmpQueueUri {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let authority_hosts =
+ if self.version_range.min >= RADROOTS_SIMPLEX_SMP_SERVER_HOSTNAMES_CLIENT_VERSION {
+ self.server.hosts.join(",")
+ } else {
+ self.server.hosts.first().cloned().ok_or(fmt::Error)?
+ };
+ write!(
+ f,
+ "{RADROOTS_SIMPLEX_SMP_URI_SCHEME}://{}@{}",
+ self.server.server_identity, authority_hosts,
+ )?;
+ if let Some(port) = self.server.port {
+ write!(f, ":{port}")?;
+ }
+ write!(f, "/{}#/?v={}", self.sender_id, self.version_range)?;
+ write!(f, "&dh={}", self.recipient_dh_public_key)?;
+ if self.version_range.min >= RADROOTS_SIMPLEX_SMP_SHORT_LINKS_CLIENT_VERSION {
+ if let Some(queue_mode) = self.queue_mode {
+ write!(f, "&q={}", queue_mode.as_query_value())?;
+ }
+ } else if self.sender_can_secure() {
+ write!(f, "&k=s")?;
+ }
+ if self.version_range.min < RADROOTS_SIMPLEX_SMP_SERVER_HOSTNAMES_CLIENT_VERSION
+ && self.server.hosts.len() > 1
+ {
+ write!(f, "&srv={}", self.server.hosts[1..].join(","))?;
+ }
+ Ok(())
+ }
+}
+
+impl FromStr for RadrootsSimplexSmpQueueUri {
+ type Err = RadrootsSimplexSmpProtoError;
+
+ fn from_str(value: &str) -> Result<Self, Self::Err> {
+ Self::parse(value)
+ }
+}
+
+fn parse_server_address(
+ authority: &str,
+) -> Result<RadrootsSimplexSmpServerAddress, RadrootsSimplexSmpProtoError> {
+ let (server_identity, host_part) = authority
+ .split_once('@')
+ .ok_or_else(|| RadrootsSimplexSmpProtoError::InvalidUri(authority.to_string()))?;
+ validate_base64_url("server_identity", server_identity)?;
+
+ let (hosts_raw, port) = match host_part.rsplit_once(':') {
+ Some((hosts, port)) if port.chars().all(|ch| ch.is_ascii_digit()) => {
+ let port = port
+ .parse::<u16>()
+ .map_err(|_| RadrootsSimplexSmpProtoError::InvalidPort(port.to_string()))?;
+ (hosts, Some(port))
+ }
+ _ => (host_part, None),
+ };
+
+ if hosts_raw.is_empty() {
+ return Err(RadrootsSimplexSmpProtoError::InvalidHostList(
+ hosts_raw.to_string(),
+ ));
+ }
+
+ let hosts = hosts_raw
+ .split(',')
+ .map(|host| host.trim().to_string())
+ .collect::<Vec<_>>();
+ if hosts.iter().any(|host| host.is_empty()) {
+ return Err(RadrootsSimplexSmpProtoError::InvalidHostList(
+ hosts_raw.to_string(),
+ ));
+ }
+
+ Ok(RadrootsSimplexSmpServerAddress {
+ server_identity: server_identity.to_string(),
+ hosts,
+ port,
+ })
+}
+
+fn parse_fragment_query<'a>(
+ fragment: &'a str,
+ original: &str,
+) -> Result<(Option<String>, &'a str), RadrootsSimplexSmpProtoError> {
+ let fragment = fragment.strip_prefix('/').unwrap_or(fragment);
+ if let Some(query) = fragment.strip_prefix('?') {
+ return Ok((None, query));
+ }
+ if let Some((dh_public_key, query)) = fragment.split_once("/?") {
+ validate_base64_url("recipient_dh_public_key", dh_public_key)?;
+ return Ok((Some(dh_public_key.to_string()), query));
+ }
+ if let Some((dh_public_key, query)) = fragment.split_once('?') {
+ validate_base64_url("recipient_dh_public_key", dh_public_key)?;
+ return Ok((Some(dh_public_key.to_string()), query));
+ }
+ Err(RadrootsSimplexSmpProtoError::InvalidUri(
+ original.to_string(),
+ ))
+}
+
+fn parse_host_list(
+ value: &str,
+ original: &str,
+) -> Result<Vec<String>, RadrootsSimplexSmpProtoError> {
+ let hosts = value
+ .split(',')
+ .map(|host| host.trim().to_string())
+ .collect::<Vec<_>>();
+ if hosts.is_empty() || hosts.iter().any(|host| host.is_empty()) {
+ return Err(RadrootsSimplexSmpProtoError::InvalidHostList(
+ original.to_string(),
+ ));
+ }
+ Ok(hosts)
+}
+
+fn validate_base64_url(
+ field: &'static str,
+ value: &str,
+) -> Result<(), RadrootsSimplexSmpProtoError> {
+ base64::engine::general_purpose::URL_SAFE_NO_PAD
+ .decode(value)
+ .map(|_| ())
+ .map_err(|_| RadrootsSimplexSmpProtoError::InvalidBase64Url {
+ field,
+ value: value.to_string(),
+ })
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn parses_and_formats_queue_uri() {
+ let uri = RadrootsSimplexSmpQueueUri::parse(
+ "smp://YWJjZA@server1.example,server2.example:5223/cXVldWU#/?v=4&dh=ZGhLZXk&q=m",
+ )
+ .unwrap();
+
+ assert_eq!(uri.server.server_identity, "YWJjZA");
+ assert_eq!(
+ uri.server.hosts,
+ vec!["server1.example".to_string(), "server2.example".to_string()]
+ );
+ assert_eq!(uri.server.port, Some(5223));
+ assert_eq!(uri.sender_id, "cXVldWU");
+ assert_eq!(uri.version_range, RadrootsSimplexSmpVersionRange::single(4));
+ assert_eq!(uri.recipient_dh_public_key, "ZGhLZXk");
+ assert_eq!(uri.queue_mode, Some(RadrootsSimplexSmpQueueMode::Messaging));
+ assert!(uri.sender_can_secure());
+ assert_eq!(
+ uri.to_string(),
+ "smp://YWJjZA@server1.example,server2.example:5223/cXVldWU#/?v=4&dh=ZGhLZXk&q=m"
+ );
+ }
+
+ #[test]
+ fn rejects_invalid_base64_fields() {
+ let error =
+ RadrootsSimplexSmpQueueUri::parse("smp://***@server.example/cXVldWU#/?v=4&dh=ZGhLZXk")
+ .unwrap_err();
+ assert!(matches!(
+ error,
+ RadrootsSimplexSmpProtoError::InvalidBase64Url {
+ field: "server_identity",
+ ..
+ }
+ ));
+ }
+
+ #[test]
+ fn parses_legacy_sender_secure_queue_uri() {
+ let uri = RadrootsSimplexSmpQueueUri::parse(
+ "smp://YWJjZA@server1.example:5223/cXVldWU#/?v=1-3&dh=ZGhLZXk&k=s&srv=server2.example",
+ )
+ .unwrap();
+
+ assert_eq!(
+ uri.server.hosts,
+ vec!["server1.example".to_string(), "server2.example".to_string()]
+ );
+ assert_eq!(uri.queue_mode, Some(RadrootsSimplexSmpQueueMode::Messaging));
+ assert_eq!(
+ uri.version_range,
+ RadrootsSimplexSmpVersionRange::new(1, 3).unwrap()
+ );
+ assert_eq!(
+ uri.to_string(),
+ "smp://YWJjZA@server1.example:5223/cXVldWU#/?v=1-3&dh=ZGhLZXk&k=s&srv=server2.example"
+ );
+ }
+}
diff --git a/crates/simplex-smp-proto/src/version.rs b/crates/simplex-smp-proto/src/version.rs
@@ -0,0 +1,114 @@
+use crate::error::RadrootsSimplexSmpProtoError;
+use alloc::string::{String, ToString};
+use core::fmt;
+use core::str::FromStr;
+
+pub const RADROOTS_SIMPLEX_SMP_INITIAL_CLIENT_VERSION: u16 = 1;
+pub const RADROOTS_SIMPLEX_SMP_SERVER_HOSTNAMES_CLIENT_VERSION: u16 = 2;
+pub const RADROOTS_SIMPLEX_SMP_SENDER_AUTH_KEY_CLIENT_VERSION: u16 = 3;
+pub const RADROOTS_SIMPLEX_SMP_SHORT_LINKS_CLIENT_VERSION: u16 = 4;
+pub const RADROOTS_SIMPLEX_SMP_CURRENT_CLIENT_VERSION: u16 =
+ RADROOTS_SIMPLEX_SMP_SHORT_LINKS_CLIENT_VERSION;
+pub const RADROOTS_SIMPLEX_SMP_INITIAL_TRANSPORT_VERSION: u16 = 6;
+pub const RADROOTS_SIMPLEX_SMP_SENDER_AUTH_KEY_TRANSPORT_VERSION: u16 = 9;
+pub const RADROOTS_SIMPLEX_SMP_DELETED_EVENT_TRANSPORT_VERSION: u16 = 10;
+pub const RADROOTS_SIMPLEX_SMP_BLOCKED_ENTITY_TRANSPORT_VERSION: u16 = 12;
+pub const RADROOTS_SIMPLEX_SMP_SHORT_LINKS_TRANSPORT_VERSION: u16 = 15;
+pub const RADROOTS_SIMPLEX_SMP_SERVICE_CERTS_TRANSPORT_VERSION: u16 = 16;
+pub const RADROOTS_SIMPLEX_SMP_NEW_NOTIFIER_CREDENTIALS_TRANSPORT_VERSION: u16 = 17;
+pub const RADROOTS_SIMPLEX_SMP_CURRENT_TRANSPORT_VERSION: u16 =
+ RADROOTS_SIMPLEX_SMP_NEW_NOTIFIER_CREDENTIALS_TRANSPORT_VERSION;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct RadrootsSimplexSmpVersionRange {
+ pub min: u16,
+ pub max: u16,
+}
+
+impl RadrootsSimplexSmpVersionRange {
+ pub const fn single(version: u16) -> Self {
+ Self {
+ min: version,
+ max: version,
+ }
+ }
+
+ pub fn new(min: u16, max: u16) -> Result<Self, RadrootsSimplexSmpProtoError> {
+ if min == 0 || max == 0 || min > max {
+ return Err(RadrootsSimplexSmpProtoError::InvalidVersionRange(
+ alloc::format!("{min}-{max}"),
+ ));
+ }
+
+ Ok(Self { min, max })
+ }
+
+ pub const fn contains(&self, version: u16) -> bool {
+ version >= self.min && version <= self.max
+ }
+}
+
+impl Default for RadrootsSimplexSmpVersionRange {
+ fn default() -> Self {
+ Self::single(RADROOTS_SIMPLEX_SMP_CURRENT_CLIENT_VERSION)
+ }
+}
+
+impl fmt::Display for RadrootsSimplexSmpVersionRange {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ if self.min == self.max {
+ write!(f, "{}", self.min)
+ } else {
+ write!(f, "{}-{}", self.min, self.max)
+ }
+ }
+}
+
+impl FromStr for RadrootsSimplexSmpVersionRange {
+ type Err = RadrootsSimplexSmpProtoError;
+
+ fn from_str(value: &str) -> Result<Self, Self::Err> {
+ let trimmed = value.trim();
+ if trimmed.is_empty() {
+ return Err(RadrootsSimplexSmpProtoError::InvalidVersionRange(
+ String::new(),
+ ));
+ }
+
+ if let Some((min, max)) = trimmed.split_once('-') {
+ let min = parse_version(min, trimmed)?;
+ let max = parse_version(max, trimmed)?;
+ Self::new(min, max)
+ } else {
+ let version = parse_version(trimmed, trimmed)?;
+ Self::new(version, version)
+ }
+ }
+}
+
+fn parse_version(value: &str, original: &str) -> Result<u16, RadrootsSimplexSmpProtoError> {
+ value
+ .parse::<u16>()
+ .map_err(|_| RadrootsSimplexSmpProtoError::InvalidVersionRange(original.to_string()))
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn parses_single_version_range() {
+ let range = "9".parse::<RadrootsSimplexSmpVersionRange>().unwrap();
+ assert_eq!(range, RadrootsSimplexSmpVersionRange::single(9));
+ assert_eq!(range.to_string(), "9");
+ }
+
+ #[test]
+ fn parses_bounded_version_range() {
+ let range = "6-9".parse::<RadrootsSimplexSmpVersionRange>().unwrap();
+ assert_eq!(range.min, 6);
+ assert_eq!(range.max, 9);
+ assert!(range.contains(7));
+ assert_eq!(range.to_string(), "6-9");
+ }
+}
diff --git a/crates/simplex-smp-proto/src/wire.rs b/crates/simplex-smp-proto/src/wire.rs
@@ -0,0 +1,1379 @@
+use crate::error::RadrootsSimplexSmpProtoError;
+use crate::uri::RadrootsSimplexSmpQueueMode;
+use crate::version::{
+ RADROOTS_SIMPLEX_SMP_CURRENT_TRANSPORT_VERSION,
+ RADROOTS_SIMPLEX_SMP_NEW_NOTIFIER_CREDENTIALS_TRANSPORT_VERSION,
+ RADROOTS_SIMPLEX_SMP_SENDER_AUTH_KEY_TRANSPORT_VERSION,
+ RADROOTS_SIMPLEX_SMP_SERVICE_CERTS_TRANSPORT_VERSION,
+ RADROOTS_SIMPLEX_SMP_SHORT_LINKS_TRANSPORT_VERSION,
+};
+use alloc::string::{String, ToString};
+use alloc::vec::Vec;
+
+const TAG_NEW: &[u8] = b"NEW";
+const TAG_SUB: &[u8] = b"SUB";
+const TAG_KEY: &[u8] = b"KEY";
+const TAG_NKEY: &[u8] = b"NKEY";
+const TAG_NDEL: &[u8] = b"NDEL";
+const TAG_GET: &[u8] = b"GET";
+const TAG_ACK: &[u8] = b"ACK";
+const TAG_OFF: &[u8] = b"OFF";
+const TAG_DEL: &[u8] = b"DEL";
+const TAG_QUE: &[u8] = b"QUE";
+const TAG_SKEY: &[u8] = b"SKEY";
+const TAG_SEND: &[u8] = b"SEND";
+const TAG_PING: &[u8] = b"PING";
+const TAG_NSUB: &[u8] = b"NSUB";
+
+const TAG_IDS: &[u8] = b"IDS";
+const TAG_NID: &[u8] = b"NID";
+const TAG_MSG: &[u8] = b"MSG";
+const TAG_NMSG: &[u8] = b"NMSG";
+const TAG_END: &[u8] = b"END";
+const TAG_DELD: &[u8] = b"DELD";
+const TAG_INFO: &[u8] = b"INFO";
+const TAG_OK: &[u8] = b"OK";
+const TAG_ERR: &[u8] = b"ERR";
+const TAG_PONG: &[u8] = b"PONG";
+
+const COMMAND_ERR_SYNTAX: &[u8] = b"SYNTAX";
+const COMMAND_ERR_PROHIBITED: &[u8] = b"PROHIBITED";
+const COMMAND_ERR_NO_AUTH: &[u8] = b"NO_AUTH";
+const COMMAND_ERR_HAS_AUTH: &[u8] = b"HAS_AUTH";
+const COMMAND_ERR_NO_ENTITY: &[u8] = b"NO_ENTITY";
+const COMMAND_ERR_NO_QUEUE: &[u8] = b"NO_QUEUE";
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum RadrootsSimplexSmpSubscriptionMode {
+ Subscribe,
+ OnlyCreate,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RadrootsSimplexSmpQueueLinkData {
+ pub fixed_data: Vec<u8>,
+ pub user_data: Vec<u8>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RadrootsSimplexSmpMessagingQueueRequest {
+ pub sender_id: Vec<u8>,
+ pub link_data: RadrootsSimplexSmpQueueLinkData,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RadrootsSimplexSmpContactQueueRequest {
+ pub link_id: Vec<u8>,
+ pub sender_id: Vec<u8>,
+ pub link_data: RadrootsSimplexSmpQueueLinkData,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum RadrootsSimplexSmpQueueRequestData {
+ Messaging(Option<RadrootsSimplexSmpMessagingQueueRequest>),
+ Contact(Option<RadrootsSimplexSmpContactQueueRequest>),
+}
+
+impl RadrootsSimplexSmpQueueRequestData {
+ pub const fn queue_mode(&self) -> RadrootsSimplexSmpQueueMode {
+ match self {
+ Self::Messaging(_) => RadrootsSimplexSmpQueueMode::Messaging,
+ Self::Contact(_) => RadrootsSimplexSmpQueueMode::Contact,
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RadrootsSimplexSmpNewNotifierCredentials {
+ pub notifier_auth_public_key: Vec<u8>,
+ pub recipient_notification_dh_public_key: Vec<u8>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RadrootsSimplexSmpServerNotifierCredentials {
+ pub notifier_id: Vec<u8>,
+ pub server_notification_dh_public_key: Vec<u8>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RadrootsSimplexSmpNewQueueRequest {
+ pub recipient_auth_public_key: Vec<u8>,
+ pub recipient_dh_public_key: Vec<u8>,
+ pub basic_auth: Option<String>,
+ pub subscription_mode: RadrootsSimplexSmpSubscriptionMode,
+ pub queue_request_data: Option<RadrootsSimplexSmpQueueRequestData>,
+ pub notifier_credentials: Option<RadrootsSimplexSmpNewNotifierCredentials>,
+}
+
+impl RadrootsSimplexSmpNewQueueRequest {
+ pub const fn sender_can_secure(&self) -> bool {
+ matches!(
+ self.queue_request_data,
+ Some(RadrootsSimplexSmpQueueRequestData::Messaging(_))
+ )
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RadrootsSimplexSmpMessageFlags {
+ pub notification: bool,
+ pub reserved: Vec<u8>,
+}
+
+impl RadrootsSimplexSmpMessageFlags {
+ pub const fn notifications_enabled() -> Self {
+ Self {
+ notification: true,
+ reserved: Vec::new(),
+ }
+ }
+
+ pub const fn notifications_disabled() -> Self {
+ Self {
+ notification: false,
+ reserved: Vec::new(),
+ }
+ }
+}
+
+impl Default for RadrootsSimplexSmpMessageFlags {
+ fn default() -> Self {
+ Self::notifications_disabled()
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RadrootsSimplexSmpSendCommand {
+ pub flags: RadrootsSimplexSmpMessageFlags,
+ pub message_body: Vec<u8>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum RadrootsSimplexSmpCommand {
+ New(RadrootsSimplexSmpNewQueueRequest),
+ Sub,
+ Key(Vec<u8>),
+ NKey {
+ notifier_auth_public_key: Vec<u8>,
+ recipient_notification_dh_public_key: Vec<u8>,
+ },
+ NDel,
+ Get,
+ Ack(Vec<u8>),
+ Off,
+ Del,
+ Que,
+ SKey(Vec<u8>),
+ Send(RadrootsSimplexSmpSendCommand),
+ Ping,
+ NSub,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RadrootsSimplexSmpQueueIdsResponse {
+ pub recipient_id: Vec<u8>,
+ pub sender_id: Vec<u8>,
+ pub server_dh_public_key: Vec<u8>,
+ pub queue_mode: Option<RadrootsSimplexSmpQueueMode>,
+ pub link_id: Option<Vec<u8>>,
+ pub service_id: Option<Vec<u8>>,
+ pub server_notification_credentials: Option<RadrootsSimplexSmpServerNotifierCredentials>,
+}
+
+impl RadrootsSimplexSmpQueueIdsResponse {
+ pub const fn sender_can_secure(&self) -> bool {
+ matches!(
+ self.queue_mode,
+ Some(RadrootsSimplexSmpQueueMode::Messaging)
+ )
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RadrootsSimplexSmpNotifierIdsResponse {
+ pub notifier_id: Vec<u8>,
+ pub server_notification_dh_public_key: Vec<u8>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RadrootsSimplexSmpReceivedMessage {
+ pub message_id: Vec<u8>,
+ pub encrypted_body: Vec<u8>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum RadrootsSimplexSmpCommandError {
+ Syntax,
+ Prohibited,
+ NoAuth,
+ HasAuth,
+ NoEntity,
+ Other(Vec<u8>),
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum RadrootsSimplexSmpError {
+ Block,
+ Session,
+ Command(RadrootsSimplexSmpCommandError),
+ Auth,
+ Quota,
+ NoMsg,
+ LargeMsg,
+ Internal,
+ Other(Vec<u8>),
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct RadrootsSimplexSmpCorrelationId([u8; 24]);
+
+impl RadrootsSimplexSmpCorrelationId {
+ pub const LENGTH: usize = 24;
+
+ pub const fn new(bytes: [u8; 24]) -> Self {
+ Self(bytes)
+ }
+
+ pub fn from_slice(value: &[u8]) -> Result<Self, RadrootsSimplexSmpProtoError> {
+ if value.len() != Self::LENGTH {
+ return Err(RadrootsSimplexSmpProtoError::InvalidCorrelationIdLength(
+ value.len(),
+ ));
+ }
+ let mut bytes = [0_u8; Self::LENGTH];
+ bytes.copy_from_slice(value);
+ Ok(Self(bytes))
+ }
+
+ pub const fn as_bytes(&self) -> &[u8; 24] {
+ &self.0
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum RadrootsSimplexSmpBrokerMessage {
+ Ids(RadrootsSimplexSmpQueueIdsResponse),
+ Nid(RadrootsSimplexSmpNotifierIdsResponse),
+ Msg(RadrootsSimplexSmpReceivedMessage),
+ NMsg {
+ nonce: [u8; 24],
+ encrypted_metadata: Vec<u8>,
+ },
+ End,
+ Deld,
+ Info(Vec<u8>),
+ Ok,
+ Err(RadrootsSimplexSmpError),
+ Pong,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RadrootsSimplexSmpCommandTransmission {
+ pub authorization: Vec<u8>,
+ pub correlation_id: Option<RadrootsSimplexSmpCorrelationId>,
+ pub entity_id: Vec<u8>,
+ pub command: RadrootsSimplexSmpCommand,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RadrootsSimplexSmpBrokerTransmission {
+ pub authorization: Vec<u8>,
+ pub correlation_id: Option<RadrootsSimplexSmpCorrelationId>,
+ pub entity_id: Vec<u8>,
+ pub message: RadrootsSimplexSmpBrokerMessage,
+}
+
+impl RadrootsSimplexSmpCommand {
+ pub fn encode(&self) -> Result<Vec<u8>, RadrootsSimplexSmpProtoError> {
+ self.encode_for_version(RADROOTS_SIMPLEX_SMP_CURRENT_TRANSPORT_VERSION)
+ }
+
+ pub fn encode_for_version(
+ &self,
+ transport_version: u16,
+ ) -> Result<Vec<u8>, RadrootsSimplexSmpProtoError> {
+ let mut buffer = Vec::new();
+ match self {
+ Self::New(request) => encode_new_request(&mut buffer, request, transport_version)?,
+ Self::Sub => buffer.extend_from_slice(TAG_SUB),
+ Self::Key(sender_auth_public_key) => {
+ buffer.extend_from_slice(TAG_KEY);
+ buffer.push(b' ');
+ push_short_bytes(&mut buffer, sender_auth_public_key)?;
+ }
+ Self::NKey {
+ notifier_auth_public_key,
+ recipient_notification_dh_public_key,
+ } => {
+ buffer.extend_from_slice(TAG_NKEY);
+ buffer.push(b' ');
+ push_short_bytes(&mut buffer, notifier_auth_public_key)?;
+ push_short_bytes(&mut buffer, recipient_notification_dh_public_key)?;
+ }
+ Self::NDel => buffer.extend_from_slice(TAG_NDEL),
+ Self::Get => buffer.extend_from_slice(TAG_GET),
+ Self::Ack(message_id) => {
+ buffer.extend_from_slice(TAG_ACK);
+ buffer.push(b' ');
+ push_short_bytes(&mut buffer, message_id)?;
+ }
+ Self::Off => buffer.extend_from_slice(TAG_OFF),
+ Self::Del => buffer.extend_from_slice(TAG_DEL),
+ Self::Que => buffer.extend_from_slice(TAG_QUE),
+ Self::SKey(sender_auth_public_key) => {
+ buffer.extend_from_slice(TAG_SKEY);
+ buffer.push(b' ');
+ push_short_bytes(&mut buffer, sender_auth_public_key)?;
+ }
+ Self::Send(send) => {
+ buffer.extend_from_slice(TAG_SEND);
+ buffer.push(b' ');
+ buffer.push(encode_bool(send.flags.notification));
+ buffer.extend_from_slice(&send.flags.reserved);
+ buffer.push(b' ');
+ buffer.extend_from_slice(&send.message_body);
+ }
+ Self::Ping => buffer.extend_from_slice(TAG_PING),
+ Self::NSub => buffer.extend_from_slice(TAG_NSUB),
+ }
+ Ok(buffer)
+ }
+
+ pub fn decode(bytes: &[u8]) -> Result<Self, RadrootsSimplexSmpProtoError> {
+ Self::decode_for_version(RADROOTS_SIMPLEX_SMP_CURRENT_TRANSPORT_VERSION, bytes)
+ }
+
+ pub fn decode_for_version(
+ transport_version: u16,
+ bytes: &[u8],
+ ) -> Result<Self, RadrootsSimplexSmpProtoError> {
+ let (tag, rest) = parse_tag(bytes)?;
+ let mut cursor = Cursor::new(rest);
+ let command = match tag.as_slice() {
+ TAG_NEW => Self::New(decode_new_request(&mut cursor, transport_version)?),
+ TAG_SUB => Self::Sub,
+ TAG_KEY => Self::Key(cursor.read_short_bytes()?),
+ TAG_NKEY => Self::NKey {
+ notifier_auth_public_key: cursor.read_short_bytes()?,
+ recipient_notification_dh_public_key: cursor.read_short_bytes()?,
+ },
+ TAG_NDEL => Self::NDel,
+ TAG_GET => Self::Get,
+ TAG_ACK => Self::Ack(cursor.read_short_bytes()?),
+ TAG_OFF => Self::Off,
+ TAG_DEL => Self::Del,
+ TAG_QUE => Self::Que,
+ TAG_SKEY => Self::SKey(cursor.read_short_bytes()?),
+ TAG_SEND => Self::Send(decode_send_payload(rest)?),
+ TAG_PING => Self::Ping,
+ TAG_NSUB => Self::NSub,
+ _ => {
+ return Err(RadrootsSimplexSmpProtoError::UnsupportedTag(
+ String::from_utf8_lossy(&tag).into_owned(),
+ ));
+ }
+ };
+ if !matches!(command, Self::Send(_)) && !cursor.is_empty() {
+ return Err(RadrootsSimplexSmpProtoError::TrailingBytes);
+ }
+ Ok(command)
+ }
+}
+
+impl RadrootsSimplexSmpBrokerMessage {
+ pub fn encode(&self) -> Result<Vec<u8>, RadrootsSimplexSmpProtoError> {
+ self.encode_for_version(RADROOTS_SIMPLEX_SMP_CURRENT_TRANSPORT_VERSION)
+ }
+
+ pub fn encode_for_version(
+ &self,
+ transport_version: u16,
+ ) -> Result<Vec<u8>, RadrootsSimplexSmpProtoError> {
+ let mut buffer = Vec::new();
+ match self {
+ Self::Ids(response) => encode_ids_response(&mut buffer, response, transport_version)?,
+ Self::Nid(response) => {
+ buffer.extend_from_slice(TAG_NID);
+ buffer.push(b' ');
+ push_short_bytes(&mut buffer, &response.notifier_id)?;
+ push_short_bytes(&mut buffer, &response.server_notification_dh_public_key)?;
+ }
+ Self::Msg(message) => {
+ buffer.extend_from_slice(TAG_MSG);
+ buffer.push(b' ');
+ push_short_bytes(&mut buffer, &message.message_id)?;
+ buffer.extend_from_slice(&message.encrypted_body);
+ }
+ Self::NMsg {
+ nonce,
+ encrypted_metadata,
+ } => {
+ buffer.extend_from_slice(TAG_NMSG);
+ buffer.push(b' ');
+ buffer.extend_from_slice(nonce);
+ buffer.extend_from_slice(encrypted_metadata);
+ }
+ Self::End => buffer.extend_from_slice(TAG_END),
+ Self::Deld => buffer.extend_from_slice(TAG_DELD),
+ Self::Info(info) => {
+ buffer.extend_from_slice(TAG_INFO);
+ buffer.push(b' ');
+ buffer.extend_from_slice(info);
+ }
+ Self::Ok => buffer.extend_from_slice(TAG_OK),
+ Self::Err(error) => {
+ buffer.extend_from_slice(TAG_ERR);
+ buffer.push(b' ');
+ buffer.extend_from_slice(&encode_error(error));
+ }
+ Self::Pong => buffer.extend_from_slice(TAG_PONG),
+ }
+ Ok(buffer)
+ }
+
+ pub fn decode(bytes: &[u8]) -> Result<Self, RadrootsSimplexSmpProtoError> {
+ Self::decode_for_version(RADROOTS_SIMPLEX_SMP_CURRENT_TRANSPORT_VERSION, bytes)
+ }
+
+ pub fn decode_for_version(
+ transport_version: u16,
+ bytes: &[u8],
+ ) -> Result<Self, RadrootsSimplexSmpProtoError> {
+ let (tag, rest) = parse_tag(bytes)?;
+ let mut cursor = Cursor::new(rest);
+ let message = match tag.as_slice() {
+ TAG_IDS => Self::Ids(decode_ids_response(&mut cursor, transport_version)?),
+ TAG_NID => Self::Nid(RadrootsSimplexSmpNotifierIdsResponse {
+ notifier_id: cursor.read_short_bytes()?,
+ server_notification_dh_public_key: cursor.read_short_bytes()?,
+ }),
+ TAG_MSG => Self::Msg(RadrootsSimplexSmpReceivedMessage {
+ message_id: cursor.read_short_bytes()?,
+ encrypted_body: cursor.read_remaining().to_vec(),
+ }),
+ TAG_NMSG => {
+ let nonce = cursor.read_array::<24>().map_err(|error| match error {
+ RadrootsSimplexSmpProtoError::UnexpectedEof => {
+ RadrootsSimplexSmpProtoError::InvalidNonceLength(cursor.remaining_len())
+ }
+ other => other,
+ })?;
+ Self::NMsg {
+ nonce,
+ encrypted_metadata: cursor.read_remaining().to_vec(),
+ }
+ }
+ TAG_END => Self::End,
+ TAG_DELD => Self::Deld,
+ TAG_INFO => Self::Info(cursor.read_remaining().to_vec()),
+ TAG_OK => Self::Ok,
+ TAG_ERR => Self::Err(decode_error(rest)?),
+ TAG_PONG => Self::Pong,
+ _ => {
+ return Err(RadrootsSimplexSmpProtoError::UnsupportedTag(
+ String::from_utf8_lossy(&tag).into_owned(),
+ ));
+ }
+ };
+ if !matches!(
+ message,
+ Self::Msg(_) | Self::NMsg { .. } | Self::Info(_) | Self::Err(_)
+ ) && !cursor.is_empty()
+ {
+ return Err(RadrootsSimplexSmpProtoError::TrailingBytes);
+ }
+ Ok(message)
+ }
+}
+
+impl RadrootsSimplexSmpCommandTransmission {
+ pub fn encode(&self) -> Result<Vec<u8>, RadrootsSimplexSmpProtoError> {
+ self.encode_for_version(RADROOTS_SIMPLEX_SMP_CURRENT_TRANSPORT_VERSION)
+ }
+
+ pub fn encode_for_version(
+ &self,
+ transport_version: u16,
+ ) -> Result<Vec<u8>, RadrootsSimplexSmpProtoError> {
+ encode_transmission(
+ &self.authorization,
+ self.correlation_id,
+ &self.entity_id,
+ &self.command.encode_for_version(transport_version)?,
+ )
+ }
+
+ pub fn decode(bytes: &[u8]) -> Result<Self, RadrootsSimplexSmpProtoError> {
+ Self::decode_for_version(RADROOTS_SIMPLEX_SMP_CURRENT_TRANSPORT_VERSION, bytes)
+ }
+
+ pub fn decode_for_version(
+ transport_version: u16,
+ bytes: &[u8],
+ ) -> Result<Self, RadrootsSimplexSmpProtoError> {
+ let (authorization, correlation_id, entity_id, frame) = decode_transmission(bytes)?;
+ Ok(Self {
+ authorization,
+ correlation_id,
+ entity_id,
+ command: RadrootsSimplexSmpCommand::decode_for_version(transport_version, frame)?,
+ })
+ }
+}
+
+impl RadrootsSimplexSmpBrokerTransmission {
+ pub fn encode(&self) -> Result<Vec<u8>, RadrootsSimplexSmpProtoError> {
+ self.encode_for_version(RADROOTS_SIMPLEX_SMP_CURRENT_TRANSPORT_VERSION)
+ }
+
+ pub fn encode_for_version(
+ &self,
+ transport_version: u16,
+ ) -> Result<Vec<u8>, RadrootsSimplexSmpProtoError> {
+ encode_transmission(
+ &self.authorization,
+ self.correlation_id,
+ &self.entity_id,
+ &self.message.encode_for_version(transport_version)?,
+ )
+ }
+
+ pub fn decode(bytes: &[u8]) -> Result<Self, RadrootsSimplexSmpProtoError> {
+ Self::decode_for_version(RADROOTS_SIMPLEX_SMP_CURRENT_TRANSPORT_VERSION, bytes)
+ }
+
+ pub fn decode_for_version(
+ transport_version: u16,
+ bytes: &[u8],
+ ) -> Result<Self, RadrootsSimplexSmpProtoError> {
+ let (authorization, correlation_id, entity_id, frame) = decode_transmission(bytes)?;
+ Ok(Self {
+ authorization,
+ correlation_id,
+ entity_id,
+ message: RadrootsSimplexSmpBrokerMessage::decode_for_version(transport_version, frame)?,
+ })
+ }
+}
+
+fn encode_new_request(
+ buffer: &mut Vec<u8>,
+ request: &RadrootsSimplexSmpNewQueueRequest,
+ transport_version: u16,
+) -> Result<(), RadrootsSimplexSmpProtoError> {
+ if transport_version < RADROOTS_SIMPLEX_SMP_SENDER_AUTH_KEY_TRANSPORT_VERSION {
+ return Err(RadrootsSimplexSmpProtoError::UnsupportedTransportVersion(
+ transport_version,
+ ));
+ }
+
+ buffer.extend_from_slice(TAG_NEW);
+ buffer.push(b' ');
+ push_short_bytes(buffer, &request.recipient_auth_public_key)?;
+ push_short_bytes(buffer, &request.recipient_dh_public_key)?;
+ push_maybe_string(buffer, request.basic_auth.as_deref())?;
+ buffer.push(encode_subscription_mode(request.subscription_mode));
+
+ if transport_version >= RADROOTS_SIMPLEX_SMP_NEW_NOTIFIER_CREDENTIALS_TRANSPORT_VERSION {
+ push_maybe(
+ buffer,
+ request.queue_request_data.as_ref(),
+ encode_queue_request_data,
+ )?;
+ push_maybe(
+ buffer,
+ request.notifier_credentials.as_ref(),
+ encode_new_notifier_credentials,
+ )?;
+ } else if transport_version >= RADROOTS_SIMPLEX_SMP_SHORT_LINKS_TRANSPORT_VERSION {
+ push_maybe(
+ buffer,
+ request.queue_request_data.as_ref(),
+ encode_queue_request_data,
+ )?;
+ } else {
+ buffer.push(encode_bool(request.sender_can_secure()));
+ }
+
+ Ok(())
+}
+
+fn decode_new_request(
+ cursor: &mut Cursor<'_>,
+ transport_version: u16,
+) -> Result<RadrootsSimplexSmpNewQueueRequest, RadrootsSimplexSmpProtoError> {
+ if transport_version < RADROOTS_SIMPLEX_SMP_SENDER_AUTH_KEY_TRANSPORT_VERSION {
+ return Err(RadrootsSimplexSmpProtoError::UnsupportedTransportVersion(
+ transport_version,
+ ));
+ }
+
+ let recipient_auth_public_key = cursor.read_short_bytes()?;
+ let recipient_dh_public_key = cursor.read_short_bytes()?;
+ let basic_auth = cursor.read_maybe_string()?;
+ let subscription_mode = decode_subscription_mode(cursor.read_byte()?)?;
+ let (queue_request_data, notifier_credentials) =
+ if transport_version >= RADROOTS_SIMPLEX_SMP_NEW_NOTIFIER_CREDENTIALS_TRANSPORT_VERSION {
+ (
+ cursor.read_maybe(decode_queue_request_data)?,
+ cursor.read_maybe(decode_new_notifier_credentials)?,
+ )
+ } else if transport_version >= RADROOTS_SIMPLEX_SMP_SHORT_LINKS_TRANSPORT_VERSION {
+ (cursor.read_maybe(decode_queue_request_data)?, None)
+ } else {
+ let sender_can_secure = decode_bool(cursor.read_byte()?)?;
+ let queue_request_data = Some(if sender_can_secure {
+ RadrootsSimplexSmpQueueRequestData::Messaging(None)
+ } else {
+ RadrootsSimplexSmpQueueRequestData::Contact(None)
+ });
+ (queue_request_data, None)
+ };
+
+ Ok(RadrootsSimplexSmpNewQueueRequest {
+ recipient_auth_public_key,
+ recipient_dh_public_key,
+ basic_auth,
+ subscription_mode,
+ queue_request_data,
+ notifier_credentials,
+ })
+}
+
+fn encode_ids_response(
+ buffer: &mut Vec<u8>,
+ response: &RadrootsSimplexSmpQueueIdsResponse,
+ transport_version: u16,
+) -> Result<(), RadrootsSimplexSmpProtoError> {
+ buffer.extend_from_slice(TAG_IDS);
+ buffer.push(b' ');
+ push_short_bytes(buffer, &response.recipient_id)?;
+ push_short_bytes(buffer, &response.sender_id)?;
+ push_short_bytes(buffer, &response.server_dh_public_key)?;
+
+ if transport_version >= RADROOTS_SIMPLEX_SMP_NEW_NOTIFIER_CREDENTIALS_TRANSPORT_VERSION {
+ push_maybe(buffer, response.queue_mode, encode_queue_mode)?;
+ push_maybe_short_bytes(buffer, response.link_id.as_deref())?;
+ push_maybe_short_bytes(buffer, response.service_id.as_deref())?;
+ push_maybe(
+ buffer,
+ response.server_notification_credentials.as_ref(),
+ encode_server_notifier_credentials,
+ )?;
+ } else if transport_version >= RADROOTS_SIMPLEX_SMP_SERVICE_CERTS_TRANSPORT_VERSION {
+ push_maybe(buffer, response.queue_mode, encode_queue_mode)?;
+ push_maybe_short_bytes(buffer, response.link_id.as_deref())?;
+ push_maybe_short_bytes(buffer, response.service_id.as_deref())?;
+ } else if transport_version >= RADROOTS_SIMPLEX_SMP_SHORT_LINKS_TRANSPORT_VERSION {
+ push_maybe(buffer, response.queue_mode, encode_queue_mode)?;
+ push_maybe_short_bytes(buffer, response.link_id.as_deref())?;
+ } else if transport_version >= RADROOTS_SIMPLEX_SMP_SENDER_AUTH_KEY_TRANSPORT_VERSION {
+ buffer.push(encode_bool(response.sender_can_secure()));
+ }
+
+ Ok(())
+}
+
+fn decode_ids_response(
+ cursor: &mut Cursor<'_>,
+ transport_version: u16,
+) -> Result<RadrootsSimplexSmpQueueIdsResponse, RadrootsSimplexSmpProtoError> {
+ let recipient_id = cursor.read_short_bytes()?;
+ let sender_id = cursor.read_short_bytes()?;
+ let server_dh_public_key = cursor.read_short_bytes()?;
+
+ let (queue_mode, link_id, service_id, server_notification_credentials) =
+ if transport_version >= RADROOTS_SIMPLEX_SMP_NEW_NOTIFIER_CREDENTIALS_TRANSPORT_VERSION {
+ (
+ cursor.read_maybe(decode_queue_mode)?,
+ cursor.read_maybe(Cursor::read_short_bytes)?,
+ cursor.read_maybe(Cursor::read_short_bytes)?,
+ cursor.read_maybe(decode_server_notifier_credentials)?,
+ )
+ } else if transport_version >= RADROOTS_SIMPLEX_SMP_SERVICE_CERTS_TRANSPORT_VERSION {
+ (
+ cursor.read_maybe(decode_queue_mode)?,
+ cursor.read_maybe(Cursor::read_short_bytes)?,
+ cursor.read_maybe(Cursor::read_short_bytes)?,
+ None,
+ )
+ } else if transport_version >= RADROOTS_SIMPLEX_SMP_SHORT_LINKS_TRANSPORT_VERSION {
+ (
+ cursor.read_maybe(decode_queue_mode)?,
+ cursor.read_maybe(Cursor::read_short_bytes)?,
+ None,
+ None,
+ )
+ } else if transport_version >= RADROOTS_SIMPLEX_SMP_SENDER_AUTH_KEY_TRANSPORT_VERSION {
+ let sender_can_secure = decode_bool(cursor.read_byte()?)?;
+ (
+ Some(if sender_can_secure {
+ RadrootsSimplexSmpQueueMode::Messaging
+ } else {
+ RadrootsSimplexSmpQueueMode::Contact
+ }),
+ None,
+ None,
+ None,
+ )
+ } else {
+ (None, None, None, None)
+ };
+
+ Ok(RadrootsSimplexSmpQueueIdsResponse {
+ recipient_id,
+ sender_id,
+ server_dh_public_key,
+ queue_mode,
+ link_id,
+ service_id,
+ server_notification_credentials,
+ })
+}
+
+fn encode_queue_request_data(
+ buffer: &mut Vec<u8>,
+ queue_request_data: &RadrootsSimplexSmpQueueRequestData,
+) -> Result<(), RadrootsSimplexSmpProtoError> {
+ match queue_request_data {
+ RadrootsSimplexSmpQueueRequestData::Messaging(data) => {
+ buffer.push(b'M');
+ push_maybe(buffer, data.as_ref(), encode_messaging_queue_request)
+ }
+ RadrootsSimplexSmpQueueRequestData::Contact(data) => {
+ buffer.push(b'C');
+ push_maybe(buffer, data.as_ref(), encode_contact_queue_request)
+ }
+ }
+}
+
+fn decode_queue_request_data(
+ cursor: &mut Cursor<'_>,
+) -> Result<RadrootsSimplexSmpQueueRequestData, RadrootsSimplexSmpProtoError> {
+ match cursor.read_byte()? {
+ b'M' => Ok(RadrootsSimplexSmpQueueRequestData::Messaging(
+ cursor.read_maybe(decode_messaging_queue_request)?,
+ )),
+ b'C' => Ok(RadrootsSimplexSmpQueueRequestData::Contact(
+ cursor.read_maybe(decode_contact_queue_request)?,
+ )),
+ value => Err(RadrootsSimplexSmpProtoError::InvalidTag(
+ String::from_utf8_lossy(&[value]).into_owned(),
+ )),
+ }
+}
+
+fn encode_messaging_queue_request(
+ buffer: &mut Vec<u8>,
+ request: &RadrootsSimplexSmpMessagingQueueRequest,
+) -> Result<(), RadrootsSimplexSmpProtoError> {
+ push_short_bytes(buffer, &request.sender_id)?;
+ encode_queue_link_data(buffer, &request.link_data)
+}
+
+fn decode_messaging_queue_request(
+ cursor: &mut Cursor<'_>,
+) -> Result<RadrootsSimplexSmpMessagingQueueRequest, RadrootsSimplexSmpProtoError> {
+ Ok(RadrootsSimplexSmpMessagingQueueRequest {
+ sender_id: cursor.read_short_bytes()?,
+ link_data: decode_queue_link_data(cursor)?,
+ })
+}
+
+fn encode_contact_queue_request(
+ buffer: &mut Vec<u8>,
+ request: &RadrootsSimplexSmpContactQueueRequest,
+) -> Result<(), RadrootsSimplexSmpProtoError> {
+ push_short_bytes(buffer, &request.link_id)?;
+ push_short_bytes(buffer, &request.sender_id)?;
+ encode_queue_link_data(buffer, &request.link_data)
+}
+
+fn decode_contact_queue_request(
+ cursor: &mut Cursor<'_>,
+) -> Result<RadrootsSimplexSmpContactQueueRequest, RadrootsSimplexSmpProtoError> {
+ Ok(RadrootsSimplexSmpContactQueueRequest {
+ link_id: cursor.read_short_bytes()?,
+ sender_id: cursor.read_short_bytes()?,
+ link_data: decode_queue_link_data(cursor)?,
+ })
+}
+
+fn encode_queue_link_data(
+ buffer: &mut Vec<u8>,
+ link_data: &RadrootsSimplexSmpQueueLinkData,
+) -> Result<(), RadrootsSimplexSmpProtoError> {
+ push_large_bytes(buffer, &link_data.fixed_data)?;
+ push_large_bytes(buffer, &link_data.user_data)
+}
+
+fn decode_queue_link_data(
+ cursor: &mut Cursor<'_>,
+) -> Result<RadrootsSimplexSmpQueueLinkData, RadrootsSimplexSmpProtoError> {
+ Ok(RadrootsSimplexSmpQueueLinkData {
+ fixed_data: cursor.read_large_bytes()?,
+ user_data: cursor.read_large_bytes()?,
+ })
+}
+
+fn encode_new_notifier_credentials(
+ buffer: &mut Vec<u8>,
+ credentials: &RadrootsSimplexSmpNewNotifierCredentials,
+) -> Result<(), RadrootsSimplexSmpProtoError> {
+ push_short_bytes(buffer, &credentials.notifier_auth_public_key)?;
+ push_short_bytes(buffer, &credentials.recipient_notification_dh_public_key)
+}
+
+fn decode_new_notifier_credentials(
+ cursor: &mut Cursor<'_>,
+) -> Result<RadrootsSimplexSmpNewNotifierCredentials, RadrootsSimplexSmpProtoError> {
+ Ok(RadrootsSimplexSmpNewNotifierCredentials {
+ notifier_auth_public_key: cursor.read_short_bytes()?,
+ recipient_notification_dh_public_key: cursor.read_short_bytes()?,
+ })
+}
+
+fn encode_server_notifier_credentials(
+ buffer: &mut Vec<u8>,
+ credentials: &RadrootsSimplexSmpServerNotifierCredentials,
+) -> Result<(), RadrootsSimplexSmpProtoError> {
+ push_short_bytes(buffer, &credentials.notifier_id)?;
+ push_short_bytes(buffer, &credentials.server_notification_dh_public_key)
+}
+
+fn decode_server_notifier_credentials(
+ cursor: &mut Cursor<'_>,
+) -> Result<RadrootsSimplexSmpServerNotifierCredentials, RadrootsSimplexSmpProtoError> {
+ Ok(RadrootsSimplexSmpServerNotifierCredentials {
+ notifier_id: cursor.read_short_bytes()?,
+ server_notification_dh_public_key: cursor.read_short_bytes()?,
+ })
+}
+
+fn encode_queue_mode(
+ buffer: &mut Vec<u8>,
+ queue_mode: RadrootsSimplexSmpQueueMode,
+) -> Result<(), RadrootsSimplexSmpProtoError> {
+ buffer.push(match queue_mode {
+ RadrootsSimplexSmpQueueMode::Messaging => b'M',
+ RadrootsSimplexSmpQueueMode::Contact => b'C',
+ });
+ Ok(())
+}
+
+fn decode_queue_mode(
+ cursor: &mut Cursor<'_>,
+) -> Result<RadrootsSimplexSmpQueueMode, RadrootsSimplexSmpProtoError> {
+ match cursor.read_byte()? {
+ b'M' => Ok(RadrootsSimplexSmpQueueMode::Messaging),
+ b'C' => Ok(RadrootsSimplexSmpQueueMode::Contact),
+ value => Err(RadrootsSimplexSmpProtoError::InvalidTag(
+ String::from_utf8_lossy(&[value]).into_owned(),
+ )),
+ }
+}
+
+fn encode_transmission(
+ authorization: &[u8],
+ correlation_id: Option<RadrootsSimplexSmpCorrelationId>,
+ entity_id: &[u8],
+ frame: &[u8],
+) -> Result<Vec<u8>, RadrootsSimplexSmpProtoError> {
+ let mut buffer = Vec::new();
+ push_short_bytes(&mut buffer, authorization)?;
+ push_short_bytes(
+ &mut buffer,
+ correlation_id
+ .map(|id| id.0.to_vec())
+ .as_deref()
+ .unwrap_or_default(),
+ )?;
+ push_short_bytes(&mut buffer, entity_id)?;
+ buffer.extend_from_slice(frame);
+ Ok(buffer)
+}
+
+fn decode_transmission(
+ bytes: &[u8],
+) -> Result<
+ (
+ Vec<u8>,
+ Option<RadrootsSimplexSmpCorrelationId>,
+ Vec<u8>,
+ &[u8],
+ ),
+ RadrootsSimplexSmpProtoError,
+> {
+ let mut cursor = Cursor::new(bytes);
+ let authorization = cursor.read_short_bytes()?;
+ let correlation_id = match cursor.read_short_bytes()?.as_slice() {
+ [] => None,
+ value => Some(RadrootsSimplexSmpCorrelationId::from_slice(value)?),
+ };
+ let entity_id = cursor.read_short_bytes()?;
+ let frame = cursor.read_remaining();
+ if frame.is_empty() {
+ return Err(RadrootsSimplexSmpProtoError::UnexpectedEof);
+ }
+ Ok((authorization, correlation_id, entity_id, frame))
+}
+
+fn decode_send_payload(
+ payload: &[u8],
+) -> Result<RadrootsSimplexSmpSendCommand, RadrootsSimplexSmpProtoError> {
+ let Some(space_index) = payload.iter().position(|byte| *byte == b' ') else {
+ return Err(RadrootsSimplexSmpProtoError::UnexpectedEof);
+ };
+ let flags_bytes = &payload[..space_index];
+ if flags_bytes.is_empty() {
+ return Err(RadrootsSimplexSmpProtoError::MissingField("msg_flags"));
+ }
+ let flags = RadrootsSimplexSmpMessageFlags {
+ notification: decode_bool(flags_bytes[0])?,
+ reserved: flags_bytes[1..].to_vec(),
+ };
+ Ok(RadrootsSimplexSmpSendCommand {
+ flags,
+ message_body: payload[space_index + 1..].to_vec(),
+ })
+}
+
+fn encode_error(error: &RadrootsSimplexSmpError) -> Vec<u8> {
+ match error {
+ RadrootsSimplexSmpError::Block => b"BLOCK".to_vec(),
+ RadrootsSimplexSmpError::Session => b"SESSION".to_vec(),
+ RadrootsSimplexSmpError::Command(command_error) => {
+ let mut bytes = b"CMD ".to_vec();
+ bytes.extend_from_slice(match command_error {
+ RadrootsSimplexSmpCommandError::Syntax => COMMAND_ERR_SYNTAX,
+ RadrootsSimplexSmpCommandError::Prohibited => COMMAND_ERR_PROHIBITED,
+ RadrootsSimplexSmpCommandError::NoAuth => COMMAND_ERR_NO_AUTH,
+ RadrootsSimplexSmpCommandError::HasAuth => COMMAND_ERR_HAS_AUTH,
+ RadrootsSimplexSmpCommandError::NoEntity => COMMAND_ERR_NO_ENTITY,
+ RadrootsSimplexSmpCommandError::Other(raw) => raw,
+ });
+ bytes
+ }
+ RadrootsSimplexSmpError::Auth => b"AUTH".to_vec(),
+ RadrootsSimplexSmpError::Quota => b"QUOTA".to_vec(),
+ RadrootsSimplexSmpError::NoMsg => b"NO_MSG".to_vec(),
+ RadrootsSimplexSmpError::LargeMsg => b"LARGE_MSG".to_vec(),
+ RadrootsSimplexSmpError::Internal => b"INTERNAL".to_vec(),
+ RadrootsSimplexSmpError::Other(raw) => raw.clone(),
+ }
+}
+
+fn decode_error(bytes: &[u8]) -> Result<RadrootsSimplexSmpError, RadrootsSimplexSmpProtoError> {
+ if bytes == b"BLOCK" {
+ return Ok(RadrootsSimplexSmpError::Block);
+ }
+ if bytes == b"SESSION" {
+ return Ok(RadrootsSimplexSmpError::Session);
+ }
+ if bytes == b"AUTH" {
+ return Ok(RadrootsSimplexSmpError::Auth);
+ }
+ if bytes == b"QUOTA" {
+ return Ok(RadrootsSimplexSmpError::Quota);
+ }
+ if bytes == b"NO_MSG" {
+ return Ok(RadrootsSimplexSmpError::NoMsg);
+ }
+ if bytes == b"LARGE_MSG" {
+ return Ok(RadrootsSimplexSmpError::LargeMsg);
+ }
+ if bytes == b"INTERNAL" {
+ return Ok(RadrootsSimplexSmpError::Internal);
+ }
+ if let Some(command) = bytes.strip_prefix(b"CMD ") {
+ let command_error = match command {
+ COMMAND_ERR_SYNTAX => RadrootsSimplexSmpCommandError::Syntax,
+ COMMAND_ERR_PROHIBITED => RadrootsSimplexSmpCommandError::Prohibited,
+ COMMAND_ERR_NO_AUTH => RadrootsSimplexSmpCommandError::NoAuth,
+ COMMAND_ERR_HAS_AUTH => RadrootsSimplexSmpCommandError::HasAuth,
+ COMMAND_ERR_NO_ENTITY | COMMAND_ERR_NO_QUEUE => {
+ RadrootsSimplexSmpCommandError::NoEntity
+ }
+ raw => RadrootsSimplexSmpCommandError::Other(raw.to_vec()),
+ };
+ return Ok(RadrootsSimplexSmpError::Command(command_error));
+ }
+ Ok(RadrootsSimplexSmpError::Other(bytes.to_vec()))
+}
+
+fn parse_tag(bytes: &[u8]) -> Result<(Vec<u8>, &[u8]), RadrootsSimplexSmpProtoError> {
+ if bytes.is_empty() {
+ return Err(RadrootsSimplexSmpProtoError::UnexpectedEof);
+ }
+ if let Some(space_index) = bytes.iter().position(|byte| *byte == b' ') {
+ Ok((bytes[..space_index].to_vec(), &bytes[space_index + 1..]))
+ } else {
+ Ok((bytes.to_vec(), &[]))
+ }
+}
+
+fn encode_subscription_mode(mode: RadrootsSimplexSmpSubscriptionMode) -> u8 {
+ match mode {
+ RadrootsSimplexSmpSubscriptionMode::Subscribe => b'S',
+ RadrootsSimplexSmpSubscriptionMode::OnlyCreate => b'C',
+ }
+}
+
+fn decode_subscription_mode(
+ value: u8,
+) -> Result<RadrootsSimplexSmpSubscriptionMode, RadrootsSimplexSmpProtoError> {
+ match value {
+ b'S' => Ok(RadrootsSimplexSmpSubscriptionMode::Subscribe),
+ b'C' => Ok(RadrootsSimplexSmpSubscriptionMode::OnlyCreate),
+ _ => Err(RadrootsSimplexSmpProtoError::InvalidTag(
+ String::from_utf8_lossy(&[value]).into_owned(),
+ )),
+ }
+}
+
+fn encode_bool(value: bool) -> u8 {
+ if value { b'T' } else { b'F' }
+}
+
+fn decode_bool(value: u8) -> Result<bool, RadrootsSimplexSmpProtoError> {
+ match value {
+ b'T' => Ok(true),
+ b'F' => Ok(false),
+ other => Err(RadrootsSimplexSmpProtoError::InvalidBoolEncoding(other)),
+ }
+}
+
+fn push_short_bytes(
+ buffer: &mut Vec<u8>,
+ bytes: &[u8],
+) -> Result<(), RadrootsSimplexSmpProtoError> {
+ let len = u8::try_from(bytes.len())
+ .map_err(|_| RadrootsSimplexSmpProtoError::InvalidShortFieldLength(bytes.len()))?;
+ buffer.push(len);
+ buffer.extend_from_slice(bytes);
+ Ok(())
+}
+
+fn push_large_bytes(
+ buffer: &mut Vec<u8>,
+ bytes: &[u8],
+) -> Result<(), RadrootsSimplexSmpProtoError> {
+ let len = u16::try_from(bytes.len())
+ .map_err(|_| RadrootsSimplexSmpProtoError::InvalidLargeFieldLength(bytes.len()))?;
+ buffer.extend_from_slice(&len.to_be_bytes());
+ buffer.extend_from_slice(bytes);
+ Ok(())
+}
+
+fn push_maybe<T, F>(
+ buffer: &mut Vec<u8>,
+ value: Option<T>,
+ mut encode: F,
+) -> Result<(), RadrootsSimplexSmpProtoError>
+where
+ T: Copy,
+ F: FnMut(&mut Vec<u8>, T) -> Result<(), RadrootsSimplexSmpProtoError>,
+{
+ match value {
+ None => {
+ buffer.push(b'0');
+ Ok(())
+ }
+ Some(value) => {
+ buffer.push(b'1');
+ encode(buffer, value)
+ }
+ }
+}
+
+fn push_maybe_short_bytes(
+ buffer: &mut Vec<u8>,
+ value: Option<&[u8]>,
+) -> Result<(), RadrootsSimplexSmpProtoError> {
+ push_maybe(buffer, value, push_short_bytes)
+}
+
+fn push_maybe_string(
+ buffer: &mut Vec<u8>,
+ value: Option<&str>,
+) -> Result<(), RadrootsSimplexSmpProtoError> {
+ match value {
+ None => {
+ buffer.push(b'0');
+ Ok(())
+ }
+ Some(value) => {
+ validate_basic_auth(value)?;
+ buffer.push(b'1');
+ push_short_bytes(buffer, value.as_bytes())
+ }
+ }
+}
+
+fn validate_basic_auth(value: &str) -> Result<(), RadrootsSimplexSmpProtoError> {
+ if value
+ .bytes()
+ .all(|byte| byte.is_ascii_graphic() && byte != b'@' && byte != b':' && byte != b'/')
+ {
+ Ok(())
+ } else {
+ Err(RadrootsSimplexSmpProtoError::InvalidUri(value.to_string()))
+ }
+}
+
+struct Cursor<'a> {
+ bytes: &'a [u8],
+ offset: usize,
+}
+
+impl<'a> Cursor<'a> {
+ const fn new(bytes: &'a [u8]) -> Self {
+ Self { bytes, offset: 0 }
+ }
+
+ fn is_empty(&self) -> bool {
+ self.offset >= self.bytes.len()
+ }
+
+ fn remaining_len(&self) -> usize {
+ self.bytes.len().saturating_sub(self.offset)
+ }
+
+ fn read_byte(&mut self) -> Result<u8, RadrootsSimplexSmpProtoError> {
+ let byte = *self
+ .bytes
+ .get(self.offset)
+ .ok_or(RadrootsSimplexSmpProtoError::UnexpectedEof)?;
+ self.offset += 1;
+ Ok(byte)
+ }
+
+ fn read_exact(&mut self, len: usize) -> Result<&'a [u8], RadrootsSimplexSmpProtoError> {
+ let end = self.offset + len;
+ let value = self
+ .bytes
+ .get(self.offset..end)
+ .ok_or(RadrootsSimplexSmpProtoError::UnexpectedEof)?;
+ self.offset = end;
+ Ok(value)
+ }
+
+ fn read_array<const N: usize>(&mut self) -> Result<[u8; N], RadrootsSimplexSmpProtoError> {
+ let mut array = [0_u8; N];
+ array.copy_from_slice(self.read_exact(N)?);
+ Ok(array)
+ }
+
+ fn read_short_bytes(&mut self) -> Result<Vec<u8>, RadrootsSimplexSmpProtoError> {
+ let len = usize::from(self.read_byte()?);
+ Ok(self.read_exact(len)?.to_vec())
+ }
+
+ fn read_large_bytes(&mut self) -> Result<Vec<u8>, RadrootsSimplexSmpProtoError> {
+ let len = usize::from(u16::from_be_bytes(self.read_array::<2>()?));
+ Ok(self.read_exact(len)?.to_vec())
+ }
+
+ fn read_maybe_string(&mut self) -> Result<Option<String>, RadrootsSimplexSmpProtoError> {
+ self.read_maybe(|cursor| {
+ let value = cursor.read_short_bytes()?;
+ let string = String::from_utf8(value)
+ .map_err(|error| RadrootsSimplexSmpProtoError::InvalidUtf8(error.to_string()))?;
+ validate_basic_auth(&string)?;
+ Ok(string)
+ })
+ }
+
+ fn read_maybe<T, F>(&mut self, decode: F) -> Result<Option<T>, RadrootsSimplexSmpProtoError>
+ where
+ F: FnOnce(&mut Self) -> Result<T, RadrootsSimplexSmpProtoError>,
+ {
+ match self.read_byte()? {
+ b'0' => Ok(None),
+ b'1' => Ok(Some(decode(self)?)),
+ other => Err(RadrootsSimplexSmpProtoError::InvalidMaybeTag(other)),
+ }
+ }
+
+ fn read_remaining(&mut self) -> &'a [u8] {
+ let remaining = &self.bytes[self.offset..];
+ self.offset = self.bytes.len();
+ remaining
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn correlation_id(byte: u8) -> RadrootsSimplexSmpCorrelationId {
+ RadrootsSimplexSmpCorrelationId::new([byte; 24])
+ }
+
+ #[test]
+ fn round_trips_current_new_command_transmission() {
+ let transmission = RadrootsSimplexSmpCommandTransmission {
+ authorization: vec![1, 2, 3],
+ correlation_id: Some(correlation_id(7)),
+ entity_id: Vec::new(),
+ command: RadrootsSimplexSmpCommand::New(RadrootsSimplexSmpNewQueueRequest {
+ recipient_auth_public_key: vec![0x01, 0x02, 0x03],
+ recipient_dh_public_key: vec![0x04, 0x05],
+ basic_auth: Some("server-pass".to_string()),
+ subscription_mode: RadrootsSimplexSmpSubscriptionMode::Subscribe,
+ queue_request_data: Some(RadrootsSimplexSmpQueueRequestData::Messaging(Some(
+ RadrootsSimplexSmpMessagingQueueRequest {
+ sender_id: vec![0x10, 0x11],
+ link_data: RadrootsSimplexSmpQueueLinkData {
+ fixed_data: vec![0xaa, 0xbb],
+ user_data: vec![0xcc, 0xdd, 0xee],
+ },
+ },
+ ))),
+ notifier_credentials: Some(RadrootsSimplexSmpNewNotifierCredentials {
+ notifier_auth_public_key: vec![0x21, 0x22],
+ recipient_notification_dh_public_key: vec![0x23, 0x24],
+ }),
+ }),
+ };
+
+ let encoded = transmission.encode().unwrap();
+ let decoded = RadrootsSimplexSmpCommandTransmission::decode(&encoded).unwrap();
+ assert_eq!(decoded, transmission);
+ }
+
+ #[test]
+ fn round_trips_v9_new_command_transmission() {
+ let transmission = RadrootsSimplexSmpCommandTransmission {
+ authorization: vec![1, 2, 3],
+ correlation_id: Some(correlation_id(7)),
+ entity_id: Vec::new(),
+ command: RadrootsSimplexSmpCommand::New(RadrootsSimplexSmpNewQueueRequest {
+ recipient_auth_public_key: vec![0x01, 0x02, 0x03],
+ recipient_dh_public_key: vec![0x04, 0x05],
+ basic_auth: Some("server-pass".to_string()),
+ subscription_mode: RadrootsSimplexSmpSubscriptionMode::Subscribe,
+ queue_request_data: Some(RadrootsSimplexSmpQueueRequestData::Messaging(None)),
+ notifier_credentials: None,
+ }),
+ };
+
+ let encoded = transmission
+ .encode_for_version(RADROOTS_SIMPLEX_SMP_SENDER_AUTH_KEY_TRANSPORT_VERSION)
+ .unwrap();
+ let decoded = RadrootsSimplexSmpCommandTransmission::decode_for_version(
+ RADROOTS_SIMPLEX_SMP_SENDER_AUTH_KEY_TRANSPORT_VERSION,
+ &encoded,
+ )
+ .unwrap();
+ assert_eq!(decoded, transmission);
+ }
+
+ #[test]
+ fn round_trips_send_command_transmission() {
+ let transmission = RadrootsSimplexSmpCommandTransmission {
+ authorization: Vec::new(),
+ correlation_id: Some(correlation_id(9)),
+ entity_id: vec![0xaa, 0xbb],
+ command: RadrootsSimplexSmpCommand::Send(RadrootsSimplexSmpSendCommand {
+ flags: RadrootsSimplexSmpMessageFlags {
+ notification: true,
+ reserved: b"0".to_vec(),
+ },
+ message_body: vec![0xde, 0xad, 0xbe, 0xef],
+ }),
+ };
+
+ let encoded = transmission.encode().unwrap();
+ let decoded = RadrootsSimplexSmpCommandTransmission::decode(&encoded).unwrap();
+ assert_eq!(decoded, transmission);
+ }
+
+ #[test]
+ fn round_trips_current_ids_broker_transmission() {
+ let transmission = RadrootsSimplexSmpBrokerTransmission {
+ authorization: Vec::new(),
+ correlation_id: Some(correlation_id(3)),
+ entity_id: Vec::new(),
+ message: RadrootsSimplexSmpBrokerMessage::Ids(RadrootsSimplexSmpQueueIdsResponse {
+ recipient_id: vec![0x10],
+ sender_id: vec![0x11],
+ server_dh_public_key: vec![0x12, 0x13],
+ queue_mode: Some(RadrootsSimplexSmpQueueMode::Messaging),
+ link_id: Some(vec![0x14, 0x15]),
+ service_id: Some(vec![0x16, 0x17]),
+ server_notification_credentials: Some(
+ RadrootsSimplexSmpServerNotifierCredentials {
+ notifier_id: vec![0x18, 0x19],
+ server_notification_dh_public_key: vec![0x1a, 0x1b],
+ },
+ ),
+ }),
+ };
+
+ let encoded = transmission.encode().unwrap();
+ let decoded = RadrootsSimplexSmpBrokerTransmission::decode(&encoded).unwrap();
+ assert_eq!(decoded, transmission);
+ }
+
+ #[test]
+ fn round_trips_v9_ids_broker_transmission() {
+ let transmission = RadrootsSimplexSmpBrokerTransmission {
+ authorization: Vec::new(),
+ correlation_id: Some(correlation_id(3)),
+ entity_id: Vec::new(),
+ message: RadrootsSimplexSmpBrokerMessage::Ids(RadrootsSimplexSmpQueueIdsResponse {
+ recipient_id: vec![0x10],
+ sender_id: vec![0x11],
+ server_dh_public_key: vec![0x12, 0x13],
+ queue_mode: Some(RadrootsSimplexSmpQueueMode::Messaging),
+ link_id: None,
+ service_id: None,
+ server_notification_credentials: None,
+ }),
+ };
+
+ let encoded = transmission
+ .encode_for_version(RADROOTS_SIMPLEX_SMP_SENDER_AUTH_KEY_TRANSPORT_VERSION)
+ .unwrap();
+ let decoded = RadrootsSimplexSmpBrokerTransmission::decode_for_version(
+ RADROOTS_SIMPLEX_SMP_SENDER_AUTH_KEY_TRANSPORT_VERSION,
+ &encoded,
+ )
+ .unwrap();
+ assert_eq!(decoded, transmission);
+ }
+
+ #[test]
+ fn round_trips_error_broker_transmission() {
+ let transmission = RadrootsSimplexSmpBrokerTransmission {
+ authorization: Vec::new(),
+ correlation_id: Some(correlation_id(5)),
+ entity_id: vec![0x01],
+ message: RadrootsSimplexSmpBrokerMessage::Err(RadrootsSimplexSmpError::Command(
+ RadrootsSimplexSmpCommandError::Prohibited,
+ )),
+ };
+
+ let encoded = transmission.encode().unwrap();
+ let decoded = RadrootsSimplexSmpBrokerTransmission::decode(&encoded).unwrap();
+ assert_eq!(decoded, transmission);
+ }
+
+ #[test]
+ fn round_trips_message_notification() {
+ let transmission = RadrootsSimplexSmpBrokerTransmission {
+ authorization: Vec::new(),
+ correlation_id: None,
+ entity_id: vec![0x99],
+ message: RadrootsSimplexSmpBrokerMessage::NMsg {
+ nonce: [0x22; 24],
+ encrypted_metadata: vec![0x33, 0x44, 0x55],
+ },
+ };
+
+ let encoded = transmission.encode().unwrap();
+ let decoded = RadrootsSimplexSmpBrokerTransmission::decode(&encoded).unwrap();
+ assert_eq!(decoded, transmission);
+ }
+}