fluent_uri/
convert.rs

1use crate::{imp::RiMaybeRef, Iri, IriRef, Uri, UriRef};
2use borrow_or_share::Bos;
3use core::str;
4
5#[cfg(feature = "alloc")]
6use crate::{
7    imp::{HostMeta, Meta, RmrRef},
8    pct_enc,
9};
10#[cfg(feature = "alloc")]
11use alloc::string::String;
12#[cfg(feature = "alloc")]
13use core::num::NonZeroUsize;
14
15macro_rules! impl_from {
16    ($($x:ident => $($y:ident),+)*) => {
17        $($(
18            impl<T: Bos<str>> From<$x<T>> for $y<T> {
19                #[doc = concat!("Consumes the `", stringify!($x), "` and creates a new [`", stringify!($y), "`] with the same contents.")]
20                fn from(value: $x<T>) -> Self {
21                    RiMaybeRef::new(value.val, value.meta)
22                }
23            }
24        )+)*
25    };
26}
27
28impl_from! {
29    Uri => UriRef, Iri, IriRef
30    UriRef => IriRef
31    Iri => IriRef
32}
33
34/// An error occurred when downcasting a URI/IRI (reference).
35#[derive(Clone, Copy, Debug, Eq, PartialEq)]
36pub enum ConvertError {
37    /// The input is not ASCII.
38    NotAscii {
39        /// The index of the first non-ASCII character.
40        index: usize,
41    },
42    /// The input has no scheme.
43    NoScheme,
44}
45
46#[cfg(feature = "impl-error")]
47impl crate::Error for ConvertError {}
48
49macro_rules! impl_try_from {
50    ($(#[$doc:meta] $x:ident if $($cond:ident)&&+ => $y:ident)*) => {
51        $(
52            impl<'a> TryFrom<$x<&'a str>> for $y<&'a str> {
53                type Error = ConvertError;
54
55                #[$doc]
56                fn try_from(value: $x<&'a str>) -> Result<Self, Self::Error> {
57                    let r = value.make_ref();
58                    $(r.$cond()?;)+
59                    Ok(RiMaybeRef::new(value.val, value.meta))
60                }
61            }
62
63            #[cfg(feature = "alloc")]
64            impl TryFrom<$x<String>> for $y<String> {
65                type Error = (ConvertError, $x<String>);
66
67                #[$doc]
68                fn try_from(value: $x<String>) -> Result<Self, Self::Error> {
69                    let r = value.make_ref();
70                    $(
71                        if let Err(e) = r.$cond() {
72                            return Err((e, value));
73                        }
74                    )+
75                    Ok(RiMaybeRef::new(value.val, value.meta))
76                }
77            }
78        )*
79    };
80}
81
82impl_try_from! {
83    /// Converts the URI reference to a URI if it has a scheme.
84    UriRef if ensure_has_scheme => Uri
85    /// Converts the IRI to a URI if it is ASCII.
86    Iri if ensure_ascii => Uri
87    /// Converts the IRI reference to a URI if it has a scheme and is ASCII.
88    IriRef if ensure_has_scheme && ensure_ascii => Uri
89    /// Converts the IRI reference to a URI reference if it is ASCII.
90    IriRef if ensure_ascii => UriRef
91    /// Converts the IRI reference to an IRI if it has a scheme.
92    IriRef if ensure_has_scheme => Iri
93}
94
95#[cfg(feature = "alloc")]
96impl<T: Bos<str>> Iri<T> {
97    /// Converts the IRI to a URI by percent-encoding non-ASCII characters.
98    ///
99    /// Punycode encoding is **not** performed during conversion.
100    ///
101    /// # Examples
102    ///
103    /// ```
104    /// use fluent_uri::Iri;
105    ///
106    /// let iri = Iri::parse("http://www.example.org/résumé.html").unwrap();
107    /// assert_eq!(iri.to_uri(), "http://www.example.org/r%C3%A9sum%C3%A9.html");
108    ///
109    /// let iri = Iri::parse("http://résumé.example.org").unwrap();
110    /// assert_eq!(iri.to_uri(), "http://r%C3%A9sum%C3%A9.example.org");
111    /// ```
112    pub fn to_uri(&self) -> Uri<String> {
113        RiMaybeRef::from_pair(encode_non_ascii(self.make_ref()))
114    }
115}
116
117#[cfg(feature = "alloc")]
118impl<T: Bos<str>> IriRef<T> {
119    /// Converts the IRI reference to a URI reference by percent-encoding non-ASCII characters.
120    ///
121    /// Punycode encoding is **not** performed during conversion.
122    ///
123    /// # Examples
124    ///
125    /// ```
126    /// use fluent_uri::IriRef;
127    ///
128    /// let iri_ref = IriRef::parse("résumé.html").unwrap();
129    /// assert_eq!(iri_ref.to_uri_ref(), "r%C3%A9sum%C3%A9.html");
130    ///
131    /// let iri_ref = IriRef::parse("//résumé.example.org").unwrap();
132    /// assert_eq!(iri_ref.to_uri_ref(), "//r%C3%A9sum%C3%A9.example.org");
133    /// ```
134    pub fn to_uri_ref(&self) -> UriRef<String> {
135        RiMaybeRef::from_pair(encode_non_ascii(self.make_ref()))
136    }
137}
138
139#[cfg(feature = "alloc")]
140fn encode_non_ascii(r: RmrRef<'_, '_>) -> (String, Meta) {
141    let len = r
142        .as_str()
143        .chars()
144        .map(|c| if c.is_ascii() { 1 } else { c.len_utf8() * 3 })
145        .sum();
146
147    let mut buf = String::with_capacity(len);
148    let mut meta = Meta::default();
149
150    if let Some(scheme) = r.scheme_opt() {
151        buf.push_str(scheme.as_str());
152        meta.scheme_end = NonZeroUsize::new(buf.len());
153        buf.push(':');
154    }
155
156    if let Some(auth) = r.authority() {
157        buf.push_str("//");
158
159        if let Some(userinfo) = auth.userinfo() {
160            encode_non_ascii_str(&mut buf, userinfo.as_str());
161            buf.push('@');
162        }
163
164        let mut auth_meta = auth.meta();
165        auth_meta.host_bounds.0 = buf.len();
166        match auth_meta.host_meta {
167            HostMeta::RegName => encode_non_ascii_str(&mut buf, auth.host()),
168            _ => buf.push_str(auth.host()),
169        }
170        auth_meta.host_bounds.1 = buf.len();
171        meta.auth_meta = Some(auth_meta);
172
173        if let Some(port) = auth.port() {
174            buf.push(':');
175            buf.push_str(port.as_str());
176        }
177    }
178
179    meta.path_bounds.0 = buf.len();
180    encode_non_ascii_str(&mut buf, r.path().as_str());
181    meta.path_bounds.1 = buf.len();
182
183    if let Some(query) = r.query() {
184        buf.push('?');
185        encode_non_ascii_str(&mut buf, query.as_str());
186        meta.query_end = NonZeroUsize::new(buf.len());
187    }
188
189    if let Some(fragment) = r.fragment() {
190        buf.push('#');
191        encode_non_ascii_str(&mut buf, fragment.as_str());
192    }
193
194    debug_assert_eq!(buf.len(), len);
195
196    (buf, meta)
197}
198
199#[cfg(feature = "alloc")]
200fn encode_non_ascii_str(buf: &mut String, s: &str) {
201    if s.is_ascii() {
202        buf.push_str(s);
203    } else {
204        let mut iter = s.char_indices();
205        while let Some((start, ch)) = iter.next() {
206            if ch.is_ascii() {
207                buf.push(ch);
208            } else {
209                // `CharIndices::offset` sadly requires an MSRV of 1.82,
210                // so we do pointer math to get the offset for now.
211                let end = iter.as_str().as_ptr() as usize - s.as_ptr() as usize;
212                for &x in &s.as_bytes()[start..end] {
213                    buf.push_str(pct_enc::encode_byte(x));
214                }
215            }
216        }
217    }
218}