Skip to main content

tango/
name.rs

1//! Types for the names Tango gives to things: case-ignored names, and type safe Tango
2//! Resource Locators.
3
4use std::fmt::{self, Debug, Display};
5use std::hash::{Hash, Hasher};
6use std::convert::Infallible;
7use std::borrow::Borrow;
8use std::num::NonZero;
9use std::str::FromStr;
10use std::ops::Deref;
11use std::sync::Arc;
12
13
14/// A name of something Tango names, compared and hashed the way Tango compares such a
15/// name: ignoring the case, and on ASCII alone, every name Tango has being ASCII.
16/// 
17/// Which is what a server has to do of the names of its classes, of its devices, and of
18/// the commands and attributes of each of them: cppTango lowercases every one of them
19/// before it looks it up, and the database, being a SQL one, matches them the same way.
20/// A name is kept as it was given all the same, because it is also what a client is shown.
21#[repr(transparent)]
22pub struct Name(str);
23
24impl Name {
25
26    /// Borrow a string as a name, which costs nothing: a name is a [`str`] with another
27    /// comparison and nothing else.
28    #[inline]
29    pub fn new(name: &str) -> &Self {
30        // SAFETY: Name is a repr(transparent) wrapper around str, so a reference to one is
31        // a reference to the other.
32        unsafe { &*(name as *const str as *const Self) }
33    }
34
35    /// Return the name as it was given, with its case.
36    #[inline]
37    pub fn as_str(&self) -> &str {
38        &self.0
39    }
40
41    /// Copy the name into an owned one.
42    #[inline]
43    pub fn to_arc(&self) -> ArcName {
44        ArcName(Arc::from(&self.0))
45    }
46
47}
48
49impl PartialEq for Name {
50    #[inline]
51    fn eq(&self, other: &Self) -> bool {
52        self.0.eq_ignore_ascii_case(&other.0)
53    }
54}
55
56impl Eq for Name {}
57
58impl Hash for Name {
59    fn hash<H: Hasher>(&self, state: &mut H) {
60        // Byte by byte rather than hashing the lowercased name, which would mean allocating
61        // one on every lookup, with the same terminator `str` itself writes so that two
62        // names one of which is a prefix of the other do not hash alike.
63        for byte in self.0.bytes() {
64            state.write_u8(byte.to_ascii_lowercase());
65        }
66        state.write_u8(0xff);
67    }
68}
69
70impl Debug for Name {
71    #[inline]
72    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73        Debug::fmt(&self.0, f)
74    }
75}
76
77impl Display for Name {
78    #[inline]
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        Display::fmt(&self.0, f)
81    }
82}
83
84/// Atomic counted reference to a [`Name`].
85pub struct ArcName(Arc<str>);
86
87impl From<Arc<str>> for ArcName {
88    fn from(value: Arc<str>) -> Self {
89        Self(value)
90    }
91}
92
93impl From<&str> for ArcName {
94    #[inline]
95    fn from(value: &str) -> Self {
96        Self(Arc::from(value))
97    }
98}
99
100impl FromStr for ArcName {
101
102    type Err = Infallible;
103
104    #[inline]
105    fn from_str(s: &str) -> Result<Self, Self::Err> {
106        Ok(Self::from(s))
107    }
108
109}
110
111impl Deref for ArcName {
112    type Target = Name;
113    #[inline]
114    fn deref(&self) -> &Self::Target {
115        Name::new(&self.0)
116    }
117}
118
119impl Borrow<Name> for ArcName {
120    #[inline]
121    fn borrow(&self) -> &Name {
122        self
123    }
124}
125
126impl PartialEq for ArcName {
127    fn eq(&self, other: &Self) -> bool {
128        **self == **other
129    }
130}
131
132impl Eq for ArcName {}
133
134impl Hash for ArcName {
135    fn hash<H: Hasher>(&self, state: &mut H) {
136        (**self).hash(state);
137    }
138}
139
140impl Debug for ArcName {
141    #[inline]
142    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143        Debug::fmt(&**self, f)
144    }
145}
146
147impl Display for ArcName {
148    #[inline]
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        Display::fmt(&**self, f)
151    }
152}
153
154// A Tango name is made of ASCII letters, digits, `_` and `-`, starting with a letter or a
155// digit, see `is_tango_name_char`; what differs from one kind of name to another is how
156// long it may be and how it is split.
157
158fn check_non_empty_name(name: &str) -> bool {
159    !name.is_empty() && name.chars().enumerate().all(is_tango_name_char)
160}
161
162fn check_non_empty_u8_name(name: &str) -> bool {
163    !name.is_empty() && name.len() <= 255 && name.chars().enumerate().all(is_tango_name_char)
164}
165
166/// Check if a given Tango class name is valid.
167pub fn check_tango_class_name(name: &str) -> bool {
168    check_non_empty_name(name)
169}
170
171/// Check if a given part of a Tango server name, its executable or its instance, is valid.
172pub fn check_tango_server_part_name(part: &str) -> bool {
173    check_non_empty_name(part)
174}
175
176/// Check if a given Tango server name, `exe/inst`, is valid.
177pub fn check_tango_server_name(name: &str) -> bool {
178    let Some((exe, inst)) = name.split_once('/') else { return false };
179    check_tango_server_part_name(exe) && 
180    check_tango_server_part_name(inst)
181}
182
183fn check_tango_dev_part_name(part: &str) -> bool {
184    !part.is_empty() && part.len() <= 85 && part.chars().enumerate().all(is_tango_name_char)
185}
186
187/// Check if a given Tango device name, `domain/family/member`, is valid, each part being
188/// at most 85 long. An alias is not a device name, see [`check_tango_alias`].
189pub fn check_tango_dev_name(name: &str) -> bool {
190    let Some((domain, rest)) = name.split_once('/') else { return false };
191    let Some((family, member)) = rest.split_once('/') else { return false };
192    check_tango_dev_part_name(domain) &&
193    check_tango_dev_part_name(family) &&
194    check_tango_dev_part_name(member)
195}
196
197/// Check if a given Tango attribute name is valid, at most 255 long.
198pub fn check_tango_attr_name(name: &str) -> bool {
199    check_non_empty_u8_name(name)
200}
201
202/// Check if a given Tango command name is valid, at most 255 long.
203pub fn check_tango_command_name(name: &str) -> bool {
204    check_non_empty_u8_name(name)
205}
206
207/// Check if a given Tango property name is valid, at most 255 long.
208pub fn check_tango_property_name(name: &str) -> bool {
209    check_non_empty_u8_name(name)
210}
211
212/// Check if a given Tango device or attribute alias is valid, at most 255 long.
213pub fn check_tango_alias(name: &str) -> bool {
214    check_non_empty_u8_name(name)
215}
216
217/// An error common for name validation in Tango.
218#[derive(thiserror::Error, Debug)]
219pub enum TrlError {
220    #[error("Invalid protocol, expecting tango://")]
221    InvalidProtocol,
222    #[error("Invalid host, wrong characters or too long (> 255)")]
223    InvalidHost,
224    #[error("Invalid port, expecting non-null 16-bit integer")]
225    InvalidPort,
226    #[error("Invalid hash, expecting #dbase=yes/no")]
227    InvalidHash,
228    #[error("Invalid or missing path, device-name[/attribute] with device-name being an alias or domain/family/member")]
229    InvalidPath,
230    #[error("Invalid device name, wrong characters or too long (> 85 per domain/family/member)")]
231    InvalidDevName,
232    #[error("Invalid attribute name, wrong characters or too long (> 255)")]
233    InvalidAttrName,
234    #[error("Invalid device or attribute alias, wrong characters or too long (> 255)")]
235    InvalidAlias,
236    #[error("Invalid property name, wrong characters or too long (> 255)")]
237    InvalidPropName,
238}
239
240/// Tango Resource Locator, borrowing the string it was parsed from.
241/// 
242/// This is a custom reference: it is [`Copy`] and parsing one allocates nothing, the
243/// owned counterpart being [`ArcTrl`].
244/// 
245/// RFC: 
246/// - <https://tango-controls.readthedocs.io/projects/rfc/en/latest/16/TangoResourceLocator.html>
247/// - <https://tango-controls.readthedocs.io/projects/rfc/en/latest/2/Device.html>
248/// - <https://tango-controls.readthedocs.io/projects/rfc/en/latest/4/Attribute.html>
249/// - <https://tango-controls.readthedocs.io/projects/rfc/en/latest/5/Property.html>
250#[derive(Clone, Copy)]
251pub struct Trl<'a> {
252    /// The raw string.
253    raw: &'a str,
254    /// Where the parts are in the raw string.
255    meta: TrlMeta,
256}
257
258/// Tango Resource Locator, owning the string it was parsed from.
259/// 
260/// Cloning this structure is cheap because it is backed by an Arc, and [`Self::as_trl`]
261/// gives the [`Trl`] to read it through.
262#[derive(Clone)]
263pub struct ArcTrl {
264    /// The raw string.
265    raw: Arc<str>,
266    /// Where the parts are in the raw string.
267    meta: TrlMeta,
268}
269
270/// Where the parts of a TRL are in its raw string, shared by [`Trl`] and [`ArcTrl`].
271#[derive(Debug, Clone, Copy)]
272struct TrlMeta {
273    /// If the host and port are present, this is the length of it.
274    host_len_and_port: Option<(NonZero<u8>, NonZero<u16>)>,
275    /// Offset of where the object starts.
276    object_index: u16,
277    /// Inner description of the object.
278    object: TrlObjectMeta,
279    /// From RFC 5/Property, maximum property name length is 255.
280    property_len: Option<NonZero<u8>>,
281    /// True if the tango:// protocol is present.
282    protocol: bool,
283    /// True if the host/port directly targets a database or not.
284    db: bool,
285}
286
287/// The length of the device located, including the slashes. This can target either a
288/// device or a device attribute, or be a simple alias without slashes. Both device
289/// name and alias have max length of 255 if we don't count the slashes.
290/// 
291/// From RFC 4/Attribute, maximum attr name length is 255.
292#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
293enum TrlObjectMeta {
294    Alias(NonZero<u8>),
295    Dev(NonZero<u8>),
296    Attr(NonZero<u8>, NonZero<u8>),
297}
298
299impl TrlMeta {
300
301    fn parse(raw: &str) -> Result<Self, TrlError> {
302
303        let mut rem = raw;
304        let mut object_offset = 0u16;
305
306        // Check protocol
307        let mut protocol = false;
308        if let Some((proto, new_rem)) = rem.split_once("://") {
309            if proto != "tango" {
310                return Err(TrlError::InvalidProtocol);
311            }
312            object_offset += "tango://".len() as u16;
313            protocol = true;
314            rem = new_rem;
315        }
316
317        // Parse the end hash first.
318        let mut db = true;
319        if let Some((new_rem, hash)) = rem.rsplit_once('#') {
320            db = match hash {
321                "dbase=yes" => true,
322                "dbase=no" => false,
323                _ => return Err(TrlError::InvalidHash),
324            };
325            rem = new_rem;
326        }
327
328        // Then parse the property, we don't validate the property immediately because 
329        // acceptable characters depend on whether this is device property or attr property.
330        let mut property_len = None;
331        if let Some((new_rem, property_raw)) = rem.rsplit_once("->") {
332            if !check_tango_property_name(property_raw) {
333                return Err(TrlError::InvalidPropName);
334            }
335            property_len = Some(NonZero::new(property_raw.len() as u8).unwrap());
336            rem = new_rem;
337        }
338
339        // Only 4 parts at most, construct an array on stack.
340        let mut parts = [""; 5];
341        let mut parts_count = 0;
342        for part in rem.split('/') {
343            // If there are more parts it's not normal!
344            if parts_count >= parts.len() {
345                return Err(TrlError::InvalidPath);
346            }
347            parts[parts_count] = part;
348            parts_count += 1;
349        }
350
351        
352        // Get a slice with the real number of parts.
353        let mut parts = &mut parts[..parts_count];
354        debug_assert!(!parts.is_empty(), "A split we should have at least one element");
355
356        // Try to find host:port in the first part if possible.
357        let mut host_len_and_port = None;
358        if let Some((host_raw, port_raw)) = parts[0].split_once(':') {
359
360            // Remove the host:port part from the parts, check if there are still parts.
361            parts = &mut parts[1..];
362            if parts.is_empty() {
363                return Err(TrlError::InvalidPath)
364            }
365
366            let host_len = u8::try_from(host_raw.len()).ok()
367                .and_then(NonZero::new)
368                .ok_or(TrlError::InvalidHost)?;
369
370            if !host_raw.chars().enumerate().all(is_hostname_char) {
371                return Err(TrlError::InvalidHost);
372            }
373
374            let port = port_raw.parse::<NonZero<u16>>()
375                .map_err(|_| TrlError::InvalidPort)?;
376
377            host_len_and_port = Some((host_len, port));
378
379            // +1 for ':', +1 for '/'
380            object_offset += host_raw.len() as u16 + 1 + port_raw.len() as u16 + 1;
381
382        }
383
384        // If this is an alias, it can be of length 255.
385        let object;
386        if parts.len() == 1 {
387            if !check_tango_alias(parts[0]) {
388                return Err(TrlError::InvalidAlias);
389            }
390            object = TrlObjectMeta::Alias(NonZero::new(parts[0].len() as u8).unwrap());
391        } else if parts.len() == 3 || parts.len() == 4 {
392
393            let mut dev_len_raw = 0; // Add the 2 slashes to len.
394            for &dev_part in &parts[..3] {
395                if !check_tango_dev_part_name(dev_part) {
396                    return Err(TrlError::InvalidDevName);
397                }
398                dev_len_raw += dev_part.len();
399            }
400
401            // Unwrap because we check every part to not be empty.
402            let dev_len = NonZero::new(dev_len_raw as u8).unwrap();
403
404            if let Some(&attr_raw) = parts.get(3) {
405                if !check_tango_attr_name(attr_raw) {
406                    return Err(TrlError::InvalidAttrName);
407                }
408                object = TrlObjectMeta::Attr(dev_len, NonZero::new(attr_raw.len() as u8).unwrap());
409            } else {
410                object = TrlObjectMeta::Dev(dev_len);
411            }
412
413        } else {
414            return Err(TrlError::InvalidPath);
415        }
416
417        Ok(Self {
418            host_len_and_port,
419            object_index: object_offset,
420            object,
421            property_len,
422            protocol,
423            db,
424        })
425
426    }
427
428}
429
430impl<'a> Trl<'a> {
431
432    /// Parse a TRL, borrowing the given string.
433    pub fn parse(raw: &'a str) -> Result<Self, TrlError> {
434        Ok(Self { raw, meta: TrlMeta::parse(raw)? })
435    }
436
437    /// Return the raw string for this TRL.
438    #[inline]
439    pub fn as_str(self) -> &'a str {
440        self.raw
441    }
442
443    /// Copy the raw string into an owned TRL, without parsing it again.
444    #[inline]
445    pub fn to_arc(self) -> ArcTrl {
446        ArcTrl { raw: Arc::from(self.raw), meta: self.meta }
447    }
448
449    #[inline]
450    fn host_and_port_index(self) -> usize {
451        if self.meta.protocol { "tango://".len() } else { 0 }
452    }
453
454    /// The host and port of this TRL, if it has any.
455    pub fn host_and_port(self) -> Option<(&'a str, u16)> {
456        let index = self.host_and_port_index();
457        self.meta.host_len_and_port.map(|(host_len, port)| {
458            (&self.raw[index..][..host_len.get() as usize], port.get())
459        })
460    }
461
462    #[inline]
463    fn object_index(self) -> usize {
464        self.meta.object_index as usize
465    }
466
467    /// The object this TRL locates: an alias, a device or a device attribute.
468    pub fn object(self) -> TrlObject<'a> {
469        let index = self.object_index();
470        match self.meta.object {
471            TrlObjectMeta::Alias(alias_len) => {
472                TrlObject::Alias(&self.raw[index..][..alias_len.get() as usize])
473            }
474            TrlObjectMeta::Dev(dev_len) => {
475                // +2 for slashes that were ignored in length.
476                TrlObject::Dev(&self.raw[index..][..dev_len.get() as usize + 2])
477            }
478            TrlObjectMeta::Attr(dev_len, attr_len) => {
479                // +2 for slashes that were ignored in length.
480                let (dev_name, raw) = self.raw[index..].split_at(dev_len.get() as usize + 2);
481                // Start at 1 to ignore the slash between device and attribute.
482                let attr_name = &raw[1..][..attr_len.get() as usize];
483                TrlObject::Attr(dev_name, attr_name)
484            }
485        }
486    }
487
488    #[inline]
489    fn property_index(self) -> usize {
490        self.object_index() + match self.meta.object {
491            TrlObjectMeta::Alias(alias_len) => 
492                alias_len.get() as usize,
493            TrlObjectMeta::Dev(dev_len) => 
494                dev_len.get() as usize + 2,
495            TrlObjectMeta::Attr(dev_len, attr_len) => 
496                dev_len.get() as usize + attr_len.get() as usize + 3,
497        } + 2  // +2 for the "->"
498    }
499
500    /// The name of the property this TRL locates on its object, if any.
501    pub fn property(self) -> Option<&'a str> {
502        let index = self.property_index();
503        self.meta.property_len.map(|len| {
504            &self.raw[index..][..len.get() as usize]
505        })
506    }
507
508    /// Whether the object is reached through a database, which `#dbase=no` turns off.
509    #[inline]
510    pub fn db(self) -> bool {
511        self.meta.db
512    }
513
514}
515
516impl ArcTrl {
517
518    /// Borrow this TRL.
519    #[inline]
520    pub fn as_trl(&self) -> Trl<'_> {
521        Trl { raw: &self.raw, meta: self.meta }
522    }
523
524    /// Return the raw string for this TRL.
525    #[inline]
526    pub fn as_str(&self) -> &str {
527        &self.raw
528    }
529
530}
531
532impl FromStr for ArcTrl {
533
534    type Err = TrlError;
535
536    fn from_str(raw: &str) -> Result<Self, Self::Err> {
537        Ok(Self { raw: Arc::from(raw), meta: TrlMeta::parse(raw)? })
538    }
539
540}
541
542impl From<Trl<'_>> for ArcTrl {
543    #[inline]
544    fn from(trl: Trl<'_>) -> Self {
545        trl.to_arc()
546    }
547}
548
549impl<'a> From<&'a ArcTrl> for Trl<'a> {
550    #[inline]
551    fn from(trl: &'a ArcTrl) -> Self {
552        trl.as_trl()
553    }
554}
555
556impl fmt::Debug for Trl<'_> {
557    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
558        f.debug_tuple("Trl").field(&self.raw).finish()
559    }
560}
561
562impl fmt::Display for Trl<'_> {
563    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
564        f.write_str(self.raw)
565    }
566}
567
568impl fmt::Debug for ArcTrl {
569    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
570        f.debug_tuple("ArcTrl").field(&&*self.raw).finish()
571    }
572}
573
574impl fmt::Display for ArcTrl {
575    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
576        f.write_str(&self.raw)
577    }
578}
579
580/// The object pointed to by a Tango Resource Locator.
581pub enum TrlObject<'a> {
582    /// An alias for a device or attribute.
583    Alias(&'a str),
584    /// A device name.
585    Dev(&'a str),
586    /// An attribute name with its device name.
587    Attr(&'a str, &'a str),
588}
589
590/// From the RFC.
591fn is_hostname_char((i, c): (usize, char)) -> bool {
592    c.is_ascii_alphanumeric() || (i != 0 && (c == '-' || c == '.'))
593}
594
595/// From the RFC. 
596/// 
597/// With added:
598/// - Dash as non first char '-'
599/// - Digits as first char
600fn is_tango_name_char((i, c): (usize, char)) -> bool {
601    c.is_ascii_alphanumeric() || (i != 0 && (c == '_' || c == '-'))
602}
603
604/// Something that can be parsed into, or borrowed as, a [`Trl`].
605pub trait IntoTrl<'a> {
606    /// Get this as a TRL, failing if it is not a valid one.
607    fn into_trl(self) -> Result<Trl<'a>, TrlError>;
608}
609
610impl<'a> IntoTrl<'a> for Trl<'a> {
611    fn into_trl(self) -> Result<Trl<'a>, TrlError> {
612        Ok(self)
613    }
614}
615
616impl<'a> IntoTrl<'a> for &'a ArcTrl {
617    fn into_trl(self) -> Result<Trl<'a>, TrlError> {
618        Ok(self.as_trl())
619    }
620}
621
622impl<'a> IntoTrl<'a> for &'a str {
623    fn into_trl(self) -> Result<Trl<'a>, TrlError> {
624        Trl::parse(self)
625    }
626}
627
628
629#[cfg(test)]
630mod tests {
631
632    use super::*;
633    use std::num::NonZero;
634    use std::collections::HashSet;
635
636    #[test]
637    fn name_ignores_case() {
638
639        assert_eq!(Name::new("Sys/TG_Test/1"), Name::new("sys/tg_test/1"));
640        assert_ne!(Name::new("sys/tg_test/1"), Name::new("sys/tg_test/2"));
641        assert_ne!(Name::new("attr"), Name::new("attr_2"));
642
643        let names = HashSet::from([ArcName::from("Double_Scalar")]);
644        assert!(names.contains(Name::new("double_scalar")));
645        assert!(!names.contains(Name::new("double_scalar_w")));
646
647    }
648
649    #[test]
650    fn name_keeps_case() {
651
652        let name = Name::new("Double_Scalar");
653        assert_eq!(name.as_str(), "Double_Scalar");
654        assert_eq!(name.to_string(), "Double_Scalar");
655
656        let arc_name = name.to_arc();
657        assert_eq!(arc_name.as_str(), "Double_Scalar");
658        assert_eq!(arc_name.to_string(), "Double_Scalar");
659        assert_eq!("double_scalar".parse::<ArcName>().unwrap(), arc_name);
660
661    }
662
663    /// Letters, digits, `_` and `-` only, the latter two never first.
664    #[test]
665    fn name_chars() {
666        assert!(check_tango_class_name("Motor_2-a"));
667        assert!(check_tango_class_name("2Motor"));
668        assert!(!check_tango_class_name(""));
669        assert!(!check_tango_class_name("_Motor"));
670        assert!(!check_tango_class_name("-Motor"));
671        assert!(!check_tango_class_name("Mo tor"));
672        assert!(!check_tango_class_name("Mo.tor"));
673        assert!(!check_tango_class_name("Mo/tor"));
674        assert!(!check_tango_class_name("Motör"));
675    }
676
677    /// At most 255 long for attributes, commands, properties and aliases, unbounded for
678    /// classes.
679    #[test]
680    fn name_length() {
681        let (max, over) = ("a".repeat(255), "a".repeat(256));
682        assert!(check_tango_attr_name(&max) && !check_tango_attr_name(&over));
683        assert!(check_tango_command_name(&max) && !check_tango_command_name(&over));
684        assert!(check_tango_property_name(&max) && !check_tango_property_name(&over));
685        assert!(check_tango_alias(&max) && !check_tango_alias(&over));
686        assert!(check_tango_class_name(&over));
687    }
688
689    /// Exactly three parts, each a name at most 85 long.
690    #[test]
691    fn dev_name() {
692        assert!(check_tango_dev_name("sys/tg_test/1"));
693        assert!(check_tango_dev_name(&format!("{0}/{0}/{0}", "a".repeat(85))));
694        assert!(!check_tango_dev_name(&format!("a/a/{}", "a".repeat(86))));
695        assert!(!check_tango_dev_name("sys/tg_test"));
696        assert!(!check_tango_dev_name("sys/tg_test/1/2"));
697        assert!(!check_tango_dev_name("sys//1"));
698        assert!(!check_tango_dev_name("sys/tg_test/_1"));
699    }
700
701    /// Exactly two parts, each a name.
702    #[test]
703    fn server_name() {
704        assert!(check_tango_server_name("TangoTest/test"));
705        assert!(!check_tango_server_name("TangoTest"));
706        assert!(!check_tango_server_name("TangoTest/"));
707        assert!(!check_tango_server_name("TangoTest/te/st"));
708        assert!(!check_tango_server_name("TangoTest/te st"));
709    }
710
711    #[test]
712    fn parse_and_get() {
713
714        #[derive(Debug, Clone, Copy, PartialEq, Eq)]
715        enum TrlObjectKind {
716            Alias,
717            Dev,
718            Attr,
719        }
720
721        for protocol in [false, true] {
722            for host_and_port in [false, true] {
723                for object in [TrlObjectKind::Alias, TrlObjectKind::Dev, TrlObjectKind::Attr] {
724                    for property in [false, true] {
725                        for db in [None, Some(false), Some(true)] {
726                        
727                            let mut buf = String::new();
728                            
729                            if protocol {
730                                buf.push_str("tango://");
731                            }
732
733                            if host_and_port {
734                                buf.push_str("example.com:10000/");
735                            }
736
737                            let object_index = buf.len() as u16;
738                            match object {
739                                TrlObjectKind::Alias => buf.push_str("alias"),
740                                TrlObjectKind::Dev => buf.push_str("domain/family/member"),
741                                TrlObjectKind::Attr => buf.push_str("domain/family/member/attribute"),
742                            }
743
744                            if property {
745                                buf.push_str("->property");
746                            }
747
748                            match db {
749                                Some(false) => buf.push_str("#dbase=no"),
750                                Some(true) => buf.push_str("#dbase=yes"),
751                                None => (),
752                            }
753
754                            let trl = Trl::parse(&buf).unwrap();
755
756                            assert_eq!(trl.meta.protocol, protocol);
757                            assert_eq!(trl.meta.host_len_and_port, host_and_port.then_some((NonZero::new(11u8).unwrap(), NonZero::new(10000u16).unwrap())));
758                            assert_eq!(trl.meta.object_index, object_index);
759                            assert_eq!(trl.meta.object, match object {
760                                TrlObjectKind::Alias => TrlObjectMeta::Alias(NonZero::new(5u8).unwrap()),
761                                TrlObjectKind::Dev => TrlObjectMeta::Dev(NonZero::new(18u8).unwrap()),
762                                TrlObjectKind::Attr => TrlObjectMeta::Attr(NonZero::new(18u8).unwrap(), NonZero::new(9u8).unwrap()),
763                            });
764                            assert_eq!(trl.meta.property_len, property.then_some(NonZero::new(8u8).unwrap()));
765                            assert_eq!(trl.meta.db, db.unwrap_or(true));
766
767                        }
768                    }
769                }
770            }
771        }
772
773    }
774
775}