lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit e974c31239f39fd6f7a7c645aef7f0d18c7d8e02
parent 26df74be5f3c86f800df9ee2cf23035dccf852dc
Author: triesap <tyson@radroots.org>
Date:   Sun, 22 Mar 2026 02:12:41 +0000

simplex: fail malformed nested error branches

Diffstat:
Mcrates/simplex-smp-proto/src/wire.rs | 172++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
1 file changed, 113 insertions(+), 59 deletions(-)

diff --git a/crates/simplex-smp-proto/src/wire.rs b/crates/simplex-smp-proto/src/wire.rs @@ -1458,14 +1458,14 @@ fn decode_error(bytes: &[u8]) -> Result<RadrootsSimplexSmpError, RadrootsSimplex return Ok(RadrootsSimplexSmpError::Command(command_error)); } if let Some(proxy_error) = bytes.strip_prefix(b"PROXY ") { - if let Some(proxy_error) = decode_proxy_error(proxy_error) { - return Ok(RadrootsSimplexSmpError::Proxy(proxy_error)); - } + return Ok(RadrootsSimplexSmpError::Proxy(decode_proxy_error( + proxy_error, + )?)); } if let Some(blocking_info) = bytes.strip_prefix(b"BLOCKED ") { - if let Some(blocking_info) = decode_blocking_info(blocking_info) { - return Ok(RadrootsSimplexSmpError::Blocked(blocking_info)); - } + return Ok(RadrootsSimplexSmpError::Blocked(decode_blocking_info( + blocking_info, + )?)); } Ok(RadrootsSimplexSmpError::Other(bytes.to_vec())) } @@ -1487,18 +1487,20 @@ fn encode_proxy_error(error: &RadrootsSimplexSmpProxyError) -> Vec<u8> { } } -fn decode_proxy_error(bytes: &[u8]) -> Option<RadrootsSimplexSmpProxyError> { - let (tag, rest) = parse_tag(bytes).ok()?; +fn decode_proxy_error( + bytes: &[u8], +) -> Result<RadrootsSimplexSmpProxyError, RadrootsSimplexSmpProtoError> { + let (tag, rest) = parse_tag(bytes)?; match tag.as_slice() { - b"PROTOCOL" => Some(RadrootsSimplexSmpProxyError::Protocol(Box::new( - decode_error(rest).ok()?, + b"PROTOCOL" => Ok(RadrootsSimplexSmpProxyError::Protocol(Box::new( + decode_error(rest)?, ))), - b"BROKER" => Some(RadrootsSimplexSmpProxyError::Broker(decode_broker_error( + b"BROKER" => Ok(RadrootsSimplexSmpProxyError::Broker(decode_broker_error( rest, )?)), - b"BASIC_AUTH" if rest.is_empty() => Some(RadrootsSimplexSmpProxyError::BasicAuth), - b"NO_SESSION" if rest.is_empty() => Some(RadrootsSimplexSmpProxyError::NoSession), - _ => None, + b"BASIC_AUTH" if rest.is_empty() => Ok(RadrootsSimplexSmpProxyError::BasicAuth), + b"NO_SESSION" if rest.is_empty() => Ok(RadrootsSimplexSmpProxyError::NoSession), + _ => Err(invalid_ascii_tag(&tag)), } } @@ -1528,28 +1530,30 @@ fn encode_broker_error(error: &RadrootsSimplexSmpBrokerError) -> Vec<u8> { } } -fn decode_broker_error(bytes: &[u8]) -> Option<RadrootsSimplexSmpBrokerError> { - let (tag, rest) = parse_tag(bytes).ok()?; +fn decode_broker_error( + bytes: &[u8], +) -> Result<RadrootsSimplexSmpBrokerError, RadrootsSimplexSmpProtoError> { + let (tag, rest) = parse_tag(bytes)?; match tag.as_slice() { - b"RESPONSE" => Some(RadrootsSimplexSmpBrokerError::Response( - decode_short_string_lossy(rest).ok()?, + b"RESPONSE" => Ok(RadrootsSimplexSmpBrokerError::Response( + decode_short_string_lossy(rest)?, )), - b"UNEXPECTED" => Some(RadrootsSimplexSmpBrokerError::Unexpected( - decode_short_string_lossy(rest).ok()?, + b"UNEXPECTED" => Ok(RadrootsSimplexSmpBrokerError::Unexpected( + decode_short_string_lossy(rest)?, )), - b"TRANSPORT" => Some(RadrootsSimplexSmpBrokerError::Transport( + b"TRANSPORT" => Ok(RadrootsSimplexSmpBrokerError::Transport( decode_transport_error(rest)?, )), - b"NETWORK" if rest.is_empty() => Some(RadrootsSimplexSmpBrokerError::Network( + b"NETWORK" if rest.is_empty() => Ok(RadrootsSimplexSmpBrokerError::Network( RadrootsSimplexSmpNetworkError::Failed, )), - b"NETWORK" => Some(RadrootsSimplexSmpBrokerError::Network( + b"NETWORK" => Ok(RadrootsSimplexSmpBrokerError::Network( decode_network_error(rest)?, )), - b"TIMEOUT" if rest.is_empty() => Some(RadrootsSimplexSmpBrokerError::Timeout), - b"HOST" if rest.is_empty() => Some(RadrootsSimplexSmpBrokerError::Host), - b"NO_SERVICE" if rest.is_empty() => Some(RadrootsSimplexSmpBrokerError::NoService), - _ => None, + b"TIMEOUT" if rest.is_empty() => Ok(RadrootsSimplexSmpBrokerError::Timeout), + b"HOST" if rest.is_empty() => Ok(RadrootsSimplexSmpBrokerError::Host), + b"NO_SERVICE" if rest.is_empty() => Ok(RadrootsSimplexSmpBrokerError::NoService), + _ => Err(invalid_ascii_tag(&tag)), } } @@ -1568,18 +1572,20 @@ fn encode_transport_error(error: &RadrootsSimplexSmpTransportError) -> Vec<u8> { } } -fn decode_transport_error(bytes: &[u8]) -> Option<RadrootsSimplexSmpTransportError> { - let (tag, rest) = parse_tag(bytes).ok()?; +fn decode_transport_error( + bytes: &[u8], +) -> Result<RadrootsSimplexSmpTransportError, RadrootsSimplexSmpProtoError> { + let (tag, rest) = parse_tag(bytes)?; match tag.as_slice() { - b"BLOCK" if rest.is_empty() => Some(RadrootsSimplexSmpTransportError::Block), - b"VERSION" if rest.is_empty() => Some(RadrootsSimplexSmpTransportError::Version), - b"LARGE_MSG" if rest.is_empty() => Some(RadrootsSimplexSmpTransportError::LargeMsg), - b"SESSION" if rest.is_empty() => Some(RadrootsSimplexSmpTransportError::Session), - b"NO_AUTH" if rest.is_empty() => Some(RadrootsSimplexSmpTransportError::NoAuth), - b"HANDSHAKE" => Some(RadrootsSimplexSmpTransportError::Handshake( - decode_handshake_error(rest), + b"BLOCK" if rest.is_empty() => Ok(RadrootsSimplexSmpTransportError::Block), + b"VERSION" if rest.is_empty() => Ok(RadrootsSimplexSmpTransportError::Version), + b"LARGE_MSG" if rest.is_empty() => Ok(RadrootsSimplexSmpTransportError::LargeMsg), + b"SESSION" if rest.is_empty() => Ok(RadrootsSimplexSmpTransportError::Session), + b"NO_AUTH" if rest.is_empty() => Ok(RadrootsSimplexSmpTransportError::NoAuth), + b"HANDSHAKE" => Ok(RadrootsSimplexSmpTransportError::Handshake( + decode_handshake_error(rest)?, )), - _ => None, + _ => Err(invalid_ascii_tag(&tag)), } } @@ -1610,22 +1616,24 @@ fn encode_network_error(error: &RadrootsSimplexSmpNetworkError) -> Vec<u8> { } } -fn decode_network_error(bytes: &[u8]) -> Option<RadrootsSimplexSmpNetworkError> { - let (tag, rest) = parse_tag(bytes).ok()?; +fn decode_network_error( + bytes: &[u8], +) -> Result<RadrootsSimplexSmpNetworkError, RadrootsSimplexSmpProtoError> { + let (tag, rest) = parse_tag(bytes)?; match tag.as_slice() { - b"CONNECT" => Some(RadrootsSimplexSmpNetworkError::Connect( - decode_short_string_lossy(rest).ok()?, + b"CONNECT" => Ok(RadrootsSimplexSmpNetworkError::Connect( + decode_short_string_lossy(rest)?, )), - b"TLS" => Some(RadrootsSimplexSmpNetworkError::Tls( - decode_short_string_lossy(rest).ok()?, + b"TLS" => Ok(RadrootsSimplexSmpNetworkError::Tls( + decode_short_string_lossy(rest)?, )), - b"UNKNOWNCA" if rest.is_empty() => Some(RadrootsSimplexSmpNetworkError::UnknownCa), - b"FAILED" if rest.is_empty() => Some(RadrootsSimplexSmpNetworkError::Failed), - b"TIMEOUT" if rest.is_empty() => Some(RadrootsSimplexSmpNetworkError::Timeout), - b"SUBSCRIBE" => Some(RadrootsSimplexSmpNetworkError::Subscribe( - decode_short_string_lossy(rest).ok()?, + b"UNKNOWNCA" if rest.is_empty() => Ok(RadrootsSimplexSmpNetworkError::UnknownCa), + b"FAILED" if rest.is_empty() => Ok(RadrootsSimplexSmpNetworkError::Failed), + b"TIMEOUT" if rest.is_empty() => Ok(RadrootsSimplexSmpNetworkError::Timeout), + b"SUBSCRIBE" => Ok(RadrootsSimplexSmpNetworkError::Subscribe( + decode_short_string_lossy(rest)?, )), - _ => None, + _ => Err(invalid_ascii_tag(&tag)), } } @@ -1639,13 +1647,15 @@ fn encode_handshake_error(error: &RadrootsSimplexSmpHandshakeError) -> Vec<u8> { } } -fn decode_handshake_error(bytes: &[u8]) -> RadrootsSimplexSmpHandshakeError { +fn decode_handshake_error( + bytes: &[u8], +) -> Result<RadrootsSimplexSmpHandshakeError, RadrootsSimplexSmpProtoError> { match bytes { - b"PARSE" => RadrootsSimplexSmpHandshakeError::Parse, - b"IDENTITY" => RadrootsSimplexSmpHandshakeError::Identity, - b"BAD_AUTH" => RadrootsSimplexSmpHandshakeError::BadAuth, - b"BAD_SERVICE" => RadrootsSimplexSmpHandshakeError::BadService, - raw => RadrootsSimplexSmpHandshakeError::Other(String::from_utf8_lossy(raw).into_owned()), + b"PARSE" => Ok(RadrootsSimplexSmpHandshakeError::Parse), + b"IDENTITY" => Ok(RadrootsSimplexSmpHandshakeError::Identity), + b"BAD_AUTH" => Ok(RadrootsSimplexSmpHandshakeError::BadAuth), + b"BAD_SERVICE" => Ok(RadrootsSimplexSmpHandshakeError::BadService), + raw => Err(invalid_ascii_tag(raw)), } } @@ -1659,14 +1669,18 @@ fn encode_blocking_info(info: &RadrootsSimplexSmpBlockingInfo) -> Vec<u8> { bytes } -fn decode_blocking_info(bytes: &[u8]) -> Option<RadrootsSimplexSmpBlockingInfo> { - let reason = bytes.strip_prefix(b"reason=")?; +fn decode_blocking_info( + bytes: &[u8], +) -> Result<RadrootsSimplexSmpBlockingInfo, RadrootsSimplexSmpProtoError> { + let Some(reason) = bytes.strip_prefix(b"reason=") else { + return Err(invalid_ascii_tag(bytes)); + }; let reason = match reason { b"spam" => RadrootsSimplexSmpBlockingReason::Spam, b"content" => RadrootsSimplexSmpBlockingReason::Content, - raw => RadrootsSimplexSmpBlockingReason::Other(String::from_utf8_lossy(raw).into_owned()), + raw => return Err(invalid_ascii_tag(raw)), }; - Some(RadrootsSimplexSmpBlockingInfo { reason }) + Ok(RadrootsSimplexSmpBlockingInfo { reason }) } fn decode_short_string_lossy(bytes: &[u8]) -> Result<String, RadrootsSimplexSmpProtoError> { @@ -1678,6 +1692,10 @@ fn decode_short_string_lossy(bytes: &[u8]) -> Result<String, RadrootsSimplexSmpP Ok(value) } +fn invalid_ascii_tag(bytes: &[u8]) -> RadrootsSimplexSmpProtoError { + RadrootsSimplexSmpProtoError::InvalidTag(String::from_utf8_lossy(bytes).into_owned()) +} + fn parse_tag(bytes: &[u8]) -> Result<(Vec<u8>, &[u8]), RadrootsSimplexSmpProtoError> { if bytes.is_empty() { return Err(RadrootsSimplexSmpProtoError::UnexpectedEof); @@ -2548,6 +2566,42 @@ mod tests { } #[test] + fn top_level_unknown_error_tags_stay_opaque() { + assert_eq!( + decode_error(b"FUTURE"), + Ok(RadrootsSimplexSmpError::Other(b"FUTURE".to_vec())) + ); + } + + #[test] + fn malformed_nested_proxy_error_fails_decode() { + assert_eq!( + decode_error(b"PROXY BROKER TRANSPORT HANDSHAKE UNKNOWN"), + Err(RadrootsSimplexSmpProtoError::InvalidTag( + "UNKNOWN".to_string(), + )) + ); + } + + #[test] + fn malformed_blocked_reason_fails_decode() { + assert_eq!( + decode_error(b"BLOCKED reason=custom"), + Err(RadrootsSimplexSmpProtoError::InvalidTag( + "custom".to_string(), + )) + ); + } + + #[test] + fn malformed_network_detail_fails_decode() { + assert_eq!( + decode_broker_error(b"NETWORK CONNECT"), + Err(RadrootsSimplexSmpProtoError::UnexpectedEof) + ); + } + + #[test] fn round_trips_proxy_forward_commands() { let pfwd = RadrootsSimplexSmpCommandTransmission { authorization: Vec::new(),