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(®ex).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}