Preview of the proposed up-rust native-frame-model branch (up-rust b6b99c6d, up-spec f0e9b17) — not released documentation. branch · write-up

up_rust/
uri.rs

1/********************************************************************************
2 * Copyright (c) 2023 Contributors to the Eclipse Foundation
3 *
4 * See the NOTICE file(s) distributed with this work for additional
5 * information regarding copyright ownership.
6 *
7 * This program and the accompanying materials are made available under the
8 * terms of the Apache License Version 2.0 which is available at
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * SPDX-License-Identifier: Apache-2.0
12 ********************************************************************************/
13
14/*!
15Implementation of the [uProtocol URI (UUri) data model](https://github.com/eclipse-uprotocol/up-spec/blob/v1.6.0-alpha.7/basics/uri.adoc).
16A UUri represents a uProtocol resource identifier and is used in various places across the uProtocol specification, e.g., to identify the source and destination of messages or to identify resources that are being accessed via RPC calls.
17*/
18
19// [impl->dsn~uri-data-model-naming~1]
20// [impl->req~uri-data-model-proto~1]
21
22use std::hash::{Hash, Hasher};
23use std::ops::Deref;
24use std::str::FromStr;
25use std::sync::LazyLock;
26
27use uriparse::{Authority, URIReference};
28
29pub(crate) const WILDCARD_AUTHORITY: &str = "*";
30pub(crate) const WILDCARD_ENTITY_INSTANCE: u32 = 0xFFFF_0000;
31pub(crate) const WILDCARD_ENTITY_TYPE: u32 = 0x0000_FFFF;
32pub(crate) const WILDCARD_ENTITY_VERSION: u8 = 0xFF;
33pub(crate) const WILDCARD_RESOURCE_ID: u16 = 0xFFFF;
34
35pub(crate) const RESOURCE_ID_RESPONSE: u16 = 0x0000;
36pub(crate) const RESOURCE_ID_MIN_EVENT: u16 = 0x8000;
37
38const AUTHORITY_NAME_MAX_LENGTH: usize = 128;
39static AUTHORITY_NAME_PATTERN: LazyLock<regex::Regex> = LazyLock::new(|| {
40    let regex = format!(r"^[a-z0-9\-._~]{{0,{}}}$", AUTHORITY_NAME_MAX_LENGTH);
41    regex::Regex::new(&regex).unwrap()
42});
43
44type AuthorityNameString = String;
45
46/// An error indicating a problem with creating or parsing a UUri.
47#[derive(Debug)]
48#[non_exhaustive]
49pub enum UUriError {
50    /// Indicates that a given URI string cannot be parsed into a UUri due to invalid formatting or content.
51    SerializationError(String),
52    /// Indicates that a given URI does not comply with the UUri specification.
53    ValidationError(String),
54}
55
56impl UUriError {
57    /// Creates a serialization error with the given message.
58    pub fn serialization_error<T>(message: T) -> UUriError
59    where
60        T: Into<String>,
61    {
62        Self::SerializationError(message.into())
63    }
64
65    /// Creates a validation error with the given message.
66    pub fn validation_error<T>(message: T) -> UUriError
67    where
68        T: Into<String>,
69    {
70        Self::ValidationError(message.into())
71    }
72}
73
74impl std::fmt::Display for UUriError {
75    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76        match self {
77            Self::SerializationError(e) => f.write_fmt(format_args!("Serialization error: {e}")),
78            Self::ValidationError(e) => f.write_fmt(format_args!("Validation error: {e}")),
79        }
80    }
81}
82
83impl std::error::Error for UUriError {}
84
85/// A URI that represents a uProtocol resource identifier.
86#[derive(Debug, Clone, PartialEq)]
87#[repr(C)]
88pub struct UUri {
89    authority_name: AuthorityNameString,
90    ue_id: u32,
91    ue_version_major: u8,
92    resource_id: u16,
93}
94
95impl std::fmt::Display for UUri {
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        write!(f, "{}", UUri::to_uri(self, true))
98    }
99}
100
101// [impl->req~uri-serialization~1]
102impl From<&UUri> for String {
103    /// Serializes a uProtocol URI to a URI string.
104    ///
105    /// # Arguments
106    ///
107    /// * `uri` - The URI to serialize. Note that the given URI is **not** validated before serialization.
108    ///   In particular, the URI's version and resource ID length are not checked to be within limits.
109    ///
110    /// # Returns
111    ///
112    /// The output of [`UUri::to_uri`] without including the uProtocol scheme.
113    ///
114    /// # Examples
115    ///
116    /// ```rust
117    /// use up_rust::UUri;
118    ///
119    /// let uuri = UUri::try_from_parts("vin.vehicles", 0x0000_800A, 0x02, 0x0000_1a50).unwrap();
120    /// let uri_string = String::from(&uuri);
121    /// assert_eq!(uri_string, "//vin.vehicles/800A/2/1A50");
122    /// ````
123    fn from(uri: &UUri) -> Self {
124        UUri::to_uri(uri, false)
125    }
126}
127
128impl FromStr for UUri {
129    type Err = UUriError;
130
131    /// Attempts to parse a `String` into a `UUri`.
132    ///
133    /// As part of the parsing, the _authority_ of the URI is getting normalized. This means that all characters
134    /// are converted to lowercase, no bytes that are in the unreserved character set remain percent-encoded,
135    /// and all alphabetical characters in percent-encodings are converted to uppercase.
136    ///
137    /// # Arguments
138    ///
139    /// * `uri` - The `String` to be converted into a `UUri`.
140    ///
141    /// # Returns
142    ///
143    /// A `Result` containing either the `UUri` representation of the URI or a `SerializationError`.
144    ///
145    /// # Examples
146    ///
147    /// ```rust
148    /// use std::str::FromStr;
149    /// use up_rust::UUri;
150    ///
151    /// let uri = UUri::try_from_parts("vin.vehicles", 0x000A_8000, 0x02, 0x0000_1a50).unwrap();
152    /// let uri_from = UUri::from_str("//vin.vehicles/A8000/2/1A50").unwrap();
153    /// assert_eq!(uri, uri_from);
154    /// ````
155    // [impl->dsn~uri-authority-name-length~1]
156    // [impl->dsn~uri-scheme~1]
157    // [impl->dsn~uri-host-only~2]
158    // [impl->dsn~uri-authority-mapping~1]
159    // [impl->dsn~uri-path-mapping~2]
160    // [impl->req~uri-serialization~1]
161    fn from_str(uri: &str) -> Result<Self, Self::Err> {
162        if uri.is_empty() {
163            return Err(UUriError::serialization_error("URI is empty"));
164        }
165        let parsed_uri = URIReference::try_from(uri)
166            .map_err(|e| UUriError::serialization_error(e.to_string()))?;
167
168        if let Some(scheme) = parsed_uri.scheme() {
169            if scheme.ne("up") {
170                return Err(UUriError::serialization_error(
171                    "uProtocol URI must use 'up' scheme",
172                ));
173            }
174        }
175        if parsed_uri.has_query() {
176            return Err(UUriError::serialization_error(
177                "uProtocol URI must not contain query",
178            ));
179        }
180        if parsed_uri.has_fragment() {
181            return Err(UUriError::serialization_error(
182                "uProtocol URI must not contain fragment",
183            ));
184        }
185        let authority_name = parsed_uri.authority().map_or(
186            Ok(AuthorityNameString::default()),
187            Self::verify_parsed_authority,
188        )?;
189
190        let path_segments = parsed_uri.path().segments();
191        match path_segments {
192            [entity, version, resource] => {
193                if entity.is_empty() {
194                    return Err(UUriError::serialization_error(
195                        "URI must contain non-empty entity ID",
196                    ));
197                }
198                let ue_id = u32::from_str_radix(entity, 16).map_err(|e| {
199                    UUriError::serialization_error(format!("Cannot parse entity ID: {e}"))
200                })?;
201                if version.is_empty() {
202                    return Err(UUriError::serialization_error(
203                        "URI must contain non-empty entity version",
204                    ));
205                }
206                let ue_version_major = u8::from_str_radix(version, 16).map_err(|e| {
207                    UUriError::serialization_error(format!("Cannot parse entity version: {e}"))
208                })?;
209                if resource.is_empty() {
210                    return Err(UUriError::serialization_error(
211                        "URI must contain non-empty resource ID",
212                    ));
213                }
214                let resource_id = u16::from_str_radix(resource, 16).map_err(|e| {
215                    UUriError::serialization_error(format!("Cannot parse resource ID: {e}"))
216                })?;
217
218                Ok(UUri {
219                    authority_name,
220                    ue_id,
221                    ue_version_major,
222                    resource_id,
223                })
224            }
225            _ => Err(UUriError::serialization_error(
226                "uProtocol URI must contain entity ID, entity version and resource ID",
227            )),
228        }
229    }
230}
231
232// [impl->req~uri-serialization~1]
233impl TryFrom<String> for UUri {
234    type Error = UUriError;
235
236    /// Attempts to serialize a `String` into a `UUri`.
237    ///
238    /// # Arguments
239    ///
240    /// * `uri` - The `String` to be converted into a `UUri`.
241    ///
242    /// # Returns
243    ///
244    /// A `Result` containing either the `UUri` representation of the URI or a `SerializationError`.
245    ///
246    /// # Examples
247    ///
248    /// ```rust
249    /// use up_rust::UUri;
250    ///
251    /// let uri = UUri::try_from_parts("", 0x001A_8000, 0x02, 0x1a50).unwrap();
252    /// let uri_from = UUri::try_from("/1A8000/2/1A50".to_string()).unwrap();
253    /// assert_eq!(uri, uri_from);
254    /// ````
255    fn try_from(uri: String) -> Result<Self, Self::Error> {
256        UUri::from_str(uri.as_str())
257    }
258}
259
260// [impl->req~uri-serialization~1]
261impl TryFrom<&str> for UUri {
262    type Error = UUriError;
263
264    /// Attempts to serialize a `String` into a `UUri`.
265    ///
266    /// # Arguments
267    ///
268    /// * `uri` - The `String` to be converted into a `UUri`.
269    ///
270    /// # Returns
271    ///
272    /// A `Result` containing either the `UUri` representation of the URI or a `SerializationError`.
273    ///
274    /// # Examples
275    ///
276    /// ```rust
277    /// use up_rust::UUri;
278    ///
279    /// let uri = UUri::try_from_parts("", 0x001A_8000, 0x02, 0x1a50).unwrap();
280    /// let uri_from = UUri::try_from("/1A8000/2/1A50").unwrap();
281    /// assert_eq!(uri, uri_from);
282    /// ````
283    fn try_from(uri: &str) -> Result<Self, Self::Error> {
284        UUri::from_str(uri)
285    }
286}
287
288impl Hash for UUri {
289    fn hash<H: Hasher>(&self, state: &mut H) {
290        self.authority_name.hash(state);
291        self.ue_id.hash(state);
292        self.ue_version_major.hash(state);
293        self.resource_id.hash(state);
294    }
295}
296
297impl Eq for UUri {}
298
299/// A [`UUri`] that has been checked to contain no wildcard components.
300///
301/// This proof type is for APIs where exact-vs-wildcard URI behavior is
302/// materially different. It only carries the result of
303/// [`UUri::verify_no_wildcards`]; it does not imply physical transport
304/// prefiltering or no-copy behavior.
305#[derive(Debug, Clone, Eq, Hash, PartialEq)]
306pub struct ExactUUri(UUri);
307
308impl ExactUUri {
309    /// Creates an exact URI proof from a URI with no wildcard components.
310    ///
311    /// # Errors
312    ///
313    /// Returns the same validation error as [`UUri::verify_no_wildcards`] when
314    /// any URI component contains a wildcard.
315    pub fn try_new(uri: UUri) -> Result<Self, UUriError> {
316        uri.verify_no_wildcards()?;
317        Ok(Self(uri))
318    }
319
320    /// Borrows the checked URI.
321    #[must_use]
322    pub fn as_uuri(&self) -> &UUri {
323        &self.0
324    }
325
326    /// Consumes this proof and returns the underlying URI.
327    #[must_use]
328    pub fn into_inner(self) -> UUri {
329        self.0
330    }
331}
332
333impl AsRef<UUri> for ExactUUri {
334    fn as_ref(&self) -> &UUri {
335        self.as_uuri()
336    }
337}
338
339impl Deref for ExactUUri {
340    type Target = UUri;
341
342    fn deref(&self) -> &Self::Target {
343        self.as_uuri()
344    }
345}
346
347impl From<ExactUUri> for UUri {
348    fn from(value: ExactUUri) -> Self {
349        value.into_inner()
350    }
351}
352
353impl TryFrom<UUri> for ExactUUri {
354    type Error = UUriError;
355
356    fn try_from(value: UUri) -> Result<Self, Self::Error> {
357        Self::try_new(value)
358    }
359}
360
361impl TryFrom<&UUri> for ExactUUri {
362    type Error = UUriError;
363
364    fn try_from(value: &UUri) -> Result<Self, Self::Error> {
365        Self::try_new(value.clone())
366    }
367}
368
369impl PartialEq<str> for UUri {
370    fn eq(&self, other: &str) -> bool {
371        match UUri::from_str(other) {
372            Ok(other_uri) => self.eq(&other_uri),
373            Err(_) => false,
374        }
375    }
376}
377
378impl PartialEq<&str> for UUri {
379    fn eq(&self, other: &&str) -> bool {
380        self.eq(*other)
381    }
382}
383
384impl PartialEq<String> for UUri {
385    fn eq(&self, other: &String) -> bool {
386        self.eq(other.as_str())
387    }
388}
389
390impl UUri {
391    /// Serializes this UUri to a URI string.
392    ///
393    /// # Arguments
394    ///
395    /// * `include_scheme` - Indicates whether to include the uProtocol scheme (`up`) in the URI.
396    ///
397    /// # Returns
398    ///
399    /// The URI as defined by the [uProtocol Specification](https://github.com/eclipse-uprotocol/up-spec).
400    ///
401    /// # Examples
402    ///
403    /// ```rust
404    /// use up_rust::UUri;
405    ///
406    /// let uuri = UUri::try_from_parts("vin.vehicles", 0x0000_800A, 0x02, 0x1a50).unwrap();
407    /// let uri_string = uuri.to_uri(true);
408    /// assert_eq!(uri_string, "up://vin.vehicles/800A/2/1A50");
409    /// ````
410    // [impl->dsn~uri-authority-mapping~1]
411    // [impl->dsn~uri-path-mapping~2]
412    // [impl->req~uri-serialization~1]
413    #[must_use]
414    pub fn to_uri(&self, include_scheme: bool) -> String {
415        let mut output = String::default();
416        if include_scheme {
417            output.push_str("up:");
418        }
419        if !self.authority_name.is_empty() {
420            output.push_str("//");
421            output.push_str(self.authority_name.as_str());
422        }
423        let uri = format!(
424            "/{:X}/{:X}/{:X}",
425            self.ue_id, self.ue_version_major, self.resource_id
426        );
427        output.push_str(&uri);
428        output
429    }
430
431    /// Creates a new UUri from its parts.
432    ///
433    /// # Errors
434    ///
435    /// Returns a [`UUriError::ValidationError`] if the authority does not comply with the UUri specification.
436    ///
437    /// # Examples
438    ///
439    /// ```rust
440    /// use up_rust::UUri;
441    ///
442    /// assert!(UUri::try_from_parts("vin", 0x0000_5a6b, 0x01, 0x0001).is_ok());
443    /// ```
444    // [impl->dsn~uri-authority-name-length~1]
445    // [impl->dsn~uri-host-only~2]
446    pub fn try_from_parts(
447        authority: &str,
448        entity_id: u32,
449        entity_version: u8,
450        resource_id: u16,
451    ) -> Result<Self, UUriError> {
452        let authority_name = Self::verify_authority_name(authority)?;
453        Ok(UUri {
454            authority_name,
455            ue_id: entity_id,
456            ue_version_major: entity_version,
457            resource_id,
458        })
459    }
460
461    /// Gets a pattern URI that [matches](UUri::matches) any given candidate URI.
462    #[must_use]
463    pub fn any() -> Self {
464        Self::any_with_resource_id(WILDCARD_RESOURCE_ID)
465    }
466
467    /// Gets a pattern URI that [matches](UUri::matches) any given candidate URI with a specific resource ID.
468    #[must_use]
469    pub fn any_with_resource_id(resource_id: u16) -> Self {
470        UUri {
471            authority_name: AuthorityNameString::from(WILDCARD_AUTHORITY),
472            ue_id: WILDCARD_ENTITY_INSTANCE | WILDCARD_ENTITY_TYPE,
473            ue_version_major: WILDCARD_ENTITY_VERSION,
474            resource_id,
475        }
476    }
477
478    /// Gets the authority name part from this uProtocol URI.
479    ///
480    /// # Examples
481    ///
482    /// ```rust
483    /// use up_rust::UUri;
484    ///
485    /// let uri = UUri::try_from_parts("my-vehicle", 0x10101234, 0x01, 0x9a10).unwrap();
486    /// assert_eq!(uri.authority_name(), "my-vehicle");
487    /// ```
488    #[must_use]
489    pub fn authority_name(&self) -> &str {
490        self.authority_name.as_str()
491    }
492
493    // Gets the uEntity type identifier part from this uProtocol URI.
494    ///
495    /// # Examples
496    ///
497    /// ```rust
498    /// use up_rust::UUri;
499    ///
500    /// let uri = UUri::try_from_parts("my-vehicle", 0x10101234, 0x01, 0x9a10).unwrap();
501    /// assert_eq!(uri.uentity_type_id(), 0x1234);
502    /// ```
503    #[must_use]
504    pub fn uentity_type_id(&self) -> u16 {
505        (self.ue_id & WILDCARD_ENTITY_TYPE) as u16
506    }
507
508    // Gets the uEntity instance identifier part from this uProtocol URI.
509    ///
510    /// # Examples
511    ///
512    /// ```rust
513    /// use up_rust::UUri;
514    ///
515    /// let uri = UUri::try_from_parts("my-vehicle", 0x10101234, 0x01, 0x9a10).unwrap();
516    /// assert_eq!(uri.uentity_instance_id(), 0x1010);
517    /// ```
518    #[must_use]
519    pub fn uentity_instance_id(&self) -> u16 {
520        ((self.ue_id & WILDCARD_ENTITY_INSTANCE) >> 16) as u16
521    }
522
523    // Gets the major version part from this uProtocol URI.
524    ///
525    /// # Examples
526    ///
527    /// ```rust
528    /// use up_rust::UUri;
529    ///
530    /// let uri = UUri::try_from_parts("my-vehicle", 0x10101234, 0x01, 0x9a10).unwrap();
531    /// assert_eq!(uri.uentity_major_version(), 0x01);
532    /// ```
533    #[must_use]
534    pub fn uentity_major_version(&self) -> u8 {
535        self.ue_version_major
536    }
537
538    // Gets the resource identifier part from this uProtocol URI.
539    ///
540    /// # Examples
541    ///
542    /// ```rust
543    /// use up_rust::UUri;
544    ///
545    /// let uri = UUri::try_from_parts("my-vehicle", 0x10101234, 0x01, 0x9a10).unwrap();
546    /// assert_eq!(uri.resource_id(), 0x9a10);
547    /// ```
548    #[must_use]
549    pub fn resource_id(&self) -> u16 {
550        self.resource_id
551    }
552
553    #[must_use]
554    /// Returns a copy of this URI with the resource id replaced.
555    pub fn clone_with_resource_id(&self, resource_id: u16) -> Self {
556        UUri {
557            authority_name: self.authority_name.clone(),
558            ue_id: self.ue_id,
559            ue_version_major: self.ue_version_major,
560            resource_id,
561        }
562    }
563
564    /// Verifies that the given authority name complies with the UUri specification.
565    ///
566    /// # Arguments
567    ///
568    /// * `authority` - The authority name to verify.
569    ///
570    /// # Errors
571    ///
572    /// Returns an error if the authority name is invalid.
573    ///
574    /// # Examples
575    ///
576    /// ```rust
577    /// use up_rust::UUri;
578    ///
579    /// assert!(UUri::verify_authority("my.vin").is_ok());
580    /// ```
581    // [impl->dsn~uri-authority-name-length~1]
582    // [impl->dsn~uri-host-only~2]
583    pub fn verify_authority(authority: &str) -> Result<(), UUriError> {
584        Self::verify_authority_name(authority).map(|_| ())
585    }
586
587    // [impl->dsn~uri-authority-name-length~1]
588    // [impl->dsn~uri-host-only~2]
589    pub(crate) fn verify_authority_name(authority: &str) -> Result<AuthorityNameString, UUriError> {
590        Authority::try_from(authority)
591            .map_err(|e| UUriError::validation_error(format!("invalid authority: {e}")))
592            .and_then(|auth| Self::verify_parsed_authority(&auth))
593    }
594
595    // [impl->dsn~uri-authority-name-length~1]
596    // [impl->dsn~uri-host-only~2]
597    pub(crate) fn verify_parsed_authority(
598        auth: &Authority,
599    ) -> Result<AuthorityNameString, UUriError> {
600        if auth.has_port() {
601            Err(UUriError::validation_error(
602                "uProtocol URI's authority must not contain port",
603            ))
604        } else if auth.has_username() || auth.has_password() {
605            Err(UUriError::validation_error(
606                "uProtocol URI's authority must not contain userinfo",
607            ))
608        } else {
609            let verified_name = match auth.host() {
610                uriparse::Host::IPv4Address(_) | uriparse::Host::IPv6Address(_) => {
611                    auth.host().to_string()
612                }
613                uriparse::Host::RegisteredName(name) => {
614                    if !WILDCARD_AUTHORITY.eq(name.as_str())
615                        && !AUTHORITY_NAME_PATTERN.is_match(name.as_str())
616                    {
617                        return Err(UUriError::validation_error(
618                            "uProtocol URI's authority contains invalid characters",
619                        ));
620                    }
621                    name.to_string()
622                }
623            };
624            Ok(verified_name)
625        }
626    }
627
628    /// Check if an `UUri` is remote, by comparing authority fields.
629    /// UUris with empty authority are considered to be local.
630    ///
631    /// # Returns
632    ///
633    /// 'true' if other_uri has a different authority than `Self`, `false` otherwise.
634    ///
635    /// # Examples
636    ///
637    /// ```rust
638    /// use std::str::FromStr;
639    /// use up_rust::UUri;
640    ///
641    /// let authority_a = UUri::from_str("up://authority.a/100A/1/0").unwrap();
642    /// let authority_b = UUri::from_str("up://authority.b/200B/2/20").unwrap();
643    /// assert!(authority_a.is_remote(&authority_b));
644    ///
645    /// let authority_local = UUri::from_str("up:///100A/1/0").unwrap();
646    /// assert!(!authority_local.is_remote(&authority_a));
647    ///
648    /// let authority_wildcard = UUri::from_str("up://*/100A/1/0").unwrap();
649    /// assert!(!authority_wildcard.is_remote(&authority_a));
650    /// assert!(!authority_a.is_remote(&authority_wildcard));
651    /// assert!(!authority_wildcard.is_remote(&authority_wildcard));
652    /// ````
653    #[must_use]
654    pub fn is_remote(&self, other_uri: &UUri) -> bool {
655        self.is_remote_authority(other_uri.authority_name.as_str())
656    }
657
658    /// Check if an authority is remote compared to the authority field of the UUri.
659    /// Empty authorities are considered to be local.
660    ///
661    /// # Returns
662    ///
663    /// 'true' if authority is a different than `Self.authority_name`, `false` otherwise.
664    ///
665    /// # Examples
666    ///
667    /// ```rust
668    /// use std::str::FromStr;
669    /// use up_rust::UUri;
670    ///
671    /// let authority_a = UUri::from_str("up://authority.a/100A/1/0").unwrap();
672    /// let authority_b = "authority.b";
673    /// assert!(authority_a.is_remote_authority(&authority_b));
674    ///
675    /// let authority_local = "";
676    /// assert!(!authority_a.is_remote_authority(&authority_local));
677    ///
678    /// let authority_wildcard = "*";
679    /// assert!(!authority_a.is_remote_authority(&authority_wildcard));
680    /// ```
681    #[must_use]
682    pub fn is_remote_authority(&self, authority: &str) -> bool {
683        !authority.is_empty()
684            && !self.authority_name.is_empty()
685            && !self.has_wildcard_authority()
686            && authority != WILDCARD_AUTHORITY
687            && self.authority_name.as_str() != authority
688    }
689
690    /// Checks if this UUri has an empty authority name.
691    ///
692    /// # Examples
693    ///
694    /// ```rust
695    /// use up_rust::UUri;
696    ///
697    /// let uuri = UUri::try_from_parts("", 0x9b3a, 0x01, 0x145b).unwrap();
698    /// assert!(uuri.has_empty_authority());
699    /// ```
700    #[must_use]
701    pub fn has_empty_authority(&self) -> bool {
702        self.authority_name.is_empty()
703    }
704
705    /// Checks if this UUri has a wildcard authority name.
706    ///
707    /// # Examples
708    ///
709    /// ```rust
710    /// use up_rust::UUri;
711    ///
712    /// let uuri = UUri::try_from_parts("*", 0x9b3a, 0x01, 0x145b).unwrap();
713    /// assert!(uuri.has_wildcard_authority());
714    /// ```
715    #[must_use]
716    pub fn has_wildcard_authority(&self) -> bool {
717        self.authority_name == WILDCARD_AUTHORITY
718    }
719
720    /// Checks if this UUri has an entity identifier matching any instance.
721    ///
722    /// # Examples
723    ///
724    /// ```rust
725    /// use up_rust::UUri;
726    ///
727    /// let uuri = UUri::try_from_parts("vin", 0xFFFF_0123, 0x01, 0x145b).unwrap();
728    /// assert!(uuri.has_wildcard_entity_instance());
729    /// ```
730    #[must_use]
731    pub fn has_wildcard_entity_instance(&self) -> bool {
732        self.ue_id & WILDCARD_ENTITY_INSTANCE == WILDCARD_ENTITY_INSTANCE
733    }
734
735    /// Checks if this UUri has an entity identifier matching any type.
736    ///
737    /// # Examples
738    ///
739    /// ```rust
740    /// use up_rust::UUri;
741    ///
742    /// let uuri = UUri::try_from_parts("vin", 0x00C0_FFFF, 0x01, 0x145b).unwrap();
743    /// assert!(uuri.has_wildcard_entity_type());
744    /// ```
745    #[must_use]
746    pub fn has_wildcard_entity_type(&self) -> bool {
747        self.ue_id & WILDCARD_ENTITY_TYPE == WILDCARD_ENTITY_TYPE
748    }
749
750    /// Checks if this UUri has a wildcard major version.
751    ///
752    /// # Examples
753    ///
754    /// ```rust
755    /// use up_rust::UUri;
756    ///
757    /// let uuri = UUri::try_from_parts("vin", 0x9b3a, 0xFF, 0x145b).unwrap();
758    /// assert!(uuri.has_wildcard_version());
759    /// ```
760    #[must_use]
761    pub fn has_wildcard_version(&self) -> bool {
762        self.ue_version_major == WILDCARD_ENTITY_VERSION
763    }
764
765    /// Checks if this UUri has a wildcard entity identifier.
766    ///
767    /// # Examples
768    ///
769    /// ```rust
770    /// use up_rust::UUri;
771    ///
772    /// let uuri = UUri::try_from_parts("vin", 0x9b3a, 0x01, 0xFFFF).unwrap();
773    /// assert!(uuri.has_wildcard_resource_id());
774    /// ```
775    #[must_use]
776    pub fn has_wildcard_resource_id(&self) -> bool {
777        self.resource_id == WILDCARD_RESOURCE_ID
778    }
779
780    /// Verifies that this UUri does not contain any wildcards.
781    ///
782    /// # Errors
783    ///
784    /// Returns an error if any of this UUri's properties contain a wildcard value.
785    ///
786    /// # Examples
787    ///
788    /// ```rust
789    /// use up_rust::UUri;
790    ///
791    /// let uri = UUri::try_from_parts("vin.vehicles", 0x0000_2310, 0x03, 0xa000).unwrap();
792    /// assert!(uri.verify_no_wildcards().is_ok());
793    /// ```
794    pub fn verify_no_wildcards(&self) -> Result<(), UUriError> {
795        if self.has_wildcard_authority() {
796            Err(UUriError::validation_error(format!(
797                "Authority must not contain wildcard character [{WILDCARD_AUTHORITY}]"
798            )))
799        } else if self.has_wildcard_entity_instance() {
800            Err(UUriError::validation_error(format!(
801                "Entity instance ID must not be set to wildcard value [{WILDCARD_ENTITY_INSTANCE:#X}]")))
802        } else if self.has_wildcard_entity_type() {
803            Err(UUriError::validation_error(format!(
804                "Entity type ID must not be set to wildcard value [{WILDCARD_ENTITY_TYPE:#X}]"
805            )))
806        } else if self.has_wildcard_version() {
807            Err(UUriError::validation_error(format!(
808                "Entity version must not be set to wildcard value [{WILDCARD_ENTITY_VERSION:#X}]"
809            )))
810        } else if self.has_wildcard_resource_id() {
811            Err(UUriError::validation_error(format!(
812                "Resource ID must not be set to wildcard value [{WILDCARD_RESOURCE_ID:#X}]"
813            )))
814        } else {
815            Ok(())
816        }
817    }
818
819    /// Checks if this UUri refers to a service method.
820    ///
821    /// Returns `true` if 0 < resource ID < 0x8000.
822    ///
823    /// # Examples
824    ///
825    /// ```rust
826    /// use up_rust::UUri;
827    ///
828    /// let uri = UUri::try_from_parts("", 0xabcd, 0x01, 0x7FFF).unwrap();
829    /// assert!(uri.is_rpc_method());
830    /// ```
831    #[must_use]
832    pub fn is_rpc_method(&self) -> bool {
833        self.resource_id > RESOURCE_ID_RESPONSE && self.resource_id < RESOURCE_ID_MIN_EVENT
834    }
835
836    /// Verifies that this UUri refers to a service method.
837    ///
838    /// # Errors
839    ///
840    /// Returns an error if [`Self::is_rpc_method`] fails or
841    /// the UUri [contains any wildcards](Self::verify_no_wildcards).
842    ///
843    /// # Examples
844    ///
845    /// ```rust
846    /// use up_rust::UUri;
847    ///
848    /// let uri = UUri::try_from_parts("", 0xabcd, 0x01, 0x8000).unwrap();
849    /// assert!(uri.verify_rpc_method().is_err());
850    ///
851    /// let uri = UUri::try_from_parts("", 0xabcd, 0x01, 0x0000).unwrap();
852    /// assert!(uri.verify_rpc_method().is_err());
853    /// ```
854    pub fn verify_rpc_method(&self) -> Result<(), UUriError> {
855        if !self.is_rpc_method() {
856            Err(UUriError::validation_error(format!(
857                "Resource ID must be a value from ]{RESOURCE_ID_RESPONSE:#X}, {RESOURCE_ID_MIN_EVENT:#X}[")))
858        } else {
859            self.verify_no_wildcards()
860        }
861    }
862
863    /// Checks if this UUri represents a destination for a Notification.
864    ///
865    /// Returns `true` if resource ID is 0.
866    ///
867    /// # Examples
868    ///
869    /// ```rust
870    /// use up_rust::UUri;
871    ///
872    /// let uri = UUri::try_from_parts("", 0xabcd, 0x01, 0x0000).unwrap();
873    /// assert!(uri.is_notification_destination());
874    /// ```
875    #[must_use]
876    pub fn is_notification_destination(&self) -> bool {
877        self.resource_id == RESOURCE_ID_RESPONSE
878    }
879
880    /// Checks if this UUri represents an RPC response address.
881    ///
882    /// Returns `true` if resource ID is 0.
883    ///
884    /// # Examples
885    ///
886    /// ```rust
887    /// use up_rust::UUri;
888    ///
889    /// let uri = UUri::try_from_parts("", 0xabcd, 0x01, 0x0000).unwrap();
890    /// assert!(uri.is_rpc_response());
891    /// ```
892    #[must_use]
893    pub fn is_rpc_response(&self) -> bool {
894        self.resource_id == RESOURCE_ID_RESPONSE
895    }
896
897    /// Verifies that this UUri represents an RPC response address.
898    ///
899    /// # Errors
900    ///
901    /// Returns an error if [`Self::is_rpc_response`] fails or
902    /// the UUri [contains any wildcards](Self::verify_no_wildcards).
903    ///
904    /// # Examples
905    ///
906    /// ```rust
907    /// use up_rust::UUri;
908    ///
909    /// let uri = UUri::try_from_parts("", 0xabcd, 0x01, 0x4001).unwrap();
910    /// assert!(uri.verify_rpc_response().is_err());
911    /// ```
912    pub fn verify_rpc_response(&self) -> Result<(), UUriError> {
913        if !self.is_rpc_response() {
914            Err(UUriError::validation_error(format!(
915                "Resource ID must be {RESOURCE_ID_RESPONSE:#X}"
916            )))
917        } else {
918            self.verify_no_wildcards()
919        }
920    }
921
922    /// Checks if this UUri can be used as the source of an event.
923    ///
924    /// Returns `true` if resource ID >= 0x8000.
925    ///
926    /// # Examples
927    ///
928    /// ```rust
929    /// use up_rust::UUri;
930    ///
931    /// let uri = UUri::try_from_parts("", 0xabcd, 0x01, 0x8000).unwrap();
932    /// assert!(uri.is_event());
933    /// ```
934    #[must_use]
935    pub fn is_event(&self) -> bool {
936        self.resource_id >= RESOURCE_ID_MIN_EVENT
937    }
938
939    /// Verifies that this UUri can be used as the source of an event.
940    ///
941    /// # Errors
942    ///
943    /// Returns an error if [`Self::is_event`] fails or
944    /// the UUri [contains any wildcards](Self::verify_no_wildcards).
945    ///
946    /// # Examples
947    ///
948    /// ```rust
949    /// use up_rust::UUri;
950    ///
951    /// let uri = UUri::try_from_parts("", 0xabcd, 0x01, 0x7FFF).unwrap();
952    /// assert!(uri.verify_event().is_err());
953    /// ```
954    pub fn verify_event(&self) -> Result<(), UUriError> {
955        if !self.is_event() {
956            Err(UUriError::validation_error(format!(
957                "Resource ID must be >= {RESOURCE_ID_MIN_EVENT:#X}"
958            )))
959        } else {
960            self.verify_no_wildcards()
961        }
962    }
963
964    fn matches_authority(&self, candidate: &UUri) -> bool {
965        self.has_wildcard_authority() || self.authority_name == candidate.authority_name
966    }
967
968    fn matches_entity_type(&self, candidate: &UUri) -> bool {
969        self.has_wildcard_entity_type() || self.uentity_type_id() == candidate.uentity_type_id()
970    }
971
972    fn matches_entity_instance(&self, candidate: &UUri) -> bool {
973        self.has_wildcard_entity_instance()
974            || self.uentity_instance_id() == candidate.uentity_instance_id()
975    }
976
977    fn matches_entity_version(&self, candidate: &UUri) -> bool {
978        self.has_wildcard_version()
979            || self.uentity_major_version() == candidate.uentity_major_version()
980    }
981
982    fn matches_entity(&self, candidate: &UUri) -> bool {
983        self.matches_entity_type(candidate)
984            && self.matches_entity_instance(candidate)
985            && self.matches_entity_version(candidate)
986    }
987
988    fn matches_resource(&self, candidate: &UUri) -> bool {
989        self.has_wildcard_resource_id() || self.resource_id == candidate.resource_id
990    }
991
992    /// Checks if a given candidate URI matches a pattern.
993    ///
994    /// # Returns
995    ///
996    /// `true` if the candiadate matches the pattern represented by this UUri.
997    ///
998    /// # Examples
999    ///
1000    /// ```rust
1001    /// use up_rust::UUri;
1002    ///
1003    /// let pattern = UUri::try_from("//vin/A14F/3/FFFF").unwrap();
1004    /// let candidate = UUri::try_from("//vin/A14F/3/B1D4").unwrap();
1005    /// assert!(pattern.matches(&candidate));
1006    /// ```
1007    // [impl->dsn~uri-pattern-matching~2]
1008    #[must_use]
1009    pub fn matches(&self, candidate: &UUri) -> bool {
1010        self.matches_authority(candidate)
1011            && self.matches_entity(candidate)
1012            && self.matches_resource(candidate)
1013    }
1014}
1015
1016#[cfg(feature = "up-core-api")]
1017mod core_types_support {
1018    use super::*;
1019    use crate::up_core_api::uri::UUri as UUriProto;
1020    use crate::ProtobufMappable;
1021    use protobuf::{well_known_types::any::Any, Message};
1022
1023    impl TryFrom<&UUriProto> for UUri {
1024        type Error = crate::UUriError;
1025
1026        fn try_from(uuri_proto: &UUriProto) -> Result<Self, Self::Error> {
1027            let version = u8::try_from(uuri_proto.ue_version_major).map_err(|_e| {
1028                UUriError::validation_error(
1029                    "uProtocol URI's major version must be an 8 bit unsigned integer".to_string(),
1030                )
1031            })?;
1032            let resource_id = u16::try_from(uuri_proto.resource_id).map_err(|_e| {
1033                UUriError::validation_error(
1034                    "uProtocol URI's resource ID must be a 16 bit unsigned integer".to_string(),
1035                )
1036            })?;
1037            UUri::try_from_parts(
1038                &uuri_proto.authority_name,
1039                uuri_proto.ue_id,
1040                version,
1041                resource_id,
1042            )
1043        }
1044    }
1045
1046    impl From<&UUri> for UUriProto {
1047        fn from(uuri: &UUri) -> Self {
1048            UUriProto {
1049                authority_name: uuri.authority_name.as_str().to_string(),
1050                ue_id: uuri.ue_id,
1051                ue_version_major: uuri.ue_version_major as u32,
1052                resource_id: uuri.resource_id as u32,
1053                ..Default::default()
1054            }
1055        }
1056    }
1057
1058    impl ProtobufMappable for UUri {
1059        fn parse_from_protobuf_bytes(proto: &[u8]) -> Result<Self, crate::SerializationError> {
1060            let uuri_proto = UUriProto::parse_from_bytes(proto).map_err(|e| {
1061                crate::SerializationError::new(format!("Protobuf decode error: {e}"))
1062            })?;
1063            UUri::try_from(&uuri_proto)
1064                .map_err(|e| crate::SerializationError::new(format!("UUri conversion error: {e}")))
1065        }
1066        fn parse_from_packed_protobuf_bytes(
1067            proto: &[u8],
1068        ) -> Result<Self, crate::SerializationError> {
1069            Any::parse_from_bytes(proto)
1070                .map_err(|err| crate::SerializationError::new(err.to_string()))
1071                .and_then(|any| match any.unpack::<UUriProto>() {
1072                    Ok(Some(uuri_proto)) => UUri::try_from(&uuri_proto)
1073                        .map_err(|e| crate::SerializationError::new(e.to_string())),
1074                    Ok(None) => Err(crate::SerializationError::new(
1075                        "Protobuf Any does not contain UUriProto".to_string(),
1076                    )),
1077                    Err(e) => Err(crate::SerializationError::new(format!(
1078                        "Protobuf Any unpack error: {e}"
1079                    ))),
1080                })
1081        }
1082        fn write_to_protobuf_bytes(&self) -> Result<Vec<u8>, crate::SerializationError> {
1083            UUriProto::from(self)
1084                .write_to_bytes()
1085                .map_err(|e| crate::SerializationError::new(format!("Protobuf encode error: {e}")))
1086        }
1087        fn write_to_packed_protobuf_bytes(&self) -> Result<Vec<u8>, crate::SerializationError> {
1088            Any::pack(&UUriProto::from(self))
1089                .map_err(|e| crate::SerializationError::new(format!("Failed to pack UUri: {e}")))
1090                .and_then(|any| any.write_to_protobuf_bytes())
1091        }
1092    }
1093}
1094
1095#[cfg(test)]
1096mod tests {
1097    use super::*;
1098    use test_case::test_case;
1099
1100    #[test_case(UUri {
1101            authority_name: "vin".into(),
1102            ue_id: 0x0000_8000,
1103            ue_version_major: 0x01,
1104            resource_id: 0x0002,
1105        },
1106        "//vin/8000/1/2" => true;
1107        "succeeds for full URI with authority")]
1108    #[test_case(UUri {
1109            authority_name: "".into(),
1110            ue_id: 0x0000_8000,
1111            ue_version_major: 0x01,
1112            resource_id: 0x0002,
1113        },
1114        "up:/8000/1/2" => true;
1115        "succeeds for URI without authority")]
1116    #[test_case(UUri {
1117            authority_name: "vin".into(),
1118            ue_id: 0x0000_8000,
1119            ue_version_major: 0x01,
1120            resource_id: 0x0002,
1121        },
1122        "//other-vin/8000/1/2" => false;
1123        "fails for different authority")]
1124    #[test_case(UUri {
1125            authority_name: "vin".into(),
1126            ue_id: 0x0000_8000,
1127            ue_version_major: 0x01,
1128            resource_id: 0x0002,
1129        },
1130        "up://vin/18000/1/2" => false;
1131        "fails for different entity ID")]
1132    #[test_case(UUri {
1133            authority_name: "vin".into(),
1134            ue_id: 0x0000_8000,
1135            ue_version_major: 0x01,
1136            resource_id: 0x0002,
1137        },
1138        "//vin/8000/2/2" => false;
1139        "fails for different version")]
1140    #[test_case(UUri {
1141            authority_name: "vin".into(),
1142            ue_id: 0x0000_8000,
1143            ue_version_major: 0x01,
1144            resource_id: 0x0002,
1145        },
1146        "up://vin/8000/1/5" => false;
1147        "fails for different resource ID")]
1148    #[allow(clippy::cmp_owned)]
1149    fn test_eq_with_str(uuri: UUri, uri_str: &str) -> bool {
1150        uuri == uri_str && uuri == *uri_str && uuri == String::from(uri_str)
1151    }
1152
1153    #[test_case("//*/A100/1/1"; "for any authority")]
1154    #[test_case("//vin/FFFF/1/1"; "for any entity type")]
1155    #[test_case("//vin/FFFF0ABC/1/1"; "for any entity instance")]
1156    #[test_case("//vin/A100/FF/1"; "for any version")]
1157    #[test_case("//vin/A100/1/FFFF"; "for any resource")]
1158    fn test_verify_no_wildcards_fails(uri: &str) {
1159        let uuri = UUri::try_from(uri).expect("should have been able to deserialize URI");
1160        assert!(uuri.verify_no_wildcards().is_err());
1161    }
1162
1163    // [utest->dsn~uri-authority-name-length~1]
1164    #[test]
1165    fn test_from_str_fails_for_authority_exceeding_max_length() {
1166        let host_name = "a".repeat(129);
1167        let uri = format!("//{}/A100/1/6501", host_name);
1168        assert!(UUri::from_str(&uri).is_err());
1169    }
1170
1171    // [utest->dsn~uri-path-mapping~2]
1172    #[test]
1173    fn test_from_str_accepts_lowercase_hex_encoding() {
1174        let result = UUri::try_from("up://vin/ffff0abc/a1/bcd1");
1175        assert!(result.is_ok_and(|uuri| {
1176            uuri.authority_name == "vin"
1177                && uuri.ue_id == 0xFFFF0ABC
1178                && uuri.ue_version_major == 0xA1
1179                && uuri.resource_id == 0xBCD1
1180        }));
1181    }
1182}
1183
1184#[cfg(test)]
1185mod round_trip_properties {
1186    use std::str::FromStr;
1187
1188    use proptest::prelude::*;
1189
1190    use super::UUri;
1191
1192    proptest! {
1193        /// Serialization round-trip: any valid URI built from parts survives
1194        /// `to_string` -> `from_str` unchanged. Hand-enumerated cases pin the
1195        /// known edges; this pins the space between them.
1196        #[test]
1197        fn uri_survives_string_round_trip(
1198            authority in "[a-z][a-z0-9]{0,11}",
1199            ue_id in 1u32..0xFFFF,
1200            version in 1u8..=0xFE,
1201            resource in 0u16..0xFFFE,
1202        ) {
1203            let uri = UUri::try_from_parts(&authority, ue_id, version, resource)
1204                .expect("parts chosen from the valid, wildcard-free range");
1205
1206            let round_tripped = UUri::from_str(&uri.to_uri(false))
1207                .expect("serialized form of a valid URI must parse");
1208
1209            prop_assert_eq!(uri, round_tripped);
1210        }
1211    }
1212}