Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ jobs:
- name: Run lychee
uses: lycheeverse/lychee-action@v2
with:
args: "--cache --max-cache-age 1d --verbose --no-progress --exclude-path './target/' --exclude-path './up-spec/' -- './**/*.md' './**/*.rs' './**/*.adoc'"
args: "--cache --max-cache-age 1d --verbose --no-progress --exclude-path './target/' --exclude-path './up-spec/' -- './**/*.md' './**/*.rs'"

feature-check:
# Comprehensive check on dependencies for all feature flag combinations, excluding development dependencies
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ mediatype = "0.20"
mockall = { version = "0.13", optional = true }
protobuf = { version = "3.7.2", features = ["with-bytes"] }
rand = { version = "0.9" }
regex = { version = "1.11" }
thiserror = { version = "1.0.69", optional = true }
tokio = { version = "1.44", default-features = false, optional = true }
tracing = { version = "0.1", default-features = false, features = ["log", "std"] }
Expand Down
94 changes: 39 additions & 55 deletions src/uri.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

use std::hash::{Hash, Hasher};
use std::str::FromStr;
use std::sync::LazyLock;

use uriparse::{Authority, URIReference};

Expand All @@ -30,6 +31,9 @@ pub(crate) const WILDCARD_RESOURCE_ID: u32 = 0x0000_FFFF;
pub(crate) const RESOURCE_ID_RESPONSE: u32 = 0;
pub(crate) const RESOURCE_ID_MIN_EVENT: u32 = 0x8000;

static AUTHORITY_NAME_PATTERN: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r"^[a-z0-9\-._~]{0,128}$").unwrap());

