Skip to main content

urly/
url.rs

1use std::any::Any;
2use std::borrow::{Borrow, Cow};
3use std::ops::{Add, AddAssign, Div};
4use std::str::FromStr;
5
6use ntex_bytes::{ByteString, Bytes, BytesMut};
7use simdutf8::compat::{Utf8Error, from_utf8};
8
9use crate::authority::{self, Authority, Port, UserInfo};
10use crate::error::{ErrorKind, InvalidUrl};
11use crate::path::{Path, PathAndQuery};
12use crate::query::{self, Fragment, Query, split_pair};
13use crate::quoting::{Component, quote, requote, unquote};
14use crate::{chars::lowercase, host::normalize_host, parse, scheme::Scheme};
15
16/// Normalized URL reference.
17///
18/// The URL is stored in a single shared [`ByteString`]; accessors return
19/// borrowed, percent-encoded components. Parsing is lenient and normalizes the
20/// input the same way Python's `yarl` does:
21///
22/// * leading and trailing whitespace and C0 controls, tabs and newlines are removed
23/// * scheme and host are lowercased, non-ASCII hosts are punycode-encoded
24/// * percent-encoding is normalized: invalid characters are encoded, escapes of
25///   unreserved characters are decoded, hex digits are uppercased
26/// * dot segments are removed from absolute paths
27/// * an empty path of `http`, `https`, `ws` and `wss` URLs becomes `/`
28///
29/// The normalized URL always passes strict [`Url::validate`].
30///
31/// ```
32/// use urly::Url;
33///
34/// let url: Url = "HTTPS://[email protected]:8443/a/./b/../c d?q=1#frag".parse().unwrap();
35/// assert_eq!(url, "https://[email protected]:8443/a/c%20d?q=1#frag");
36/// assert_eq!(url.scheme_str(), Some("https"));
37/// assert_eq!(url.host(), Some("example.com"));
38/// assert_eq!(url.port_u16(), Some(8443));
39/// assert_eq!(url.path(), "/a/c%20d");
40/// assert_eq!(url.path().decode(), "/a/c d");
41/// assert_eq!(url.query().unwrap().get("q").unwrap(), "1");
42/// assert_eq!(url.fragment().unwrap(), "frag");
43/// ```
44///
45/// URLs are limited to 65535 bytes; operations that can't return an error panic if
46/// the result exceeds the limit.
47#[derive(Clone)]
48pub struct Url {
49    data: ByteString,
50    // index of ':', 0 if there is no scheme
51    scheme_end: u16,
52    // index after "//", 0 if there is no authority
53    auth_start: u16,
54    path_start: u16,
55    path_end: u16,
56    query_end: u16,
57    // host range within the authority, both 0 if there is no authority
58    host_start: u16,
59    host_end: u16,
60}
61
62/// Borrowed URL components, used to assemble a new URL.
63#[derive(Copy, Clone, Default)]
64pub(crate) struct Components<'a> {
65    pub(crate) scheme: Option<&'a str>,
66    pub(crate) authority: Option<&'a str>,
67    pub(crate) path: &'a str,
68    pub(crate) query: Option<&'a str>,
69    pub(crate) fragment: Option<&'a str>,
70}
71
72/// Assembles normalized components into a URL.
73///
74/// The path is adjusted so the result can be parsed back unambiguously. `orig`
75/// is reused if it is equal to the result.
76pub(crate) fn assemble(c: &Components<'_>, orig: Option<&ByteString>) -> Result<Url, InvalidUrl> {
77    let prefix = if c.authority.is_some() {
78        if c.path.is_empty() {
79            let special = c
80                .scheme
81                .is_some_and(|s| Scheme::from_str_unchecked(s).is_special());
82            if special { "/" } else { "" }
83        } else if c.path.starts_with('/') {
84            ""
85        } else {
86            "/"
87        }
88    } else if c.path.starts_with("//") {
89        "/."
90    } else if c.scheme.is_none() && c.path.split('/').next().is_some_and(|s| s.contains(':')) {
91        "./"
92    } else {
93        ""
94    };
95
96    let pieces = [
97        c.scheme.unwrap_or(""),
98        if c.scheme.is_some() { ":" } else { "" },
99        if c.authority.is_some() { "//" } else { "" },
100        c.authority.unwrap_or(""),
101        prefix,
102        c.path,
103        if c.query.is_some() { "?" } else { "" },
104        c.query.unwrap_or(""),
105        if c.fragment.is_some() { "#" } else { "" },
106        c.fragment.unwrap_or(""),
107    ];
108    let len: usize = pieces.iter().map(|p| p.len()).sum();
109    if len > parse::MAX_LEN {
110        return Err(InvalidUrl::new(ErrorKind::TooLong));
111    }
112
113    let reuse = orig.filter(|orig| {
114        orig.len() == len && {
115            // the lengths add up to `len`, indexing can't panic. most pieces are
116            // empty, and comparing empty slices is surprisingly slow
117            let orig = orig.as_str().as_bytes();
118            let mut pos = 0;
119            pieces.iter().all(|p| {
120                let start = pos;
121                pos += p.len();
122                p.is_empty() || orig[start..pos] == *p.as_bytes()
123            })
124        }
125    });
126    let data = match reuse {
127        Some(orig) => orig.clone(),
128        None => concat(&pieces, len),
129    };
130
131    // all lengths fit, the total is at most `MAX_LEN`
132    let lens = pieces.map(|p| p.len() as u16);
133    let scheme_end = lens[0];
134    let auth_start = if c.authority.is_some() {
135        lens[0] + lens[1] + lens[2]
136    } else {
137        0
138    };
139    // the `/.` prefix of an authority-less `//` path is not part of the path
140    let hidden = if prefix == "/." { lens[4] } else { 0 };
141    let path_start = lens[..4].iter().sum::<u16>() + hidden;
142    let path_end = lens[..6].iter().sum::<u16>();
143    let query_end = path_end + lens[6] + lens[7];
144    let (host_start, host_end) = match c.authority {
145        Some(a) => {
146            let host = authority::split(a).1;
147            let start = auth_start + authority::offset(a, host) as u16;
148            (start, start + host.len() as u16)
149        }
150        None => (0, 0),
151    };
152    Ok(Url {
153        data,
154        scheme_end,
155        auth_start,
156        path_start,
157        path_end,
158        query_end,
159        host_start,
160        host_end,
161    })
162}
163
164/// Concatenates `pieces` of total length `len` with a single copy.
165fn concat(pieces: &[&str], len: usize) -> ByteString {
166    // short urls are stored inline in `Bytes`, without allocation
167    const INLINE: usize = 23;
168
169    let bytes = if len <= INLINE {
170        let mut buf = [0u8; INLINE];
171        let mut pos = 0;
172        for p in pieces {
173            buf[pos..pos + p.len()].copy_from_slice(p.as_bytes());
174            pos += p.len();
175        }
176        Bytes::copy_from_slice(&buf[..len])
177    } else {
178        let mut buf = BytesMut::with_capacity(len);
179        for p in pieces {
180            buf.extend_from_slice(p.as_bytes());
181        }
182        buf.freeze()
183    };
184    // SAFETY: a concatenation of strings is valid UTF-8
185    unsafe { ByteString::from_bytes_unchecked(bytes) }
186}
187
188fn too_long<T>(res: Result<T, InvalidUrl>) -> T {
189    match res {
190        Ok(v) => v,
191        Err(e) => panic!("{e}"),
192    }
193}
194
195impl Url {
196    /// Returns the relative URL `/`.
197    pub const fn new() -> Url {
198        Url {
199            data: ByteString::from_static("/"),
200            scheme_end: 0,
201            auth_start: 0,
202            path_start: 0,
203            path_end: 1,
204            query_end: 1,
205            host_start: 0,
206            host_end: 0,
207        }
208    }
209
210    /// Parses and normalizes an authority-form `[userinfo@]host[:port]`, like
211    /// `http::Uri` does for a string without a scheme or a leading `/`.
212    ///
213    /// The result is a network-path reference without a path. Use
214    /// [`Url::parse_ref`] to parse any URL reference, it treats such a string
215    /// as a relative path.
216    ///
217    /// ```
218    /// use urly::Url;
219    ///
220    /// let url = Url::parse("Custom.Domain:8080").unwrap();
221    /// assert_eq!(url, "//custom.domain:8080");
222    /// assert_eq!(url.host(), Some("custom.domain"));
223    /// assert_eq!(url.port_u16(), Some(8080));
224    /// assert_eq!(url.path(), "");
225    ///
226    /// assert!(Url::parse("custom.domain/path").is_err());
227    /// assert_eq!(Url::parse_ref("custom.domain").unwrap().host(), None);
228    /// ```
229    pub fn parse<T: AsRef<str>>(src: T) -> Result<Url, InvalidUrl> {
230        parse::parse_authority(src.as_ref())
231    }
232
233    /// Parses and normalizes a URL reference.
234    ///
235    /// Same as `Url::try_from()` and `str::parse()`.
236    pub fn parse_ref<T: AsRef<str>>(src: T) -> Result<Url, InvalidUrl> {
237        parse::parse(src.as_ref(), None)
238    }
239
240    /// Strictly validates a URI reference according to RFC 3986.
241    ///
242    /// Unlike parsing, validation does not normalize: whitespace, non-ASCII
243    /// characters and invalid escapes are errors. The error position is a byte
244    /// offset into `src`. Input that is not valid UTF-8 is rejected with
245    /// [`ErrorKind::InvalidChar`](crate::ErrorKind::InvalidChar) at its first
246    /// non-ASCII byte.
247    ///
248    /// ```
249    /// use urly::{ErrorKind, Url};
250    ///
251    /// assert!(Url::validate("https://example.com/a%20b?q#f").is_ok());
252    /// assert!(Url::validate(b"/path?q").is_ok());
253    ///
254    /// let err = Url::validate("http://ex ample.com").unwrap_err();
255    /// assert_eq!(err.kind(), ErrorKind::InvalidChar(' '));
256    /// assert_eq!(err.position(), Some(9));
257    ///
258    /// let err = Url::validate(b"/a\xff").unwrap_err();
259    /// assert_eq!(err.kind(), ErrorKind::InvalidChar(char::REPLACEMENT_CHARACTER));
260    /// assert_eq!(err.position(), Some(2));
261    /// ```
262    pub fn validate<T: AsRef<[u8]>>(src: T) -> Result<(), InvalidUrl> {
263        let src = src.as_ref();
264        if let Ok(s) = from_utf8(src) {
265            return parse::validate(s);
266        }
267        let i = src.iter().position(|b| !b.is_ascii()).unwrap_or_default();
268        let c = src[i..]
269            .utf8_chunks()
270            .next()
271            .and_then(|chunk| chunk.valid().chars().next())
272            .unwrap_or(char::REPLACEMENT_CHARACTER);
273        Err(InvalidUrl::at(ErrorKind::InvalidChar(c), i))
274    }
275
276    /// Converts a static string to a URL. The string is not copied if it is
277    /// already normalized.
278    ///
279    /// # Panics
280    ///
281    /// Panics if the URL is not valid.
282    pub fn from_static(src: &'static str) -> Url {
283        match parse::parse(src, Some(&ByteString::from_static(src))) {
284            Ok(url) => url,
285            Err(e) => panic!("invalid static url {src:?}: {e}"),
286        }
287    }
288
289    /// Converts a `Bytes`, `String`, `Vec<u8>` or any other
290    /// byte buffer to a URL, reusing the buffer if possible.
291    pub fn from_maybe_shared<T>(src: T) -> Result<Url, InvalidUrl>
292    where
293        T: AsRef<[u8]> + 'static,
294    {
295        let mut src = Some(src);
296        let any = &mut src as &mut dyn Any;
297        if let Some(src) = any.downcast_mut::<Option<Bytes>>() {
298            return Url::try_from(src.take().unwrap());
299        }
300        if let Some(src) = any.downcast_mut::<Option<String>>() {
301            return Url::try_from(src.take().unwrap());
302        }
303        if let Some(src) = any.downcast_mut::<Option<Vec<u8>>>() {
304            return Url::try_from(Bytes::from(src.take().unwrap()));
305        }
306        Url::try_from(src.unwrap().as_ref())
307    }
308
309    /// Returns a new [`Builder`](crate::Builder).
310    pub fn builder() -> crate::Builder {
311        crate::Builder::new()
312    }
313
314    pub(crate) fn empty() -> Url {
315        Url {
316            data: ByteString::new(),
317            scheme_end: 0,
318            auth_start: 0,
319            path_start: 0,
320            path_end: 0,
321            query_end: 0,
322            host_start: 0,
323            host_end: 0,
324        }
325    }
326
327    fn components(&self) -> Components<'_> {
328        Components {
329            scheme: self.scheme_str(),
330            authority: self.authority().map(Authority::as_str),
331            path: self.path().as_str(),
332            query: self.query().map(Query::as_str),
333            fragment: self.fragment().map(Fragment::as_str),
334        }
335    }
336
337    fn range(&self, start: u16, end: u16) -> &str {
338        &self.data[start as usize..end as usize]
339    }
340
341    // ===== accessors =====
342
343    /// Returns the URL as a string.
344    pub fn as_str(&self) -> &str {
345        &self.data
346    }
347
348    /// Returns the URL as bytes.
349    pub fn as_bytes(&self) -> &[u8] {
350        self.data.as_bytes()
351    }
352
353    /// Returns the underlying buffer.
354    pub fn as_byte_string(&self) -> &ByteString {
355        &self.data
356    }
357
358    /// Returns the scheme, if present.
359    pub fn scheme(&self) -> Option<&Scheme> {
360        self.scheme_str().map(Scheme::from_str_unchecked)
361    }
362
363    /// Returns the scheme as a string, if present.
364    pub fn scheme_str(&self) -> Option<&str> {
365        (self.scheme_end > 0).then(|| self.range(0, self.scheme_end))
366    }
367
368    /// Returns the authority, if present.
369    pub fn authority(&self) -> Option<&Authority> {
370        (self.auth_start > 0)
371            .then(|| Authority::from_str_unchecked(self.range(self.auth_start, self.path_start)))
372    }
373
374    /// Returns the userinfo, if present.
375    pub fn userinfo(&self) -> Option<&UserInfo> {
376        (self.host_start > self.auth_start)
377            .then(|| UserInfo::from_str_unchecked(self.range(self.auth_start, self.host_start - 1)))
378    }
379
380    /// Returns the decoded user name, if present.
381    pub fn username(&self) -> Option<Cow<'_, str>> {
382        self.userinfo().map(UserInfo::decoded_username)
383    }
384
385    /// Returns the decoded password, if present.
386    pub fn password(&self) -> Option<Cow<'_, str>> {
387        self.userinfo()?.decoded_password()
388    }
389
390    /// Returns the host, if present and not empty. IPv6 addresses include the
391    /// brackets, non-ASCII domains are punycode-encoded.
392    pub fn host(&self) -> Option<&str> {
393        (self.host_end > self.host_start).then(|| self.range(self.host_start, self.host_end))
394    }
395
396    /// Returns the parsed host, if present and not empty.
397    ///
398    /// ```
399    /// use std::net::Ipv4Addr;
400    /// use urly::{Host, Url};
401    ///
402    /// let url = Url::from_static("http://127.0.0.1/");
403    /// assert_eq!(url.host_parsed(), Some(Host::Ipv4(Ipv4Addr::LOCALHOST)));
404    ///
405    /// let url = Url::from_static("http://münchen.de/");
406    /// assert_eq!(url.host(), Some("xn--mnchen-3ya.de"));
407    /// assert_eq!(url.host_parsed().unwrap().to_unicode(), "münchen.de");
408    /// ```
409    pub fn host_parsed(&self) -> Option<crate::Host<'_>> {
410        self.host().map(crate::Host::classify)
411    }
412
413    /// Returns the explicit port, if present.
414    pub fn port(&self) -> Option<Port<&str>> {
415        if self.auth_start > 0 && self.host_end < self.path_start {
416            Port::parse(self.range(self.host_end + 1, self.path_start))
417        } else {
418            None
419        }
420    }
421
422    /// Returns the explicit port as a number, if present.
423    pub fn port_u16(&self) -> Option<u16> {
424        self.port().map(|p| p.as_u16())
425    }
426
427    /// Returns the explicit port, or the default port of the scheme.
428    pub fn port_or_known_default(&self) -> Option<u16> {
429        self.port_u16().or_else(|| self.scheme()?.default_port())
430    }
431
432    /// Returns the percent-encoded path.
433    ///
434    /// A path starting with `//` in a URL without authority is serialized with a
435    /// `/.` prefix, which is not part of the path.
436    ///
437    /// ```
438    /// use urly::Url;
439    ///
440    /// let url = Url::from_static("/.//a?q");
441    /// assert_eq!(url.path(), "//a");
442    /// assert_eq!(url.path_and_query(), "//a?q");
443    /// ```
444    pub fn path(&self) -> &Path {
445        Path::from_str_unchecked(self.range(self.path_start, self.path_end))
446    }
447
448    /// Returns the percent-encoded query, if present.
449    pub fn query(&self) -> Option<&Query> {
450        (self.query_end > self.path_end)
451            .then(|| Query::from_str_unchecked(self.range(self.path_end + 1, self.query_end)))
452    }
453
454    /// Returns the path and query.
455    pub fn path_and_query(&self) -> &PathAndQuery {
456        PathAndQuery::from_str_unchecked(self.range(self.path_start, self.query_end))
457    }
458
459    /// Returns the percent-encoded fragment, if present.
460    pub fn fragment(&self) -> Option<&Fragment> {
461        ((self.query_end as usize) < self.data.len())
462            .then(|| Fragment::from_str_unchecked(&self.data[self.query_end as usize + 1..]))
463    }
464
465    /// Returns `true` if the URL has a scheme.
466    pub fn is_absolute(&self) -> bool {
467        self.scheme_end > 0
468    }
469
470    /// Returns `true` if the port is absent or is the default port of the scheme.
471    pub fn is_default_port(&self) -> bool {
472        match self.port_u16() {
473            None => true,
474            Some(port) => self.scheme().and_then(Scheme::default_port) == Some(port),
475        }
476    }
477
478    // ===== derived urls =====
479
480    /// Returns the origin: scheme, host and port.
481    ///
482    /// ```
483    /// use urly::Url;
484    ///
485    /// let url = Url::from_static("https://user:[email protected]:8443/a?b#c");
486    /// assert_eq!(url.origin().unwrap(), "https://example.com:8443/");
487    /// assert!(Url::from_static("/a").origin().is_none());
488    /// ```
489    pub fn origin(&self) -> Option<Url> {
490        let scheme = self.scheme_str()?;
491        let authority = self.authority()?;
492        if authority.host().is_empty() {
493            return None;
494        }
495        Some(self.derive(|c| {
496            *c = Components {
497                scheme: Some(scheme),
498                authority: Some(authority.host_port()),
499                ..Components::default()
500            };
501        }))
502    }
503
504    /// Returns the relative part of the URL: path, query and fragment.
505    ///
506    /// ```
507    /// use urly::Url;
508    ///
509    /// let url = Url::from_static("https://example.com/a?b#c");
510    /// assert_eq!(url.relative(), "/a?b#c");
511    /// ```
512    pub fn relative(&self) -> Url {
513        self.derive(|c| {
514            c.scheme = None;
515            c.authority = None;
516        })
517    }
518
519    /// Returns the URL with the last path segment removed, without query and
520    /// fragment.
521    ///
522    /// ```
523    /// use urly::Url;
524    ///
525    /// assert_eq!(Url::from_static("http://h/a/b?q").parent(), "http://h/a");
526    /// assert_eq!(Url::from_static("http://h/a/b/").parent(), "http://h/a");
527    /// assert_eq!(Url::from_static("http://h/a").parent(), "http://h/");
528    /// assert_eq!(Url::from_static("http://h/").parent(), "http://h/");
529    /// ```
530    pub fn parent(&self) -> Url {
531        let path = self.path().as_str();
532        let trimmed = path.trim_end_matches('/');
533        let parent = match trimmed.rfind('/') {
534            Some(0) => "/",
535            Some(i) => &trimmed[..i],
536            None if path.starts_with('/') => "/",
537            None => "",
538        };
539        self.with_path(parent)
540    }
541
542    /// Appends literal path segments, without query and fragment.
543    ///
544    /// Segments are percent-encoded; `/` is kept and a leading `/` is ignored.
545    /// The same operation is available as the `/` operator.
546    ///
547    /// ```
548    /// use urly::Url;
549    ///
550    /// let url = Url::from_static("http://h/api?q");
551    /// assert_eq!(url.push_segments(["v1", "a b"]), "http://h/api/v1/a%20b");
552    /// assert_eq!(&url / "users/" / "1", "http://h/api/users/1");
553    /// ```
554    pub fn push_segments<I, S>(&self, segments: I) -> Url
555    where
556        I: IntoIterator<Item = S>,
557        S: AsRef<str>,
558    {
559        let mut path = self.path().as_str().to_string();
560        for segment in segments {
561            let segment = segment.as_ref();
562            let segment = segment.strip_prefix('/').unwrap_or(segment);
563            if !path.is_empty() && !path.ends_with('/') {
564                path.push('/');
565            }
566            path.push_str(&quote(segment, Component::Path));
567        }
568        if path.starts_with('/') && parse::has_dot_segments(&path) {
569            path = parse::remove_dot_segments(&path);
570        }
571        self.with_path(&path)
572    }
573
574    /// Resolves a reference against this URL, RFC 3986 section 5.2.
575    ///
576    /// ```
577    /// use urly::Url;
578    ///
579    /// let base = Url::from_static("http://a/b/c/d;p?q");
580    /// assert_eq!(base.join("../g").unwrap(), "http://a/b/g");
581    /// assert_eq!(base.join("//g/x").unwrap(), "http://g/x");
582    /// assert_eq!(base.join("?y").unwrap(), "http://a/b/c/d;p?y");
583    /// ```
584    pub fn join(&self, reference: &str) -> Result<Url, InvalidUrl> {
585        if reference.trim_matches(|c: char| c <= ' ').is_empty() {
586            return self.rebuild(|c| c.fragment = None);
587        }
588        Ok(self.join_url(&Url::parse_ref(reference)?))
589    }
590
591    /// Resolves a parsed reference against this URL, RFC 3986 section 5.2.2.
592    ///
593    /// The `+` and `+=` operators are equivalent.
594    ///
595    /// ```
596    /// use urly::Url;
597    ///
598    /// let base = Url::from_static("http://a/b/c/d;p?q");
599    /// let r = Url::from_static("../g");
600    /// assert_eq!(base.join_url(&r), "http://a/b/g");
601    /// assert_eq!(&base + &r, "http://a/b/g");
602    ///
603    /// let mut url = base.clone();
604    /// url += r;
605    /// assert_eq!(url, "http://a/b/g");
606    /// ```
607    pub fn join_url(&self, r: &Url) -> Url {
608        let base = self.components();
609        let r = r.components();
610        let resolved = |path| Cow::Owned(parse::remove_dot_segments(path));
611        let (scheme, authority, path, query) = if r.scheme.is_some() {
612            (r.scheme, r.authority, resolved(r.path), r.query)
613        } else if r.authority.is_some() {
614            (base.scheme, r.authority, resolved(r.path), r.query)
615        } else if r.path.is_empty() {
616            let query = r.query.or(base.query);
617            (base.scheme, base.authority, Cow::Borrowed(base.path), query)
618        } else if r.path.starts_with('/') {
619            (base.scheme, base.authority, resolved(r.path), r.query)
620        } else {
621            let dir = if base.authority.is_some() && base.path.is_empty() {
622                "/"
623            } else {
624                dir(base.path)
625            };
626            let path = resolved(&format!("{dir}{}", r.path));
627            (base.scheme, base.authority, path, r.query)
628        };
629        self.derive(|c| {
630            *c = Components {
631                scheme,
632                authority,
633                path: &path,
634                query,
635                fragment: r.fragment,
636            };
637        })
638    }
639
640    // ===== setters =====
641
642    /// Assembles a URL from modified components, reusing the buffer if
643    /// nothing changed.
644    fn rebuild<'a>(&'a self, f: impl FnOnce(&mut Components<'a>)) -> Result<Url, InvalidUrl> {
645        let mut c = self.components();
646        f(&mut c);
647        assemble(&c, Some(&self.data))
648    }
649
650    fn derive<'a>(&'a self, f: impl FnOnce(&mut Components<'a>)) -> Url {
651        too_long(self.rebuild(f))
652    }
653
654    /// Returns the URL with a new path, without query and fragment.
655    fn with_path(&self, path: &str) -> Url {
656        self.derive(|c| {
657            c.path = path;
658            c.query = None;
659            c.fragment = None;
660        })
661    }
662
663    /// Sets the scheme.
664    pub fn set_scheme(&mut self, scheme: &str) -> Result<(), InvalidUrl> {
665        let scheme = lowercase(Scheme::new(scheme)?.as_str().into());
666        self.rebuild(|c| c.scheme = Some(&scheme))
667            .map(|url| *self = url)
668    }
669
670    /// Sets or removes the raw authority: `[userinfo "@"] host [":" port]`.
671    pub fn set_authority(&mut self, authority: Option<&str>) -> Result<(), InvalidUrl> {
672        let authority = authority.map(parse::normalize_authority).transpose()?;
673        self.rebuild(|c| c.authority = authority.as_deref())
674            .map(|url| *self = url)
675    }
676
677    fn with_authority(
678        &self,
679        userinfo: Option<&str>,
680        host: &str,
681        port: Option<u16>,
682    ) -> Result<Url, InvalidUrl> {
683        let authority = parse::join_authority(userinfo, host, port);
684        self.rebuild(|c| c.authority = Some(&authority))
685    }
686
687    fn host_parts(&self) -> Result<(Option<&str>, &str, Option<u16>), InvalidUrl> {
688        let authority = self
689            .authority()
690            .ok_or(InvalidUrl::new(ErrorKind::AuthorityMissing))?;
691        let (userinfo, host, _) = authority::split(authority.as_str());
692        if host.is_empty() {
693            return Err(InvalidUrl::new(ErrorKind::InvalidHost));
694        }
695        Ok((userinfo, host, authority.port_u16()))
696    }
697
698    /// Sets or removes the literal user name and password.
699    ///
700    /// ```
701    /// use urly::Url;
702    ///
703    /// let mut url = Url::from_static("http://example.com/");
704    /// url.set_userinfo(Some("us@r"), Some("p:w")).unwrap();
705    /// assert_eq!(url, "http://us%40r:p%[email protected]/");
706    /// assert_eq!(url.password().unwrap(), "p:w");
707    /// ```
708    pub fn set_userinfo(
709        &mut self,
710        username: Option<&str>,
711        password: Option<&str>,
712    ) -> Result<(), InvalidUrl> {
713        let (_, host, port) = self.host_parts()?;
714        let userinfo = match (username, password) {
715            (None, None) => None,
716            (user, None) => Some(quote(user.unwrap_or(""), Component::UserInfo)),
717            (user, Some(password)) => Some(Cow::Owned(format!(
718                "{}:{}",
719                quote(user.unwrap_or(""), Component::UserInfo),
720                quote(password, Component::UserInfo)
721            ))),
722        };
723        self.with_authority(userinfo.as_deref(), host, port)
724            .map(|url| *self = url)
725    }
726
727    /// Sets the host. Escapes are decoded, non-ASCII domains are
728    /// punycode-encoded, IPv6 addresses may be given without brackets.
729    ///
730    /// An authority is added if the URL doesn't have one.
731    ///
732    /// ```
733    /// use urly::Url;
734    ///
735    /// let mut url = Url::from_static("http://[email protected]:8080/a");
736    /// url.set_host("::1").unwrap();
737    /// assert_eq!(url, "http://user@[::1]:8080/a");
738    /// url.set_host("Bücher.example").unwrap();
739    /// assert_eq!(url, "http://[email protected]:8080/a");
740    /// ```
741    pub fn set_host(&mut self, host: &str) -> Result<(), InvalidUrl> {
742        let bracketed;
743        let host = if host.contains(':') && !host.starts_with('[') {
744            bracketed = format!("[{host}]");
745            &bracketed
746        } else {
747            host
748        };
749        let host = normalize_host(host)?;
750        if host.is_empty() {
751            return Err(InvalidUrl::new(ErrorKind::InvalidHost));
752        }
753        let (userinfo, port) = match self.authority() {
754            Some(a) => (a.userinfo().map(UserInfo::as_str), a.port_u16()),
755            None => (None, None),
756        };
757        self.with_authority(userinfo, &host, port)
758            .map(|url| *self = url)
759    }
760
761    /// Sets or removes the port.
762    pub fn set_port(&mut self, port: Option<u16>) -> Result<(), InvalidUrl> {
763        let (userinfo, host, _) = self.host_parts()?;
764        self.with_authority(userinfo, host, port)
765            .map(|url| *self = url)
766    }
767
768    /// Sets the percent-encoded path, keeping query and fragment.
769    ///
770    /// Invalid characters are encoded and dot segments are removed.
771    ///
772    /// ```
773    /// use urly::Url;
774    ///
775    /// let mut url = Url::from_static("http://h/a?q");
776    /// url.set_path("/b/../c d");
777    /// assert_eq!(url, "http://h/c%20d?q");
778    /// ```
779    pub fn set_path(&mut self, path: &str) {
780        let prefixed;
781        let path = if self.auth_start != 0 && !path.is_empty() && !path.starts_with('/') {
782            prefixed = format!("/{path}");
783            &prefixed
784        } else {
785            path
786        };
787        let path = parse::normalize_path(path);
788        *self = self.derive(|c| c.path = &path);
789    }
790
791    /// Replaces the last path segment with a literal file name.
792    ///
793    /// ```
794    /// use urly::Url;
795    ///
796    /// let mut url = Url::from_static("http://h/a/b.txt?q");
797    /// url.set_file_name("c d.html").unwrap();
798    /// assert_eq!(url, "http://h/a/c%20d.html?q");
799    /// ```
800    pub fn set_file_name(&mut self, name: &str) -> Result<(), InvalidUrl> {
801        if name.contains('/') || name == "." || name == ".." {
802            return Err(InvalidUrl::new(ErrorKind::InvalidPath));
803        }
804        self.with_file_name(&quote(name, Component::Path))
805            .map(|url| *self = url)
806    }
807
808    /// Replaces the last path segment with an encoded file name.
809    fn with_file_name(&self, name: &str) -> Result<Url, InvalidUrl> {
810        let path = format!("{}{name}", dir(self.path().as_str()));
811        self.rebuild(|c| c.path = &path)
812    }
813
814    /// Replaces the extension of the file name; an empty extension removes it.
815    ///
816    /// ```
817    /// use urly::Url;
818    ///
819    /// let mut url = Url::from_static("http://h/a/b.txt");
820    /// url.set_extension("tar.gz").unwrap();
821    /// assert_eq!(url, "http://h/a/b.tar.gz");
822    /// url.set_extension("").unwrap();
823    /// assert_eq!(url, "http://h/a/b.tar");
824    /// ```
825    pub fn set_extension(&mut self, extension: &str) -> Result<(), InvalidUrl> {
826        let stem = self
827            .path()
828            .file_stem()
829            .ok_or(InvalidUrl::new(ErrorKind::InvalidPath))?;
830        if extension.contains('/') {
831            return Err(InvalidUrl::new(ErrorKind::InvalidPath));
832        }
833        let name = if extension.is_empty() {
834            Cow::Borrowed(stem)
835        } else {
836            let extension = quote(extension, Component::Path);
837            Cow::Owned(format!("{stem}.{extension}"))
838        };
839        self.with_file_name(&name).map(|url| *self = url)
840    }
841
842    /// Sets or removes the percent-encoded query.
843    ///
844    /// ```
845    /// use urly::Url;
846    ///
847    /// let mut url = Url::from_static("http://h/#f");
848    /// url.set_query(Some("a=b c&d"));
849    /// assert_eq!(url, "http://h/?a=b+c&d#f");
850    /// ```
851    pub fn set_query(&mut self, query: Option<&str>) {
852        let query = query.map(|q| requote(q, Component::Query));
853        *self = self.derive(|c| c.query = query.as_deref());
854    }
855
856    /// Keeps existing pairs whose decoded key passes `keep` and appends literal
857    /// `pairs`. An empty result removes the query.
858    fn edit_query_pairs<I, K, V>(&mut self, keep: impl Fn(&str) -> bool, pairs: I)
859    where
860        I: IntoIterator<Item = (K, V)>,
861        K: AsRef<str>,
862        V: AsRef<str>,
863    {
864        let existing = self.query().map_or("", Query::as_str);
865        let pieces: Vec<Cow<'_, str>> = query::pieces(existing)
866            .filter(|piece| keep(&unquote(split_pair(piece).0, Component::QueryPart)))
867            .map(Cow::Borrowed)
868            .chain(pairs.into_iter().map(|(k, v)| {
869                let (k, v) = (k.as_ref(), v.as_ref());
870                let (k, v) = (
871                    quote(k, Component::QueryPart),
872                    quote(v, Component::QueryPart),
873                );
874                Cow::Owned(format!("{k}={v}"))
875            }))
876            .collect();
877        let query = pieces.join("&");
878        *self = self.derive(|c| c.query = (!query.is_empty()).then_some(query.as_str()));
879    }
880
881    /// Replaces the query with literal key-value pairs. Empty pairs remove the query.
882    ///
883    /// ```
884    /// use urly::Url;
885    ///
886    /// let mut url = Url::from_static("http://h/?old");
887    /// url.set_query_pairs([("a", "1 2"), ("b&", "=")]);
888    /// assert_eq!(url, "http://h/?a=1+2&b%26=%3D");
889    /// ```
890    pub fn set_query_pairs<I, K, V>(&mut self, pairs: I)
891    where
892        I: IntoIterator<Item = (K, V)>,
893        K: AsRef<str>,
894        V: AsRef<str>,
895    {
896        self.edit_query_pairs(|_| false, pairs);
897    }
898
899    /// Appends literal key-value pairs to the query.
900    pub fn extend_query_pairs<I, K, V>(&mut self, pairs: I)
901    where
902        I: IntoIterator<Item = (K, V)>,
903        K: AsRef<str>,
904        V: AsRef<str>,
905    {
906        self.edit_query_pairs(|_| true, pairs);
907    }
908
909    /// Replaces all values of the given keys, appending the new pairs.
910    ///
911    /// ```
912    /// use urly::Url;
913    ///
914    /// let mut url = Url::from_static("http://h/?a=1&b=2&a=3");
915    /// url.update_query_pairs([("a", "4")]);
916    /// assert_eq!(url, "http://h/?b=2&a=4");
917    /// ```
918    pub fn update_query_pairs<I, K, V>(&mut self, pairs: I)
919    where
920        I: IntoIterator<Item = (K, V)>,
921        K: AsRef<str>,
922        V: AsRef<str>,
923    {
924        let pairs: Vec<(K, V)> = pairs.into_iter().collect();
925        let keep = |key: &str| !pairs.iter().any(|(k, _)| k.as_ref() == key);
926        self.edit_query_pairs(keep, pairs.iter().map(|(k, v)| (k.as_ref(), v.as_ref())));
927    }
928
929    /// Removes all pairs with the given keys from the query.
930    ///
931    /// ```
932    /// use urly::Url;
933    ///
934    /// let mut url = Url::from_static("http://h/?a=1&b=2&a=3");
935    /// url.remove_query_params(["a"]);
936    /// assert_eq!(url, "http://h/?b=2");
937    /// url.remove_query_params(["b"]);
938    /// assert_eq!(url, "http://h/");
939    /// ```
940    pub fn remove_query_params<I, K>(&mut self, keys: I)
941    where
942        I: IntoIterator<Item = K>,
943        K: AsRef<str>,
944    {
945        let keys: Vec<K> = keys.into_iter().collect();
946        let keep = |key: &str| !keys.iter().any(|k| k.as_ref() == key);
947        self.edit_query_pairs(keep, std::iter::empty::<(&str, &str)>());
948    }
949
950    /// Sets or removes the percent-encoded fragment.
951    pub fn set_fragment(&mut self, fragment: Option<&str>) {
952        let fragment = fragment.map(|f| requote(f, Component::Fragment));
953        *self = self.derive(|c| c.fragment = fragment.as_deref());
954    }
955
956    /// Splits the URL into its parts.
957    pub fn into_parts(self) -> crate::Parts {
958        let slice = |start: u16, end: usize| self.data.slice(start as usize..end);
959        crate::Parts {
960            scheme: self.scheme().map(|_| slice(0, self.scheme_end as usize)),
961            authority: self
962                .authority()
963                .map(|_| slice(self.auth_start, self.path_start as usize)),
964            path_and_query: slice(self.path_start, self.query_end as usize),
965            fragment: self
966                .fragment()
967                .map(|_| slice(self.query_end + 1, self.data.len())),
968        }
969    }
970
971    /// Converts the URL into its buffer.
972    pub fn into_byte_string(self) -> ByteString {
973        self.data
974    }
975}
976
977/// Returns the path up to and including the last `/`.
978fn dir(path: &str) -> &str {
979    path.rfind('/').map_or("", |i| &path[..=i])
980}
981
982// ===== trait impls =====
983
984str_fmt!(Url);
985str_eq!(Url);
986
987/// Returns the relative URL `/`, like `http::Uri::default()`.
988impl Default for Url {
989    fn default() -> Url {
990        Url::new()
991    }
992}
993
994impl FromStr for Url {
995    type Err = InvalidUrl;
996
997    fn from_str(s: &str) -> Result<Url, InvalidUrl> {
998        parse::parse(s, None)
999    }
1000}
1001
1002macro_rules! try_from {
1003    ($($ty:ty => |$s:ident| $conv:expr;)*) => {$(
1004        impl TryFrom<$ty> for Url {
1005            type Error = InvalidUrl;
1006
1007            fn try_from($s: $ty) -> Result<Url, InvalidUrl> {
1008                $conv
1009            }
1010        }
1011    )*};
1012}
1013
1014try_from! {
1015    &str => |s| parse::parse(s, None);
1016    &String => |s| parse::parse(s, None);
1017    // `ByteString::from(String)` copies, assembling copies only once
1018    String => |s| parse::parse(&s, None);
1019    ByteString => |s| parse::parse(&s, Some(&s));
1020    &ByteString => |s| parse::parse(s, Some(s));
1021    &[u8] => |s| parse::parse(from_utf8(s).map_err(utf8_error)?, None);
1022    Bytes => |s| {
1023        from_utf8(&s).map_err(utf8_error)?;
1024        // SAFETY: validated above
1025        Url::try_from(unsafe { ByteString::from_bytes_unchecked(s) })
1026    };
1027}
1028
1029#[allow(clippy::needless_pass_by_value)]
1030fn utf8_error(e: Utf8Error) -> InvalidUrl {
1031    InvalidUrl::at(ErrorKind::InvalidChar('\u{FFFD}'), e.valid_up_to())
1032}
1033
1034impl From<Url> for ByteString {
1035    fn from(url: Url) -> ByteString {
1036        url.data
1037    }
1038}
1039
1040impl From<Url> for String {
1041    fn from(url: Url) -> String {
1042        url.as_str().to_string()
1043    }
1044}
1045
1046impl<S: AsRef<str>> Div<S> for &Url {
1047    type Output = Url;
1048
1049    fn div(self, segment: S) -> Url {
1050        self.push_segments([segment])
1051    }
1052}
1053
1054impl<S: AsRef<str>> Div<S> for Url {
1055    type Output = Url;
1056
1057    fn div(self, segment: S) -> Url {
1058        self.push_segments([segment])
1059    }
1060}
1061
1062impl<U: Borrow<Url>> Add<U> for &Url {
1063    type Output = Url;
1064
1065    fn add(self, reference: U) -> Url {
1066        self.join_url(reference.borrow())
1067    }
1068}
1069
1070impl<U: Borrow<Url>> Add<U> for Url {
1071    type Output = Url;
1072
1073    fn add(self, reference: U) -> Url {
1074        self.join_url(reference.borrow())
1075    }
1076}
1077
1078impl<U: Borrow<Url>> AddAssign<U> for Url {
1079    fn add_assign(&mut self, reference: U) {
1080        *self = self.join_url(reference.borrow());
1081    }
1082}
1083
1084#[cfg(feature = "http")]
1085mod http_impls {
1086    use http::uri::{InvalidUri, Uri};
1087
1088    use super::Url;
1089    use crate::error::InvalidUrl;
1090
1091    impl TryFrom<&Uri> for Url {
1092        type Error = InvalidUrl;
1093
1094        fn try_from(uri: &Uri) -> Result<Url, InvalidUrl> {
1095            if uri.scheme().is_none()
1096                && let Some(authority) = uri.authority()
1097            {
1098                return Url::parse(authority.as_str());
1099            }
1100            let uri = uri.to_string();
1101            // an origin-form path starting with `//` is not an authority
1102            if uri.starts_with("//") {
1103                Url::try_from(format!("/.{uri}"))
1104            } else {
1105                Url::try_from(uri)
1106            }
1107        }
1108    }
1109
1110    impl TryFrom<Uri> for Url {
1111        type Error = InvalidUrl;
1112
1113        fn try_from(uri: Uri) -> Result<Url, InvalidUrl> {
1114            Url::try_from(&uri)
1115        }
1116    }
1117
1118    /// The fragment is removed, `Uri` doesn't support it. A network-path
1119    /// reference converts to authority-form, it fails if it has a path or
1120    /// query.
1121    impl TryFrom<&Url> for Uri {
1122        type Error = InvalidUri;
1123
1124        fn try_from(url: &Url) -> Result<Uri, InvalidUri> {
1125            // `Uri` parses a relative reference as origin-form, without the `/.`
1126            // prefix, and a reference without scheme and `//` as authority-form
1127            let start = match (url.scheme_end, url.auth_start) {
1128                (0, 0) => url.path_start,
1129                (0, auth_start) => auth_start,
1130                _ => 0,
1131            };
1132            Uri::try_from(url.range(start, url.query_end))
1133        }
1134    }
1135
1136    /// The fragment is removed, `Uri` doesn't support it.
1137    impl TryFrom<Url> for Uri {
1138        type Error = InvalidUri;
1139
1140        fn try_from(url: Url) -> Result<Uri, InvalidUri> {
1141            Uri::try_from(&url)
1142        }
1143    }
1144}
1145
1146#[cfg(feature = "serde")]
1147mod serde_impls {
1148    use std::fmt;
1149
1150    use serde::de::{self, Deserialize, Deserializer, Visitor};
1151    use serde::{Serialize, Serializer};
1152
1153    use super::Url;
1154
1155    impl Serialize for Url {
1156        fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1157            serializer.serialize_str(self.as_str())
1158        }
1159    }
1160
1161    struct UrlVisitor;
1162
1163    impl Visitor<'_> for UrlVisitor {
1164        type Value = Url;
1165
1166        fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1167            f.write_str("a url")
1168        }
1169
1170        fn visit_str<E: de::Error>(self, v: &str) -> Result<Url, E> {
1171            Url::try_from(v).map_err(E::custom)
1172        }
1173
1174        fn visit_string<E: de::Error>(self, v: String) -> Result<Url, E> {
1175            Url::try_from(v).map_err(E::custom)
1176        }
1177    }
1178
1179    impl<'de> Deserialize<'de> for Url {
1180        fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Url, D::Error> {
1181            deserializer.deserialize_str(UrlVisitor)
1182        }
1183    }
1184}
1185
1186#[cfg(test)]
1187mod tests {
1188    use super::*;
1189
1190    #[test]
1191    fn default() {
1192        const URL: Url = Url::new();
1193        assert_eq!(URL, Url::default());
1194        let (url, p) = (Url::default(), Url::from_static("/"));
1195        assert_eq!(url.scheme_end, p.scheme_end);
1196        assert_eq!(url.auth_start, p.auth_start);
1197        assert_eq!(url.path_start, p.path_start);
1198        assert_eq!(url.path_end, p.path_end);
1199        assert_eq!(url.query_end, p.query_end);
1200        assert_eq!((url.host_start, url.host_end), (p.host_start, p.host_end));
1201        assert_eq!(url, "/");
1202        assert_eq!(url.path(), "/");
1203        assert!(url.query().is_none());
1204        assert!(!url.is_absolute());
1205    }
1206
1207    #[test]
1208    fn cached_host() {
1209        let cases = [
1210            (
1211                "http://u:p@[::1]:8080/a",
1212                Some("u:p"),
1213                Some("[::1]"),
1214                Some(8080),
1215            ),
1216            ("http://@h/", Some(""), Some("h"), None),
1217            ("http://h:0/", None, Some("h"), Some(0)),
1218            ("file:///etc", None, None, None),
1219            ("//h", None, Some("h"), None),
1220            ("/a", None, None, None),
1221        ];
1222        for (src, userinfo, host, port) in cases {
1223            let url = Url::from_static(src);
1224            let auth = url.authority();
1225            assert_eq!(url.userinfo().map(UserInfo::as_str), userinfo, "{src}");
1226            assert_eq!(url.host(), host, "{src}");
1227            assert_eq!(url.port_u16(), port, "{src}");
1228            assert_eq!(
1229                auth.and_then(|a| a.userinfo()).map(UserInfo::as_str),
1230                userinfo
1231            );
1232            assert_eq!(auth.map(Authority::host).filter(|h| !h.is_empty()), host);
1233            assert_eq!(auth.and_then(Authority::port_u16), port);
1234            let mut copy = url.clone();
1235            copy.set_path("/x");
1236            assert_eq!((copy.host(), copy.port_u16()), (host, port), "{src}");
1237        }
1238    }
1239
1240    #[test]
1241    fn double_slash_path() {
1242        let url = Url::from_static("/.//a/../b?q#f");
1243        assert_eq!(url, "/.//b?q#f");
1244        assert_eq!(url.path(), "//b");
1245        assert_eq!(url.path_and_query(), "//b?q");
1246        assert!(url.authority().is_none());
1247        assert_eq!(Url::from_parts(url.clone().into_parts()).unwrap(), url);
1248        assert_eq!(url.join("c").unwrap().path(), "//c");
1249
1250        let mut url = Url::default();
1251        url.set_path("//p");
1252        assert_eq!(url, "/.//p");
1253        assert_eq!(url.path(), "//p");
1254
1255        let mut url = Url::from_static("http://h//p?q");
1256        assert_eq!(url.path(), "//p");
1257        url.set_authority(None).unwrap();
1258        assert_eq!(url, "http:/.//p?q");
1259        assert_eq!(url.path(), "//p");
1260        url.set_authority(Some("h")).unwrap();
1261        assert_eq!(url, "http://h//p?q");
1262
1263        let parts = crate::Parts {
1264            path_and_query: "//p?q".into(),
1265            ..crate::Parts::default()
1266        };
1267        assert_eq!(Url::from_parts(parts).unwrap().path(), "//p");
1268    }
1269}