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
155#[derive(thiserror::Error, Debug)]
156pub enum ParseTrlError {
157    #[error("Invalid protocol, expecting tango://")]
158    InvalidProtocol,
159    #[error("Invalid host, wrong characters or too long (> 255)")]
160    InvalidHost,
161    #[error("Invalid port, expecting non-null 16-bit integer")]
162    InvalidPort,
163    #[error("Invalid hash, expecting #dbase=yes/no")]
164    InvalidHash,
165    #[error("Invalid or missing path, device-name[/attribute] with device-name being an alias or domain/family/member")]
166    InvalidPath,
167    #[error("Invalid device name, wrong characters or too long (> 85 per domain/family/member)")]
168    InvalidDeviceName,
169    #[error("Invalid attribute name, wrong characters or too long (> 255)")]
170    InvalidAttrName,
171    #[error("Invalid device or attribute alias, wrong characters or too long (> 255)")]
172    InvalidAlias,
173    #[error("Invalid property name, wrong characters or too long (> 255)")]
174    InvalidPropertyName,
175}
176
177/// Tango Resource Locator, borrowing the string it was parsed from.
178/// 
179/// This is a custom reference: it is [`Copy`] and parsing one allocates nothing, the
180/// owned counterpart being [`ArcTrl`].
181/// 
182/// RFC: 
183/// - https://tango-controls.readthedocs.io/projects/rfc/en/latest/16/TangoResourceLocator.html
184/// - https://tango-controls.readthedocs.io/projects/rfc/en/latest/2/Device.html
185/// - https://tango-controls.readthedocs.io/projects/rfc/en/latest/4/Attribute.html
186/// - https://tango-controls.readthedocs.io/projects/rfc/en/latest/5/Property.html
187#[derive(Clone, Copy)]
188pub struct Trl<'a> {
189    /// The raw string.
190    raw: &'a str,
191    /// Where the parts are in the raw string.
192    meta: TrlMeta,
193}
194
195/// Tango Resource Locator, owning the string it was parsed from.
196/// 
197/// Cloning this structure is cheap because it is backed by a Arc, and [`Self::as_trl`]
198/// gives the [`Trl`] to read it through.
199#[derive(Clone)]
200pub struct ArcTrl {
201    /// The raw string.
202    raw: Arc<str>,
203    /// Where the parts are in the raw string.
204    meta: TrlMeta,
205}
206
207/// Where the parts of a TRL are in its raw string, shared by [`Trl`] and [`ArcTrl`].
208#[derive(Debug, Clone, Copy)]
209struct TrlMeta {
210    /// If the host and port are present, this is the length of it.
211    host_len_and_port: Option<(NonZero<u8>, NonZero<u16>)>,
212    /// Offset of where the object starts.
213    object_index: u16,
214    /// Inner description of the object.
215    object: TrlObjectMeta,
216    /// From RFC 5/Property, maximum property name length is 255.
217    property_len: Option<NonZero<u8>>,
218    /// True if the tango:// protocol is present.
219    protocol: bool,
220    /// True if the host/port directly targets a database or not.
221    db: bool,
222}
223
224/// The length of the device located, including the slashes. This can target either a
225/// device or a device attribute, or be a simple alias without slashs. Both device
226/// name and alias have max length of 255 if we don't count the slashes.
227/// 
228/// From RFC 4/Attribute, maximum attr name length is 255.
229#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
230enum TrlObjectMeta {
231    Alias(NonZero<u8>),
232    Dev(NonZero<u8>),
233    Attr(NonZero<u8>, NonZero<u8>),
234}
235
236impl TrlMeta {
237
238    fn parse(raw: &str) -> Result<Self, ParseTrlError> {
239
240        let mut rem = raw;
241        let mut object_offset = 0u16;
242
243        // Check protocol
244        let mut protocol = false;
245        if let Some((proto, new_rem)) = rem.split_once("://") {
246            if proto != "tango" {
247                return Err(ParseTrlError::InvalidProtocol);
248            }
249            object_offset += "tango://".len() as u16;
250            protocol = true;
251            rem = new_rem;
252        }
253
254        // Parse the end hash first.
255        let mut db = true;
256        if let Some((new_rem, hash)) = rem.rsplit_once('#') {
257            db = match hash {
258                "dbase=yes" => true,
259                "dbase=no" => false,
260                _ => return Err(ParseTrlError::InvalidHash),
261            };
262            rem = new_rem;
263        }
264
265        // Then parse the property, we don't validate the property immediately because 
266        // acceptable characters depends on if this is device property or attr property.
267        let mut property_len = None;
268        if let Some((new_rem, property_raw)) = rem.rsplit_once("->") {
269
270            property_len = Some(u8::try_from(property_raw.len()).ok()
271                .and_then(NonZero::new)
272                .ok_or(ParseTrlError::InvalidPropertyName)?);
273
274            if !property_raw.chars().enumerate().all(is_tango_name_char) {
275                return Err(ParseTrlError::InvalidPropertyName);
276            }
277
278            rem = new_rem;
279
280        }
281
282        // Only 4 parts at most, construct an array on stack.
283        let mut parts = [""; 5];
284        let mut parts_count = 0;
285        for part in rem.split('/') {
286            // If there are more part it's not normal!
287            if parts_count >= parts.len() {
288                return Err(ParseTrlError::InvalidPath);
289            }
290            parts[parts_count] = part;
291            parts_count += 1;
292        }
293
294        
295        // Get a slice with the real number of parts.
296        let mut parts = &mut parts[..parts_count];
297        debug_assert!(!parts.is_empty(), "A split we should have at least one element");
298
299        // Try to find host:port in the first part if possible.
300        let mut host_len_and_port = None;
301        if let Some((host_raw, port_raw)) = parts[0].split_once(':') {
302
303            // Remove the host:port part from the parts, check if there is still parts.
304            parts = &mut parts[1..];
305            if parts.is_empty() {
306                return Err(ParseTrlError::InvalidPath)
307            }
308
309            let host_len = u8::try_from(host_raw.len()).ok()
310                .and_then(NonZero::new)
311                .ok_or(ParseTrlError::InvalidHost)?;
312
313            if !host_raw.chars().enumerate().all(is_hostname_char) {
314                return Err(ParseTrlError::InvalidHost);
315            }
316
317            let port = port_raw.parse::<NonZero<u16>>()
318                .map_err(|_| ParseTrlError::InvalidPort)?;
319
320            host_len_and_port = Some((host_len, port));
321
322            // +1 for ':', +1 for '/'
323            object_offset += host_raw.len() as u16 + 1 + port_raw.len() as u16 + 1;
324
325        }
326
327        // If this is an alias, it can be of length 255.
328        let object;
329        if parts.len() == 1 {
330            
331            object = TrlObjectMeta::Alias(u8::try_from(parts[0].len()).ok()
332                .and_then(NonZero::new)
333                .ok_or(ParseTrlError::InvalidAlias)?);
334
335            if !parts[0].chars().enumerate().all(is_tango_name_char) {
336                return Err(ParseTrlError::InvalidAlias)
337            }
338
339        } else if parts.len() == 3 || parts.len() == 4 {
340
341            let mut dev_len_raw = 0; // Add the 2 slashes to len.
342            for dev_part in &parts[..3] {
343                
344                if dev_part.len() == 0 || dev_part.len() > 85 {
345                    return Err(ParseTrlError::InvalidDeviceName);
346                }
347
348                if !dev_part.chars().enumerate().all(is_tango_name_char) {
349                    return Err(ParseTrlError::InvalidDeviceName);
350                }
351
352                dev_len_raw += dev_part.len();
353
354            }
355
356            // Unwrap because we check every part to not be empty.
357            let dev_len = NonZero::new(dev_len_raw as u8).unwrap();
358
359            if let Some(&attr_raw) = parts.get(3) {
360
361                object = TrlObjectMeta::Attr(dev_len, u8::try_from(attr_raw.len()).ok()
362                    .and_then(NonZero::new)
363                    .ok_or(ParseTrlError::InvalidAttrName)?);
364
365                if !attr_raw.chars().enumerate().all(is_tango_name_char) {
366                    return Err(ParseTrlError::InvalidAttrName);
367                }
368
369            } else {
370                object = TrlObjectMeta::Dev(dev_len);
371            }
372
373        } else {
374            return Err(ParseTrlError::InvalidPath);
375        }
376
377        Ok(Self {
378            host_len_and_port,
379            object_index: object_offset,
380            object,
381            property_len,
382            protocol,
383            db,
384        })
385
386    }
387
388}
389
390impl<'a> Trl<'a> {
391
392    /// Parse a TRL, borrowing the given string.
393    pub fn parse(raw: &'a str) -> Result<Self, ParseTrlError> {
394        Ok(Self { raw, meta: TrlMeta::parse(raw)? })
395    }
396
397    /// Return the raw string for this TRL.
398    #[inline]
399    pub fn as_str(self) -> &'a str {
400        self.raw
401    }
402
403    /// Copy the raw string into an owned TRL, without parsing it again.
404    #[inline]
405    pub fn to_arc(self) -> ArcTrl {
406        ArcTrl { raw: Arc::from(self.raw), meta: self.meta }
407    }
408
409    #[inline]
410    fn host_and_port_index(self) -> usize {
411        if self.meta.protocol { "tango://".len() } else { 0 }
412    }
413
414    pub fn host_and_port(self) -> Option<(&'a str, u16)> {
415        let index = self.host_and_port_index();
416        self.meta.host_len_and_port.map(|(host_len, port)| {
417            (&self.raw[index..][..host_len.get() as usize], port.get())
418        })
419    }
420
421    #[inline]
422    fn object_index(self) -> usize {
423        self.meta.object_index as usize
424    }
425
426    pub fn object(self) -> TrlObject<'a> {
427        let index = self.object_index();
428        match self.meta.object {
429            TrlObjectMeta::Alias(alias_len) => {
430                TrlObject::Alias(&self.raw[index..][..alias_len.get() as usize])
431            }
432            TrlObjectMeta::Dev(dev_len) => {
433                // +2 for slashes that were ignored in length.
434                TrlObject::Dev(&self.raw[index..][..dev_len.get() as usize + 2])
435            }
436            TrlObjectMeta::Attr(dev_len, attr_len) => {
437                // +2 for slashes that were ignored in length.
438                let (dev_name, raw) = self.raw[index..].split_at(dev_len.get() as usize + 2);
439                // Start at 1 to ignore the slash between device and attribute.
440                let attr_name = &raw[1..][..attr_len.get() as usize];
441                TrlObject::Attr(dev_name, attr_name)
442            }
443        }
444    }
445
446    #[inline]
447    fn property_index(self) -> usize {
448        self.object_index() + match self.meta.object {
449            TrlObjectMeta::Alias(alias_len) => 
450                alias_len.get() as usize,
451            TrlObjectMeta::Dev(dev_len) => 
452                dev_len.get() as usize + 2,
453            TrlObjectMeta::Attr(dev_len, attr_len) => 
454                dev_len.get() as usize + attr_len.get() as usize + 3,
455        } + 2  // +2 for the "->"
456    }
457
458    pub fn property(self) -> Option<&'a str> {
459        let index = self.property_index();
460        self.meta.property_len.map(|len| {
461            &self.raw[index..][..len.get() as usize]
462        })
463    }
464
465    #[inline]
466    pub fn db(self) -> bool {
467        self.meta.db
468    }
469
470}
471
472impl ArcTrl {
473
474    /// Borrow this TRL.
475    #[inline]
476    pub fn as_trl(&self) -> Trl<'_> {
477        Trl { raw: &self.raw, meta: self.meta }
478    }
479
480    /// Return the raw string for this TRL.
481    #[inline]
482    pub fn as_str(&self) -> &str {
483        &self.raw
484    }
485
486}
487
488impl FromStr for ArcTrl {
489
490    type Err = ParseTrlError;
491
492    fn from_str(raw: &str) -> Result<Self, Self::Err> {
493        Ok(Self { raw: Arc::from(raw), meta: TrlMeta::parse(raw)? })
494    }
495
496}
497
498impl From<Trl<'_>> for ArcTrl {
499    #[inline]
500    fn from(trl: Trl<'_>) -> Self {
501        trl.to_arc()
502    }
503}
504
505impl<'a> From<&'a ArcTrl> for Trl<'a> {
506    #[inline]
507    fn from(trl: &'a ArcTrl) -> Self {
508        trl.as_trl()
509    }
510}
511
512impl fmt::Debug for Trl<'_> {
513    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
514        f.debug_tuple("Trl").field(&self.raw).finish()
515    }
516}
517
518impl fmt::Display for Trl<'_> {
519    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
520        f.write_str(self.raw)
521    }
522}
523
524impl fmt::Debug for ArcTrl {
525    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
526        f.debug_tuple("ArcTrl").field(&&*self.raw).finish()
527    }
528}
529
530impl fmt::Display for ArcTrl {
531    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
532        f.write_str(&self.raw)
533    }
534}
535
536/// The object pointed to by a Tango Resource Locator.
537pub enum TrlObject<'a> {
538    /// An alias for a device or attribute.
539    Alias(&'a str),
540    /// A device name.
541    Dev(&'a str),
542    /// An attribute name with its device name.
543    Attr(&'a str, &'a str),
544}
545
546/// From the RFC.
547fn is_hostname_char((i, c): (usize, char)) -> bool {
548    c.is_ascii_alphanumeric() || (i != 0 && (c == '-' || c == '.'))
549}
550
551/// From the RFC. 
552/// 
553/// With added:
554/// - Dash as non first char '-'
555/// - Digits as first char
556fn is_tango_name_char((i, c): (usize, char)) -> bool {
557    c.is_ascii_alphanumeric() || (i != 0 && (c == '_' || c == '-'))
558}
559
560// fn is_tango_attr_property_name_char((i, c): (usize, char)) -> bool {
561//     c.is_ascii_alphabetic() || c == '_' || (i != 0 && c.is_ascii_digit())
562// }
563
564pub trait IntoTrl<'a> {
565    fn into_trl(self) -> Result<Trl<'a>, ParseTrlError>;
566}
567
568impl<'a> IntoTrl<'a> for Trl<'a> {
569    fn into_trl(self) -> Result<Trl<'a>, ParseTrlError> {
570        Ok(self)
571    }
572}
573
574impl<'a> IntoTrl<'a> for &'a ArcTrl {
575    fn into_trl(self) -> Result<Trl<'a>, ParseTrlError> {
576        Ok(self.as_trl())
577    }
578}
579
580impl<'a> IntoTrl<'a> for &'a str {
581    fn into_trl(self) -> Result<Trl<'a>, ParseTrlError> {
582        Trl::parse(self)
583    }
584}
585
586
587#[cfg(test)]
588mod tests {
589
590    use super::*;
591    use std::num::NonZero;
592    use std::collections::HashSet;
593
594    #[test]
595    fn name_ignores_case() {
596
597        assert_eq!(Name::new("Sys/TG_Test/1"), Name::new("sys/tg_test/1"));
598        assert_ne!(Name::new("sys/tg_test/1"), Name::new("sys/tg_test/2"));
599        assert_ne!(Name::new("attr"), Name::new("attr_2"));
600
601        let names = HashSet::from([ArcName::from("Double_Scalar")]);
602        assert!(names.contains(Name::new("double_scalar")));
603        assert!(!names.contains(Name::new("double_scalar_w")));
604
605    }
606
607    #[test]
608    fn name_keeps_case() {
609
610        let name = Name::new("Double_Scalar");
611        assert_eq!(name.as_str(), "Double_Scalar");
612        assert_eq!(name.to_string(), "Double_Scalar");
613
614        let arc_name = name.to_arc();
615        assert_eq!(arc_name.as_str(), "Double_Scalar");
616        assert_eq!(arc_name.to_string(), "Double_Scalar");
617        assert_eq!("double_scalar".parse::<ArcName>().unwrap(), arc_name);
618
619    }
620
621    #[test]
622    fn parse_and_get() {
623
624        #[derive(Debug, Clone, Copy, PartialEq, Eq)]
625        enum TrlObjectKind {
626            Alias,
627            Dev,
628            Attr,
629        }
630
631        for protocol in [false, true] {
632            for host_and_port in [false, true] {
633                for object in [TrlObjectKind::Alias, TrlObjectKind::Dev, TrlObjectKind::Attr] {
634                    for property in [false, true] {
635                        for db in [None, Some(false), Some(true)] {
636                        
637                            let mut buf = String::new();
638                            
639                            if protocol {
640                                buf.push_str("tango://");
641                            }
642
643                            if host_and_port {
644                                buf.push_str("example.com:10000/");
645                            }
646
647                            let object_index = buf.len() as u16;
648                            match object {
649                                TrlObjectKind::Alias => buf.push_str("alias"),
650                                TrlObjectKind::Dev => buf.push_str("domain/family/member"),
651                                TrlObjectKind::Attr => buf.push_str("domain/family/member/attribute"),
652                            }
653
654                            if property {
655                                buf.push_str("->property");
656                            }
657
658                            match db {
659                                Some(false) => buf.push_str("#dbase=no"),
660                                Some(true) => buf.push_str("#dbase=yes"),
661                                None => (),
662                            }
663
664                            let trl = Trl::parse(&buf).unwrap();
665
666                            assert_eq!(trl.meta.protocol, protocol);
667                            assert_eq!(trl.meta.host_len_and_port, host_and_port.then_some((NonZero::new(11u8).unwrap(), NonZero::new(10000u16).unwrap())));
668                            assert_eq!(trl.meta.object_index, object_index);
669                            assert_eq!(trl.meta.object, match object {
670                                TrlObjectKind::Alias => TrlObjectMeta::Alias(NonZero::new(5u8).unwrap()),
671                                TrlObjectKind::Dev => TrlObjectMeta::Dev(NonZero::new(18u8).unwrap()),
672                                TrlObjectKind::Attr => TrlObjectMeta::Attr(NonZero::new(18u8).unwrap(), NonZero::new(9u8).unwrap()),
673                            });
674                            assert_eq!(trl.meta.property_len, property.then_some(NonZero::new(8u8).unwrap()));
675                            assert_eq!(trl.meta.db, db.unwrap_or(true));
676
677                        }
678                    }
679                }
680            }
681        }
682
683    }
684
685}