#[derive(Debug)]
pub enum UUriError {
SerializationError(String),
Expand Down Expand Up @@ -82,15 +86,15 @@ impl From<&UUri> for String {
/// use up_rust::UUri;
///
/// let uuri = UUri {
/// authority_name: String::from("VIN.vehicles"),
/// authority_name: String::from("vin.vehicles"),
/// ue_id: 0x0000_800A,
/// ue_version_major: 0x02,
/// resource_id: 0x0000_1a50,
/// ..Default::default()
/// };
///
/// let uri_string = String::from(&uuri);
/// assert_eq!(uri_string, "//VIN.vehicles/800A/2/1A50");
/// assert_eq!(uri_string, "//vin.vehicles/800A/2/1A50");
/// ````
fn from(uri: &UUri) -> Self {
UUri::to_uri(uri, false)
Expand Down Expand Up @@ -121,14 +125,14 @@ impl FromStr for UUri {
/// use up_rust::UUri;
///
/// let uri = UUri {
/// authority_name: "VIN.vehicles".to_string(),
/// authority_name: "vin.vehicles".to_string(),
/// ue_id: 0x000A_8000,
/// ue_version_major: 0x02,
/// resource_id: 0x0000_1a50,
/// ..Default::default()
/// };
///
/// let uri_from = UUri::from_str("//VIN.vehicles/A8000/2/1A50").unwrap();
/// let uri_from = UUri::from_str("//vin.vehicles/A8000/2/1A50").unwrap();
/// assert_eq!(uri, uri_from);
/// ````
// [impl->dsn~uri-authority-name-length~1]
Expand Down Expand Up @@ -306,15 +310,15 @@ impl UUri {
/// use up_rust::UUri;
///
/// let uuri = UUri {
/// authority_name: String::from("VIN.vehicles"),
/// authority_name: String::from("vin.vehicles"),
/// ue_id: 0x0000_800A,
/// ue_version_major: 0x02,
/// resource_id: 0x0000_1a50,
/// ..Default::default()
/// };
///
/// let uri_string = uuri.to_uri(true);
/// assert_eq!(uri_string, "up://VIN.vehicles/800A/2/1A50");
/// assert_eq!(uri_string, "up://vin.vehicles/800A/2/1A50");
/// ````
// [impl->dsn~uri-authority-mapping~1]
// [impl->dsn~uri-path-mapping~1]
Expand Down Expand Up @@ -455,15 +459,15 @@ impl UUri {

// [impl->dsn~uri-authority-name-length~1]
// [impl->dsn~uri-host-only~2]
fn verify_authority(authority: &str) -> Result<String, UUriError> {
pub(crate) fn verify_authority(authority: &str) -> Result<String, UUriError> {
Authority::try_from(authority)
.map_err(|e| UUriError::validation_error(format!("invalid authority: {e}")))
.and_then(|auth| Self::verify_parsed_authority(&auth))
}

// [impl->dsn~uri-authority-name-length~1]
// [impl->dsn~uri-host-only~2]
fn verify_parsed_authority(auth: &Authority) -> Result<String, UUriError> {
pub(crate) fn verify_parsed_authority(auth: &Authority) -> Result<String, UUriError> {
if auth.has_port() {
Err(UUriError::validation_error(
"uProtocol URI's authority must not contain port",
Expand All @@ -473,13 +477,20 @@ impl UUri {
"uProtocol URI's authority must not contain userinfo",
))
} else {
let auth_name = auth.host().to_string();
if auth_name.len() <= 128 {
Ok(auth_name)
} else {
Err(UUriError::validation_error(
"URI's authority name must not exceed 128 characters",
))
match auth.host() {
uriparse::Host::IPv4Address(_) | uriparse::Host::IPv6Address(_) => {
Ok(auth.host().to_string())
}
uriparse::Host::RegisteredName(name) => {
if !WILDCARD_AUTHORITY.eq(name.as_str())
&& !AUTHORITY_NAME_PATTERN.is_match(name.as_str())
{
return Err(UUriError::validation_error(
"uProtocol URI's authority contains invalid characters",
));
}
Ok(name.to_string())
}
}
}
}
Expand Down Expand Up @@ -544,7 +555,7 @@ impl UUri {
/// ```rust
/// use up_rust::UUri;
///
/// let uuri = UUri::try_from_parts("MYVIN", 0xa13b, 0x01, 0x7f4e).unwrap();
/// let uuri = UUri::try_from_parts("myvin", 0xa13b, 0x01, 0x7f4e).unwrap();
/// assert!(!uuri.is_empty());
/// assert!(UUri::default().is_empty());
/// ```
Expand All @@ -565,8 +576,8 @@ impl UUri {
/// use std::str::FromStr;
/// use up_rust::UUri;
///
/// let authority_a = UUri::from_str("up://Authority.A/100A/1/0").unwrap();
/// let authority_b = UUri::from_str("up://Authority.B/200B/2/20").unwrap();
/// let authority_a = UUri::from_str("up://authority.a/100A/1/0").unwrap();
/// let authority_b = UUri::from_str("up://authority.b/200B/2/20").unwrap();
/// assert!(authority_a.is_remote(&authority_b));
///
/// let authority_local = UUri::from_str("up:///100A/1/0").unwrap();
Expand Down Expand Up @@ -594,8 +605,8 @@ impl UUri {
/// use std::str::FromStr;
/// use up_rust::UUri;
///
/// let authority_a = UUri::from_str("up://Authority.A/100A/1/0").unwrap();
/// let authority_b = "Authority.B".to_string();
/// let authority_a = UUri::from_str("up://authority.a/100A/1/0").unwrap();
/// let authority_b = "authority.b".to_string();
/// assert!(authority_a.is_remote_authority(&authority_b));
///
/// let authority_local = "".to_string();
Expand Down Expand Up @@ -945,8 +956,8 @@ impl UUri {
/// ```rust
/// use up_rust::UUri;
///
/// let pattern = UUri::try_from("//VIN/A14F/3/FFFF").unwrap();
/// let candidate = UUri::try_from("//VIN/A14F/3/B1D4").unwrap();
/// let pattern = UUri::try_from("//vin/A14F/3/FFFF").unwrap();
/// let candidate = UUri::try_from("//vin/A14F/3/B1D4").unwrap();
/// assert!(pattern.matches(&candidate));
/// ```
// [impl->dsn~uri-pattern-matching~2]
Expand Down Expand Up @@ -1003,10 +1014,10 @@ mod tests {
}

#[test_case("//*/A100/1/1"; "for any authority")]
#[test_case("//VIN/FFFF/1/1"; "for any entity type")]
#[test_case("//VIN/FFFF0ABC/1/1"; "for any entity instance")]
#[test_case("//VIN/A100/FF/1"; "for any version")]
#[test_case("//VIN/A100/1/FFFF"; "for any resource")]
#[test_case("//vin/FFFF/1/1"; "for any entity type")]
#[test_case("//vin/FFFF0ABC/1/1"; "for any entity instance")]
#[test_case("//vin/A100/FF/1"; "for any version")]
#[test_case("//vin/A100/1/FFFF"; "for any resource")]
fn test_verify_no_wildcards_fails(uri: &str) {
let uuri = UUri::try_from(uri).expect("should have been able to deserialize URI");
assert!(uuri.verify_no_wildcards().is_err());
Expand All @@ -1015,35 +1026,8 @@ mod tests {
// [utest->dsn~uri-authority-name-length~1]
#[test]
fn test_from_str_fails_for_authority_exceeding_max_length() {
let host_name = ['a'; 129];
let uri = format!("//{}/A100/1/6501", host_name.iter().collect::<String>());
let host_name = "a".repeat(129);
let uri = format!("//{}/A100/1/6501", host_name);
assert!(UUri::from_str(&uri).is_err());

let host_name = ['a'; 126];
// add single percent encoded character
// this should result in a 129 character host
let uri = format!("//{}%42/A100/1/6501", host_name.iter().collect::<String>());
assert!(UUri::from_str(&uri).is_err());
}

// [utest->dsn~uri-authority-name-length~1]
#[test]
fn test_try_from_parts_fails_for_authority_exceeding_max_length() {
let authority = ['a'; 129].iter().collect::<String>();
assert!(UUri::try_from_parts(&authority, 0xa100, 0x01, 0x6501).is_err());

let mut authority = ['a'; 126].iter().collect::<String>();
// add single percent encoded character
// this should result in a 129 character host
authority.push_str("%42");
assert!(UUri::try_from_parts(&authority, 0xa100, 0x01, 0x6501).is_err());
}

// [utest->dsn~uri-host-only~2]
#[test_case("MYVIN:1000"; "with port")]
#[test_case("user:pwd@MYVIN"; "with userinfo")]
#[test_case("MY%VIN"; "with reserved character")]
fn test_try_from_parts_fails_for_invalid_authority(authority: &str) {
assert!(UUri::try_from_parts(authority, 0xa100, 0x01, 0x6501).is_err());
}
}
79 changes: 72 additions & 7 deletions src/utransport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,20 @@ pub fn verify_filter_criteria(
source_filter: &UUri,
sink_filter: Option<&UUri>,
) -> Result<(), UStatus> {
UUri::check_validity(source_filter).map_err(|err| {
UStatus::fail_with_code(
UCode::INVALID_ARGUMENT,
format!("invalid source filter URI: {err}"),
)
})?;
if let Some(sink_filter_uuri) = sink_filter {
UUri::check_validity(sink_filter_uuri).map_err(|err| {
UStatus::fail_with_code(
UCode::INVALID_ARGUMENT,
format!("invalid sink filter URI: {err}"),
)
})?;

if sink_filter_uuri.is_notification_destination()
&& source_filter.is_notification_destination()
{
Expand Down Expand Up @@ -589,20 +602,72 @@ mod tests {
}

#[test_case::test_case(
"//vehicle1/AA/1/0",
"//vehicle1/AA/1/FFFF",
Some("//vehicle2/BB/1/FFFF");
"source and sink both having wildcard resource ID")]
#[test_case::test_case(
"//vehicle1/AA/1/9000",
Some("//vehicle2/BB/1/0");
"sending notification")]
#[test_case::test_case(
"//vehicle1/AA/1/0",
Some("//vehicle2/BB/1/1");
"RPC method invocation")]
#[test_case::test_case(
"//vehicle1/AA/1/FFFF",
Some("//vehicle2/BB/1/1");
"receiving RPC requests using wildcard resource ID")]
#[test_case::test_case(
"//vehicle1/AA/1/0",
Some("//vehicle2/BB/1/1");
"receiving RPC requests using default resource ID")]
#[test_case::test_case(
"//vehicle1/AA/1/9000",
None;
"receiving events published to specific topic")]
#[test_case::test_case(
"//vehicle1/AA/1/FFFF",
None;
"receiving events published to any topic")]
fn test_verify_filter_criteria_succeeds_for(source: &str, sink: Option<&str>) {
let source_filter = UUri::from_str(source).expect("invalid source URI");
let sink_filter = sink.map(|s| UUri::from_str(s).expect("invalid sink URI"));
assert!(verify_filter_criteria(&source_filter, sink_filter.as_ref()).is_ok());
}

#[test_case::test_case(
UUri::from_str("//vehicle1/AA/1/0").unwrap(),
Some(UUri::from_str("//vehicle2/BB/1/0").unwrap());
"source and sink both having resource ID 0")]
#[test_case::test_case(
"//vehicle1/AA/1/CC",
Some("//vehicle2/BB/1/1A");
UUri::from_str("//vehicle1/AA/1/CC").unwrap(),
Some(UUri::from_str("//vehicle2/BB/1/1A").unwrap());
"sink is RPC but source has invalid resource ID")]
#[test_case::test_case(
"//vehicle1/AA/1/CC",
UUri::from_str("//vehicle1/AA/1/CC").unwrap(),
None;
"sink is empty but source has non-topic resource ID")]
fn test_verify_filter_criteria_fails_for(source: &str, sink: Option<&str>) {
let source_filter = UUri::from_str(source).expect("invalid source URI");
let sink_filter = sink.map(|s| UUri::from_str(s).expect("invalid sink URI"));
#[test_case::test_case(
UUri {
authority_name: "VEHICLE1".to_string(),
ue_id: 0x00AA,
ue_version_major: 0x01,
resource_id: 0x9000,
..Default::default()
},
None;
"source has upper-case authority")]
#[test_case::test_case(
UUri::from_str("//vehicle1/AA/1/9000").unwrap(),
Some(UUri {
authority_name: "VEHICLE2".to_string(),
ue_id: 0x00BB,
ue_version_major: 0x01,
resource_id: 0x0000,
..Default::default()
});
"sink has upper-case authority")]
fn test_verify_filter_criteria_fails_for(source_filter: UUri, sink_filter: Option<UUri>) {
assert!(verify_filter_criteria(&source_filter, sink_filter.as_ref())
.is_err_and(|err| matches!(err.get_code(), UCode::INVALID_ARGUMENT)));
}
Expand Down
Loading
Loading