fluent_uri/
component.rs

1//! URI/IRI components.
2
3use crate::{
4    imp::{AuthMeta, HostMeta},
5    pct_enc::{
6        encoder::{IRegName, IUserinfo, Port, RegName, Userinfo},
7        table, EStr, Encoder,
8    },
9};
10use core::{hash, iter, marker::PhantomData, num::ParseIntError};
11use ref_cast::{ref_cast_custom, RefCastCustom};
12
13#[cfg(feature = "net")]
14use crate::net::{Ipv4Addr, Ipv6Addr};
15
16#[cfg(all(feature = "net", feature = "std"))]
17use std::{
18    io,
19    net::{SocketAddr, ToSocketAddrs},
20};
21
22/// An authority component for IRI.
23pub type IAuthority<'a> = Authority<'a, IUserinfo, IRegName>;
24
25/// A parsed host component for IRI.
26pub type IHost<'a> = Host<'a, IRegName>;
27
28/// A [scheme] component.
29///
30/// [scheme]: https://datatracker.ietf.org/doc/html/rfc3986#section-3.1
31///
32/// # Comparison
33///
34/// `Scheme`s are compared case-insensitively. You should do a case-insensitive
35/// comparison if the scheme specification allows both letter cases in the scheme name.
36///
37/// # Examples
38///
39/// ```
40/// use fluent_uri::{component::Scheme, Uri};
41///
42/// const SCHEME_HTTP: &Scheme = Scheme::new_or_panic("http");
43///
44/// let scheme = Uri::parse("HTTP://EXAMPLE.COM/")?.scheme();
45///
46/// // Case-insensitive comparison.
47/// assert_eq!(scheme, SCHEME_HTTP);
48/// // Case-sensitive comparison.
49/// assert_eq!(scheme.as_str(), "HTTP");
50/// # Ok::<_, fluent_uri::ParseError>(())
51/// ```
52#[derive(RefCastCustom)]
53#[repr(transparent)]
54pub struct Scheme {
55    inner: str,
56}
57
58const ASCII_CASE_MASK: u8 = 0b0010_0000;
59
60impl Scheme {
61    #[ref_cast_custom]
62    #[inline]
63    pub(crate) const fn new_validated(scheme: &str) -> &Self;
64
65    /// Converts a string slice to `&Scheme`.
66    ///
67    /// # Panics
68    ///
69    /// Panics if the string is not a valid scheme name according to
70    /// [Section 3.1 of RFC 3986][scheme]. For a non-panicking variant,
71    /// use [`new`](Self::new).
72    ///
73    /// [scheme]: https://datatracker.ietf.org/doc/html/rfc3986#section-3.1
74    #[inline]
75    #[must_use]
76    pub const fn new_or_panic(s: &str) -> &Self {
77        match Self::new(s) {
78            Some(scheme) => scheme,
79            None => panic!("invalid scheme"),
80        }
81    }
82
83    /// Converts a string slice to `&Scheme`, returning `None` if the conversion fails.
84    #[inline]
85    #[must_use]
86    pub const fn new(s: &str) -> Option<&Self> {
87        if matches!(s.as_bytes(), [first, rem @ ..]
88        if first.is_ascii_alphabetic() && table::SCHEME.validate(rem))
89        {
90            Some(Self::new_validated(s))
91        } else {
92            None
93        }
94    }
95
96    /// Returns the scheme component as a string slice.
97    ///
98    /// # Examples
99    ///
100    /// ```
101    /// use fluent_uri::Uri;
102    ///
103    /// let uri = Uri::parse("http://example.com/")?;
104    /// assert_eq!(uri.scheme().as_str(), "http");
105    /// let uri = Uri::parse("HTTP://EXAMPLE.COM/")?;
106    /// assert_eq!(uri.scheme().as_str(), "HTTP");
107    /// # Ok::<_, fluent_uri::ParseError>(())
108    /// ```
109    #[inline]
110    #[must_use]
111    pub fn as_str(&self) -> &str {
112        &self.inner
113    }
114}
115
116macro_rules! default_port {
117    ($($name:literal, $bname:literal => $port:literal, rfc($rfc:literal))*) => {
118        impl Scheme {
119            /// Returns the optional default port of the scheme if it is
120            /// registered [at IANA][iana] with a permanent status.
121            ///
122            /// [iana]: https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml
123            ///
124            /// The following table lists all schemes concerned, their default ports, and references:
125            ///
126            /// | Scheme | Port | Reference |
127            /// | - | - | - |
128            $(#[doc = concat!("| ", $name, " | ", $port, " | [RFC ", $rfc, "](https://datatracker.ietf.org/doc/html/rfc", $rfc, ") |")])*
129            #[must_use]
130            pub fn default_port(&self) -> Option<u16> {
131                const MAX_LEN: usize = {
132                    let mut res = 0;
133                    $(
134                        if $name.len() > res {
135                            res = $name.len();
136                        }
137                    )*
138                    res
139                };
140
141                let len = self.inner.len();
142                if len > MAX_LEN {
143                    return None;
144                }
145
146                let mut buf = [0; MAX_LEN];
147                for (i, b) in self.inner.bytes().enumerate() {
148                    buf[i] = b | ASCII_CASE_MASK;
149                }
150
151                match &buf[..len] {
152                    $($bname => Some($port),)*
153                    _ => None,
154                }
155            }
156        }
157    };
158}
159
160default_port! {
161    "aaa", b"aaa" => 3868, rfc(6733)
162    "aaas", b"aaas" => 5658, rfc(6733)
163    "acap", b"acap" => 674, rfc(2244)
164    "cap", b"cap" => 1026, rfc(4324)
165    "coap", b"coap" => 5683, rfc(7252)
166    "coap+tcp", b"coap+tcp" => 5683, rfc(8323)
167    "coap+ws", b"coap+ws" => 80, rfc(8323)
168    "coaps", b"coaps" => 5684, rfc(7252)
169    "coaps+tcp", b"coaps+tcp" => 5684, rfc(8323)
170    "coaps+ws", b"coaps+ws" => 443, rfc(8323)
171    "dict", b"dict" => 2628, rfc(2229)
172    "dns", b"dns" => 53, rfc(4501)
173    "ftp", b"ftp" => 21, rfc(1738)
174    "go", b"go" => 1096, rfc(3368)
175    "gopher", b"gopher" => 70, rfc(4266)
176    "http", b"http" => 80, rfc(9110)
177    "https", b"https" => 443, rfc(9110)
178    "icap", b"icap" => 1344, rfc(3507)
179    "imap", b"imap" => 143, rfc(5092)
180    "ipp", b"ipp" => 631, rfc(3510)
181    "ipps", b"ipps" => 631, rfc(7472)
182    "ldap", b"ldap" => 389, rfc(4516)
183    "mtqp", b"mtqp" => 1038, rfc(3887)
184    "mupdate", b"mupdate" => 3905, rfc(3656)
185    "nfs", b"nfs" => 2049, rfc(2224)
186    "nntp", b"nntp" => 119, rfc(5538)
187    "pop", b"pop" => 110, rfc(2384)
188    "rtsp", b"rtsp" => 554, rfc(7826)
189    "rtsps", b"rtsps" => 322, rfc(7826)
190    "rtspu", b"rtspu" => 554, rfc(2326)
191    "snmp", b"snmp" => 161, rfc(4088)
192    "stun", b"stun" => 3478, rfc(7064)
193    "stuns", b"stuns" => 5349, rfc(7064)
194    "telnet", b"telnet" => 23, rfc(4248)
195    "tip", b"tip" => 3372, rfc(2371)
196    "tn3270", b"tn3270" => 23, rfc(6270)
197    "turn", b"turn" => 3478, rfc(7065)
198    "turns", b"turns" => 5349, rfc(7065)
199    "vemmi", b"vemmi" => 575, rfc(2122)
200    "vnc", b"vnc" => 5900, rfc(7869)
201    "ws", b"ws" => 80, rfc(6455)
202    "wss", b"wss" => 443, rfc(6455)
203    "z39.50r", b"z39.50r" => 210, rfc(2056)
204    "z39.50s", b"z39.50s" => 210, rfc(2056)
205}
206
207impl PartialEq for Scheme {
208    #[inline]
209    fn eq(&self, other: &Self) -> bool {
210        let (a, b) = (self.inner.as_bytes(), other.inner.as_bytes());
211        // The only characters allowed in a scheme are alphabets, digits, '+', '-' and '.'.
212        // Their ASCII codes allow us to simply set the sixth bits and compare.
213        a.len() == b.len()
214            && iter::zip(a, b).all(|(x, y)| x | ASCII_CASE_MASK == y | ASCII_CASE_MASK)
215    }
216}
217
218impl Eq for Scheme {}
219
220impl hash::Hash for Scheme {
221    fn hash<H: hash::Hasher>(&self, state: &mut H) {
222        let mut buf = [0; 8];
223        for chunk in self.inner.as_bytes().chunks(8) {
224            let len = chunk.len();
225            for i in 0..len {
226                buf[i] = chunk[i] | ASCII_CASE_MASK;
227            }
228            state.write(&buf[..len]);
229        }
230    }
231}
232
233#[derive(Clone, Copy)]
234struct AuthorityInner<'a> {
235    val: &'a str,
236    meta: AuthMeta,
237}
238
239impl<'a> AuthorityInner<'a> {
240    fn userinfo(&self) -> Option<&'a EStr<IUserinfo>> {
241        let host_start = self.meta.host_bounds.0;
242        (host_start != 0).then(|| EStr::new_validated(&self.val[..host_start - 1]))
243    }
244
245    fn host(&self) -> &'a str {
246        let (start, end) = self.meta.host_bounds;
247        &self.val[start..end]
248    }
249
250    fn port(&self) -> Option<&'a EStr<Port>> {
251        let host_end = self.meta.host_bounds.1;
252        (host_end != self.val.len()).then(|| EStr::new_validated(&self.val[host_end + 1..]))
253    }
254
255    fn port_to_u16(&self) -> Result<Option<u16>, ParseIntError> {
256        self.port()
257            .filter(|s| !s.is_empty())
258            .map(|s| s.as_str().parse())
259            .transpose()
260    }
261
262    #[cfg(all(feature = "net", feature = "std"))]
263    fn socket_addrs(&self, default_port: u16) -> io::Result<impl Iterator<Item = SocketAddr>> {
264        use std::vec;
265
266        let port = self
267            .port_to_u16()
268            .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid port value"))?
269            .unwrap_or(default_port);
270
271        match self.meta.host_meta {
272            HostMeta::Ipv4(addr) => Ok(vec![(addr, port).into()].into_iter()),
273            HostMeta::Ipv6(addr) => Ok(vec![(addr, port).into()].into_iter()),
274            HostMeta::IpvFuture => Err(io::Error::new(
275                io::ErrorKind::InvalidInput,
276                "address mechanism not supported",
277            )),
278            HostMeta::RegName => {
279                let name = EStr::<IRegName>::new_validated(self.host());
280                let name = name.decode().to_string().map_err(|_| {
281                    io::Error::new(
282                        io::ErrorKind::InvalidInput,
283                        "registered name does not decode to valid UTF-8",
284                    )
285                })?;
286                (&name[..], port).to_socket_addrs()
287            }
288        }
289    }
290}
291
292/// An [authority] component.
293///
294/// [authority]: https://datatracker.ietf.org/doc/html/rfc3986#section-3.2
295#[derive(Clone, Copy)]
296pub struct Authority<'a, UserinfoE = Userinfo, RegNameE = RegName> {
297    inner: AuthorityInner<'a>,
298    _marker: PhantomData<(UserinfoE, RegNameE)>,
299}
300
301impl<'a, T, U> Authority<'a, T, U> {
302    pub(crate) fn cast<V, W>(self) -> Authority<'a, V, W> {
303        Authority {
304            inner: self.inner,
305            _marker: PhantomData,
306        }
307    }
308}
309
310impl<'a, UserinfoE: Encoder, RegNameE: Encoder> Authority<'a, UserinfoE, RegNameE> {
311    pub(crate) const fn new(val: &'a str, meta: AuthMeta) -> Self {
312        Self {
313            inner: AuthorityInner { val, meta },
314            _marker: PhantomData,
315        }
316    }
317
318    /// An empty authority component.
319    pub const EMPTY: Authority<'static, UserinfoE, RegNameE> = Authority::new("", AuthMeta::EMPTY);
320
321    #[cfg(feature = "alloc")]
322    pub(crate) fn meta(&self) -> AuthMeta {
323        self.inner.meta
324    }
325
326    /// Returns the authority component as a string slice.
327    ///
328    /// # Examples
329    ///
330    /// ```
331    /// use fluent_uri::Uri;
332    ///
333    /// let uri = Uri::parse("http://user@example.com:8080/")?;
334    /// let auth = uri.authority().unwrap();
335    /// assert_eq!(auth.as_str(), "user@example.com:8080");
336    /// # Ok::<_, fluent_uri::ParseError>(())
337    /// ```
338    #[inline]
339    #[must_use]
340    pub fn as_str(&self) -> &'a str {
341        self.inner.val
342    }
343
344    /// Returns the optional [userinfo] subcomponent.
345    ///
346    /// [userinfo]: https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.1
347    ///
348    /// # Examples
349    ///
350    /// ```
351    /// use fluent_uri::{pct_enc::EStr, Uri};
352    ///
353    /// let uri = Uri::parse("http://user@example.com/")?;
354    /// let auth = uri.authority().unwrap();
355    /// assert_eq!(auth.userinfo(), Some(EStr::new_or_panic("user")));
356    ///
357    /// let uri = Uri::parse("http://example.com/")?;
358    /// let auth = uri.authority().unwrap();
359    /// assert_eq!(auth.userinfo(), None);
360    /// # Ok::<_, fluent_uri::ParseError>(())
361    /// ```
362    #[must_use]
363    pub fn userinfo(&self) -> Option<&'a EStr<UserinfoE>> {
364        self.inner.userinfo().map(EStr::cast)
365    }
366
367    /// Returns the [host] subcomponent as a string slice.
368    ///
369    /// The host subcomponent is always present, although it may be empty.
370    ///
371    /// The square brackets enclosing an IPv6 or IPvFuture address are included.
372    ///
373    /// Note that ASCII characters within a host are *case-insensitive*.
374    ///
375    /// [host]: https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2
376    ///
377    /// # Examples
378    ///
379    /// ```
380    /// use fluent_uri::Uri;
381    ///
382    /// let uri = Uri::parse("http://user@example.com:8080/")?;
383    /// let auth = uri.authority().unwrap();
384    /// assert_eq!(auth.host(), "example.com");
385    ///
386    /// let uri = Uri::parse("file:///path/to/file")?;
387    /// let auth = uri.authority().unwrap();
388    /// assert_eq!(auth.host(), "");
389    ///
390    /// let uri = Uri::parse("http://[::1]")?;
391    /// let auth = uri.authority().unwrap();
392    /// assert_eq!(auth.host(), "[::1]");
393    /// # Ok::<_, fluent_uri::ParseError>(())
394    /// ```
395    #[must_use]
396    pub fn host(&self) -> &'a str {
397        self.inner.host()
398    }
399
400    /// Returns the parsed [host] subcomponent.
401    ///
402    /// Note that ASCII characters within a host are *case-insensitive*.
403    ///
404    /// [host]: https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2
405    ///
406    /// # Examples
407    ///
408    /// ```
409    /// use fluent_uri::{component::Host, pct_enc::EStr, Uri};
410    #[cfg_attr(feature = "net", doc = "use std::net::{Ipv4Addr, Ipv6Addr};")]
411    ///
412    /// let uri = Uri::parse("foo://127.0.0.1")?;
413    /// let auth = uri.authority().unwrap();
414    #[cfg_attr(
415        feature = "net",
416        doc = "assert!(matches!(auth.host_parsed(), Host::Ipv4(Ipv4Addr::LOCALHOST)));"
417    )]
418    #[cfg_attr(
419        not(feature = "net"),
420        doc = "assert!(matches!(auth.host_parsed(), Host::Ipv4 { .. }));"
421    )]
422    ///
423    /// let uri = Uri::parse("foo://[::1]")?;
424    /// let auth = uri.authority().unwrap();
425    #[cfg_attr(
426        feature = "net",
427        doc = "assert!(matches!(auth.host_parsed(), Host::Ipv6(Ipv6Addr::LOCALHOST)));"
428    )]
429    #[cfg_attr(
430        not(feature = "net"),
431        doc = "assert!(matches!(auth.host_parsed(), Host::Ipv6 { .. }));"
432    )]
433    ///
434    /// let uri = Uri::parse("foo://[v1.addr]")?;
435    /// let auth = uri.authority().unwrap();
436    /// // The API design for IPvFuture addresses is to be determined.
437    /// assert!(matches!(auth.host_parsed(), Host::IpvFuture { .. }));
438    ///
439    /// let uri = Uri::parse("foo://localhost")?;
440    /// let auth = uri.authority().unwrap();
441    /// assert!(matches!(auth.host_parsed(), Host::RegName(name) if name == "localhost"));
442    /// # Ok::<_, fluent_uri::ParseError>(())
443    /// ```
444    #[must_use]
445    pub fn host_parsed(&self) -> Host<'a, RegNameE> {
446        match self.inner.meta.host_meta {
447            #[cfg(feature = "net")]
448            HostMeta::Ipv4(addr) => Host::Ipv4(addr),
449            #[cfg(feature = "net")]
450            HostMeta::Ipv6(addr) => Host::Ipv6(addr),
451
452            #[cfg(not(feature = "net"))]
453            HostMeta::Ipv4() => Host::Ipv4(),
454            #[cfg(not(feature = "net"))]
455            HostMeta::Ipv6() => Host::Ipv6(),
456
457            HostMeta::IpvFuture => Host::IpvFuture,
458            HostMeta::RegName => Host::RegName(EStr::new_validated(self.host())),
459        }
460    }
461
462    /// Returns the optional [port] subcomponent.
463    ///
464    /// A scheme may define a [default port] to use when the port is
465    /// not present or is empty.
466    ///
467    /// Note that the port may be empty, with leading zeros, or larger than [`u16::MAX`].
468    /// It is up to you to decide whether to deny such ports, fallback to the scheme's
469    /// default if it is empty, ignore the leading zeros, or use a special addressing
470    /// mechanism that allows ports larger than [`u16::MAX`].
471    ///
472    /// [port]: https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.3
473    /// [default port]: Scheme::default_port
474    ///
475    /// # Examples
476    ///
477    /// ```
478    /// use fluent_uri::{pct_enc::EStr, Uri};
479    ///
480    /// let uri = Uri::parse("foo://localhost:4673/")?;
481    /// let auth = uri.authority().unwrap();
482    /// assert_eq!(auth.port(), Some(EStr::new_or_panic("4673")));
483    ///
484    /// let uri = Uri::parse("foo://localhost:/")?;
485    /// let auth = uri.authority().unwrap();
486    /// assert_eq!(auth.port(), Some(EStr::EMPTY));
487    ///
488    /// let uri = Uri::parse("foo://localhost/")?;
489    /// let auth = uri.authority().unwrap();
490    /// assert_eq!(auth.port(), None);
491    ///
492    /// let uri = Uri::parse("foo://localhost:123456/")?;
493    /// let auth = uri.authority().unwrap();
494    /// assert_eq!(auth.port(), Some(EStr::new_or_panic("123456")));
495    /// # Ok::<_, fluent_uri::ParseError>(())
496    /// ```
497    #[must_use]
498    pub fn port(&self) -> Option<&'a EStr<Port>> {
499        self.inner.port()
500    }
501
502    /// Converts the [port] subcomponent to `u16`, if present and nonempty.
503    ///
504    /// Returns `Ok(None)` if the port is not present or is empty. Leading zeros are ignored.
505    ///
506    /// [port]: https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.3
507    ///
508    /// # Errors
509    ///
510    /// Returns `Err` if the port cannot be parsed into `u16`.
511    ///
512    /// # Examples
513    ///
514    /// ```
515    /// use fluent_uri::Uri;
516    ///
517    /// let uri = Uri::parse("foo://localhost:4673/")?;
518    /// let auth = uri.authority().unwrap();
519    /// assert_eq!(auth.port_to_u16(), Ok(Some(4673)));
520    ///
521    /// let uri = Uri::parse("foo://localhost/")?;
522    /// let auth = uri.authority().unwrap();
523    /// assert_eq!(auth.port_to_u16(), Ok(None));
524    ///
525    /// let uri = Uri::parse("foo://localhost:/")?;
526    /// let auth = uri.authority().unwrap();
527    /// assert_eq!(auth.port_to_u16(), Ok(None));
528    ///
529    /// let uri = Uri::parse("foo://localhost:123456/")?;
530    /// let auth = uri.authority().unwrap();
531    /// assert!(auth.port_to_u16().is_err());
532    /// # Ok::<_, fluent_uri::ParseError>(())
533    /// ```
534    pub fn port_to_u16(&self) -> Result<Option<u16>, ParseIntError> {
535        self.inner.port_to_u16()
536    }
537
538    /// Converts the host and the port subcomponent to an iterator of resolved [`SocketAddr`]s.
539    ///
540    /// The default port is used if the port component is not present or is empty.
541    /// A registered name is first [decoded] and then resolved with [`ToSocketAddrs`].
542    /// Punycode encoding is **not** performed prior to resolution.
543    ///
544    /// [decoded]: EStr::decode
545    ///
546    /// # Errors
547    ///
548    /// Returns `Err` if any of the following is true.
549    ///
550    /// - The port cannot be parsed into `u16`.
551    /// - The host is an IPvFuture address.
552    /// - A registered name does not decode to valid UTF-8 or fails to resolve.
553    #[cfg(all(feature = "net", feature = "std"))]
554    pub fn socket_addrs(&self, default_port: u16) -> io::Result<impl Iterator<Item = SocketAddr>> {
555        self.inner.socket_addrs(default_port)
556    }
557
558    /// Checks whether a userinfo subcomponent is present.
559    ///
560    /// # Examples
561    ///
562    /// ```
563    /// use fluent_uri::Uri;
564    ///
565    /// let uri = Uri::parse("http://user@example.com/")?;
566    /// assert!(uri.authority().unwrap().has_userinfo());
567    ///
568    /// let uri = Uri::parse("http://example.com/")?;
569    /// assert!(!uri.authority().unwrap().has_userinfo());
570    /// # Ok::<_, fluent_uri::ParseError>(())
571    #[inline]
572    #[must_use]
573    pub fn has_userinfo(&self) -> bool {
574        self.inner.meta.host_bounds.0 != 0
575    }
576
577    /// Checks whether a port subcomponent is present.
578    ///
579    /// # Examples
580    ///
581    /// ```
582    /// use fluent_uri::Uri;
583    ///
584    /// let uri = Uri::parse("foo://localhost:4673/")?;
585    /// assert!(uri.authority().unwrap().has_port());
586    ///
587    /// // The port subcomponent can be empty.
588    /// let uri = Uri::parse("foo://localhost:/")?;
589    /// assert!(uri.authority().unwrap().has_port());
590    ///
591    /// let uri = Uri::parse("foo://localhost/")?;
592    /// let auth = uri.authority().unwrap();
593    /// assert!(!uri.authority().unwrap().has_port());
594    /// # Ok::<_, fluent_uri::ParseError>(())
595    /// ```
596    #[inline]
597    #[must_use]
598    pub fn has_port(&self) -> bool {
599        self.inner.meta.host_bounds.1 != self.inner.val.len()
600    }
601}
602
603/// A parsed [host] component.
604///
605/// [host]: https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2
606#[derive(Clone, Copy)]
607#[cfg_attr(fuzzing, derive(PartialEq, Eq))]
608pub enum Host<'a, RegNameE: Encoder = RegName> {
609    /// An IPv4 address.
610    #[cfg_attr(not(feature = "net"), non_exhaustive)]
611    Ipv4(
612        /// The address.
613        #[cfg(feature = "net")]
614        Ipv4Addr,
615    ),
616    /// An IPv6 address.
617    #[cfg_attr(not(feature = "net"), non_exhaustive)]
618    Ipv6(
619        /// The address.
620        #[cfg(feature = "net")]
621        Ipv6Addr,
622    ),
623    /// An IP address of future version.
624    ///
625    /// This variant is marked as non-exhaustive because the API design
626    /// for IPvFuture addresses is to be determined.
627    #[non_exhaustive]
628    IpvFuture,
629    /// A registered name.
630    ///
631    /// Note that ASCII characters within a registered name are *case-insensitive*.
632    RegName(&'a EStr<RegNameE>),
633}