diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 6dfc413..f550088 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -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 diff --git a/Cargo.lock b/Cargo.lock index db27b18..c66baca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1539,6 +1539,7 @@ dependencies = [ "protobuf-codegen", "protoc-bin-vendored", "rand", + "regex", "test-case", "thiserror", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 3691dc7..1c5a89b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] } diff --git a/src/uri.rs b/src/uri.rs index a7f3cef..3366137 100644 --- a/src/uri.rs +++ b/src/uri.rs @@ -16,6 +16,7 @@ use std::hash::{Hash, Hasher}; use std::str::FromStr; +use std::sync::LazyLock; use uriparse::{Authority, URIReference}; @@ -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 = + LazyLock::new(|| regex::Regex::new(r"^[a-z0-9\-._~]{0,128}$").unwrap()); + #[derive(Debug)] pub enum UUriError { SerializationError(String), @@ -82,7 +86,7 @@ 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, @@ -90,7 +94,7 @@ impl From<&UUri> for String { /// }; /// /// 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) @@ -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] @@ -306,7 +310,7 @@ 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, @@ -314,7 +318,7 @@ impl UUri { /// }; /// /// 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] @@ -455,7 +459,7 @@ impl UUri { // [impl->dsn~uri-authority-name-length~1] // [impl->dsn~uri-host-only~2] - fn verify_authority(authority: &str) -> Result { + pub(crate) fn verify_authority(authority: &str) -> Result { Authority::try_from(authority) .map_err(|e| UUriError::validation_error(format!("invalid authority: {e}"))) .and_then(|auth| Self::verify_parsed_authority(&auth)) @@ -463,7 +467,7 @@ impl UUri { // [impl->dsn~uri-authority-name-length~1] // [impl->dsn~uri-host-only~2] - fn verify_parsed_authority(auth: &Authority) -> Result { + pub(crate) fn verify_parsed_authority(auth: &Authority) -> Result { if auth.has_port() { Err(UUriError::validation_error( "uProtocol URI's authority must not contain port", @@ -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()) + } } } } @@ -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()); /// ``` @@ -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(); @@ -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(); @@ -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] @@ -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()); @@ -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::()); + 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::()); - 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::(); - assert!(UUri::try_from_parts(&authority, 0xa100, 0x01, 0x6501).is_err()); - - let mut authority = ['a'; 126].iter().collect::(); - // 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()); } } diff --git a/src/utransport.rs b/src/utransport.rs index 5a627a4..11c6338 100644 --- a/src/utransport.rs +++ b/src/utransport.rs @@ -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() { @@ -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) { assert!(verify_filter_criteria(&source_filter, sink_filter.as_ref()) .is_err_and(|err| matches!(err.get_code(), UCode::INVALID_ARGUMENT))); } diff --git a/tests/features/uuri_pattern_matching.feature b/tests/features/uuri_pattern_matching.feature new file mode 100644 index 0000000..62d8d3f --- /dev/null +++ b/tests/features/uuri_pattern_matching.feature @@ -0,0 +1,71 @@ +# +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-FileType: SOURCE +# SPDX-License-Identifier: Apache-2.0 +# +Feature: Matching endpoint identifiers (UUri) against patterns + + Scenario Outline: + Developers using a uProtocol language library should be able to verify that a specific + endpoint identifier matches a given pattern as specified by the UUri specification. + + [utest->dsn~uri-pattern-matching~2] + + Given a URI string + When deserializing the URI to a UUri + Then the UUri matches pattern + + Examples: + | uri | pattern | + | "/1/1/A1FB" | /1/1/A1FB | + | "/1/1/A1FB" | //*/1/1/A1FB | + | "/1/1/A1FB" | /FFFF/1/A1FB | + | "/1/1/A1FB" | //*/FFFF/1/A1FB | + | "/1/1/A1FB" | /FFFFFFFF/1/A1FB | + | "/1/1/A1FB" | //*/FFFFFFFF/1/A1FB | + | "/1/1/A1FB" | /1/FF/A1FB | + | "/1/1/A1FB" | //*/1/FF/A1FB | + | "/1/1/A1FB" | /1/1/FFFF | + | "/1/1/A1FB" | //*/1/1/FFFF | + | "/1/1/A1FB" | /FFFFFFFF/FF/FFFF | + | "/1/1/A1FB" | //*/FFFFFFFF/FF/FFFF | + | "/10001/1/A1FB" | /10001/1/A1FB | + | "/10001/1/A1FB" | //*/10001/1/A1FB | + | "/10001/1/A1FB" | /FFFFFFFF/1/A1FB | + | "/10001/1/A1FB" | //*/FFFFFFFF/1/A1FB | + | "/10001/1/A1FB" | /FFFFFFFF/FF/FFFF | + | "/10001/1/A1FB" | //*/FFFFFFFF/FF/FFFF | + | "//vcu.my_vin/1/1/A1FB" | //vcu.my_vin/1/1/A1FB | + | "//vcu.my_vin/1/1/A1FB" | //*/1/1/A1FB | + + Scenario Outline: + Developers using a uProtocol language library should be able to verify that a specific + endpoint identifier does not match a given pattern as specified by the UUri specification. + + [utest->dsn~uri-pattern-matching~2] + + Given a URI string + When deserializing the URI to a UUri + Then the UUri does not match pattern + + Examples: + | uri | pattern | + | "/1/1/A1FB" | //mcu1/1/1/A1FB | + | "//vcu.my_vin/1/1/A1FB" | //mcu1/1/1/A1FB | + | "//vcu/B1A5/1/A1FB" | //vc/FFFFFFFF/FF/FFFF | + | "/B1A5/1/A1FB" | //*/25B1/FF/FFFF | + | "/B1A5/1/A1FB" | //*/FFFFFFFF/2/FFFF | + | "/B1A5/1/A1FB" | //*/FFFFFFFF/FF/ABCD | + | "/B1A5/1/A1FB" | /25B1/1/A1FB | + | "/B1A5/1/A1FB" | /2B1A5/1/A1FB | + | "/10B1A5/1/A1FB" | /40B1A5/1/A1FB | + | "/B1A5/1/A1FB" | /B1A5/4/A1FB | + | "/B1A5/1/A1FB" | /B1A5/1/90FB | diff --git a/tests/features/uuri_protobuf_serialization.feature b/tests/features/uuri_protobuf_serialization.feature new file mode 100644 index 0000000..42a4da0 --- /dev/null +++ b/tests/features/uuri_protobuf_serialization.feature @@ -0,0 +1,50 @@ +# +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-FileType: SOURCE +# SPDX-License-Identifier: Apache-2.0 +# +Feature: Efficient binary encoding of endpoint identifiers (UUri) + + Scenario Outline: + Developers using a uProtocol language library should be able to get the binary + encoding of a UUri instance as specified by the UUri proto3 definition file. + + [utest->req~uri-data-model-proto~1] + + The byte sequences representing the Protocol Buffer encodings have been created + using https://www.protobufpal.com/ based on the UUri proto3 definition from the + uProtocol specification. + + Note that comparing the serialized Protobuf to the byte sequence is not feasible + due to the fact that Proto serialization is not intended/designed to be canonical, + as outlined here: https://protobuf.dev/programming-guides/serialization-not-canonical/ + + Given a UUri having authority + And having entity identifier + And having major version + And having resource identifier + When serializing the UUri to its protobuf wire format + Then the original UUri can be recreated from the protobuf wire format + And the same UUri can be deserialized from + + Examples: + | authority_name | entity_id | version | resource_id | byte_sequence | + | "" | 0x00000001 | 0x01 | 0xa1fb | 1001180120fbc302 | + | "my_vin" | 0x10000001 | 0x02 | 0x001a | 0a066d795f76696e1081808080011802201a | + | "*" | 0x00000101 | 0xa0 | 0xa1fb | 0a012a10810218a00120fbc302 | + | "mcu1" | 0x0000FFFF | 0x01 | 0xa1fb | 0a046d63753110ffff03180120fbc302 | + | "vcu.my_vin" | 0x01a40101 | 0x01 | 0x8000 | 0a0a7663752e6d795f76696e108182900d180120808002 | + | "vcu.my_vin" | 0xFFFF0101 | 0x01 | 0xa1fb | 0a0a7663752e6d795f76696e108182fcff0f180120fbc302 | + | "vcu.my_vin" | 0xFFFFFFFF | 0x01 | 0xa1fb | 0a0a7663752e6d795f76696e10ffffffff0f180120fbc302 | + | "vcu.my_vin" | 0x00000101 | 0x00 | 0xa1fb | 0a0a7663752e6d795f76696e10810220fbc302 | + | "vcu.my_vin" | 0x00000101 | 0xFF | 0xa1fb | 0a0a7663752e6d795f76696e10810218ff0120fbc302 | + | "vcu.my_vin" | 0x00000101 | 0x01 | 0x0000 | 0a0a7663752e6d795f76696e1081021801 | + | "vcu.my_vin" | 0x00000101 | 0x01 | 0xFFFF | 0a0a7663752e6d795f76696e108102180120ffff03 | diff --git a/tests/features/uuri_uri_serialization.feature b/tests/features/uuri_uri_serialization.feature new file mode 100644 index 0000000..b007428 --- /dev/null +++ b/tests/features/uuri_uri_serialization.feature @@ -0,0 +1,92 @@ +# +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-FileType: SOURCE +# SPDX-License-Identifier: Apache-2.0 +# +Feature: String representation of endpoint identfiers (UUri) + + Scenario Outline: + Developers using a uProtocol language library should be able to get the URI + string representation of a UUri instance as specified by the UUri specification. + + [utest->req~uri-serialization~1] + [utest->dsn~uri-scheme~1] + [utest->dsn~uri-host-only~2] + [utest->dsn~uri-authority-mapping~1] + [utest->dsn~uri-path-mapping~1] + + Given a UUri having authority + And having entity identifier + And having major version + And having resource identifier + When serializing the UUri to a URI + Then the resulting URI string is + And the original UUri can be recreated from the URI string + + Examples: + | authority_name | entity_id | version | resource_id | uri_string | + | "" | 0x00000001 | 0x01 | 0xa1fb | up:/1/1/A1FB | + | "192.168.1.1" | 0x00000001 | 0x01 | 0xa1fb | up://192.168.1.1/1/1/A1FB | + | "[2001::7]" | 0x00000001 | 0x01 | 0xa1fb | up://[2001::7]/1/1/A1FB | + | "my_vin" | 0x10000001 | 0x02 | 0x001a | up://my_vin/10000001/2/1A | + | "*" | 0x00000101 | 0xa0 | 0xa1fb | up://*/101/A0/A1FB | + | "mcu1" | 0x0000FFFF | 0x01 | 0xa1fb | up://mcu1/FFFF/1/A1FB | + | "vcu.my_vin" | 0x01a40101 | 0x01 | 0x8000 | up://vcu.my_vin/1A40101/1/8000 | + | "vcu.my_vin" | 0xFFFF0101 | 0x01 | 0xa1fb | up://vcu.my_vin/FFFF0101/1/A1FB | + | "vcu.my_vin" | 0xFFFFFFFF | 0x01 | 0xa1fb | up://vcu.my_vin/FFFFFFFF/1/A1FB | + | "vcu.my_vin" | 0x00000101 | 0x00 | 0xa1fb | up://vcu.my_vin/101/0/A1FB | + | "vcu.my_vin" | 0x00000101 | 0xFF | 0xa1fb | up://vcu.my_vin/101/FF/A1FB | + | "vcu.my_vin" | 0x00000101 | 0x01 | 0x0000 | up://vcu.my_vin/101/1/0 | + | "vcu.my_vin" | 0x00000101 | 0x01 | 0xFFFF | up://vcu.my_vin/101/1/FFFF | + + Scenario Outline: + Developers using a uProtocol language library should not be able to create a UUri from a + URI string that does not comply with the UUri specification. + + [utest->req~uri-serialization~1] + [utest->dsn~uri-scheme~1] + [utest->dsn~uri-host-only~2] + [utest->dsn~uri-authority-mapping~1] + [utest->dsn~uri-path-mapping~1] + + Given a URI string + When deserializing the URI to a UUri + Then the attempt fails + + Examples: + | uri_string | reason for failure | + | "" | not a URI | + | " " | not a URI | + | "$$" | not a URI | + | "up:" | not a URI | + | "up:/" | not a URI | + | "/" | not a URI | + | "//" | not a URI | + | "//vcu.my_vin" | just an authority | + | "//vcu.my_vin//1/A1FB" | missing entity ID | + | "//vcu.my_vin/101//A1FB" | missing version | + | "//vcu.my_vin/101/1/" | missing resource ID | + | "up://vcu.my_vin/101/1/A/unexpected" | too many path segments | + | "xy://vcu.my_vin/101/1/A" | unsupported schema | + | "//vcu.my_vin/101/1/A?foo=bar" | URI with query | + | "//vcu.my_vin/101/1/A#foo" | URI with fragment | + | "//VCU.my-vin/101/1/A" | server-based authority with upper-case letters | + | "//vcu.my-vin:1516/101/1/A" | server-based authority with port | + | "//user:pwd@vcu.my-vin/101/1/A" | server-based authority with user info | + | "//[2001:db87aa::8]/101/1/A" | invalid IP literal authority | + | "//MY_VIN/101/1/A" | registry-based authority with uppercase characters | + | "//reg_based:1516/101/1/A" | registry-based authority name with invalid characters | + | "up://vcu.my-vin/1G1/1/A1FB" | non-hex entity ID | + | "/123456789/1/A1FB" | entity ID exceeds max length | + | "up:/101/G/A1FB" | non-hex version | + | "//vcu.my-vin/101/123/A1FB" | version exceeds max length | + | "/101/1/G1FB" | non-hex resource ID | + | "up://vcu.my-vin/101/1/12345" | resource ID exceeds max length | diff --git a/tests/tck_uuri.rs b/tests/tck_uuri.rs index ed805de..7c8ca4b 100644 --- a/tests/tck_uuri.rs +++ b/tests/tck_uuri.rs @@ -19,7 +19,9 @@ use up_rust::UUri; mod common; -const FEATURES_GLOB_PATTERN: &str = "up-spec/basics/uuri_*.feature"; +// TODO: Change back to official location when local changes are available from upstream up-spec +// const FEATURES_GLOB_PATTERN: &str = "up-spec/basics/uuri_*.feature"; +const FEATURES_GLOB_PATTERN: &str = "tests/features/uuri_*.feature"; #[derive(cucumber::World, Default, Debug)] struct UUriWorld { @@ -29,7 +31,7 @@ struct UUriWorld { error: Option>, } -#[given(expr = "a URI string {word}")] +#[given(expr = "a URI string {string}")] async fn with_uri_string(w: &mut UUriWorld, uri_string: String) { w.uri = uri_string; }