Coverage Report

Created: 2026-08-14 07:12

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/rust-url/url/src/host.rs
Line
Count
Source
1
// Copyright 2013-2016 The rust-url developers.
2
//
3
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
6
// option. This file may not be copied, modified, or distributed
7
// except according to those terms.
8
9
use crate::net::{Ipv4Addr, Ipv6Addr};
10
use alloc::borrow::Cow;
11
use alloc::borrow::ToOwned;
12
use alloc::string::String;
13
use alloc::vec::Vec;
14
use core::cmp;
15
use core::fmt::{self, Formatter};
16
17
use percent_encoding::{percent_decode, utf8_percent_encode, CONTROLS};
18
#[cfg(feature = "serde")]
19
use serde_derive::{Deserialize, Serialize};
20
21
use crate::parser::{ParseError, ParseResult};
22
23
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
24
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
25
pub(crate) enum HostInternal {
26
    None,
27
    Domain,
28
    Ipv4(Ipv4Addr),
29
    Ipv6(Ipv6Addr),
30
}
31
32
impl From<Host<Cow<'_, str>>> for HostInternal {
33
17.9k
    fn from(host: Host<Cow<'_, str>>) -> Self {
34
16.7k
        match host {
35
16.7k
            Host::Domain(ref s) if s.is_empty() => Self::None,
36
16.3k
            Host::Domain(_) => Self::Domain,
37
817
            Host::Ipv4(address) => Self::Ipv4(address),
38
431
            Host::Ipv6(address) => Self::Ipv6(address),
39
        }
40
17.9k
    }
41
}
42
43
/// The host name of an URL.
44
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
45
#[derive(Clone, Debug, Eq, Ord, PartialOrd, Hash)]
46
pub enum Host<S = String> {
47
    /// A DNS domain name, as '.' dot-separated labels.
48
    /// Non-ASCII labels are encoded in punycode per IDNA if this is the host of
49
    /// a special URL, or percent encoded for non-special URLs. Hosts for
50
    /// non-special URLs are also called opaque hosts.
51
    Domain(S),
52
53
    /// An IPv4 address.
54
    /// `Url::host_str` returns the serialization of this address,
55
    /// as four decimal integers separated by `.` dots.
56
    Ipv4(Ipv4Addr),
57
58
    /// An IPv6 address.
59
    /// `Url::host_str` returns the serialization of that address between `[` and `]` brackets,
60
    /// in the format per [RFC 5952 *A Recommendation
61
    /// for IPv6 Address Text Representation*](https://tools.ietf.org/html/rfc5952):
62
    /// lowercase hexadecimal with maximal `::` compression.
63
    Ipv6(Ipv6Addr),
64
}
65
66
impl Host<&str> {
67
    /// Return a copy of `self` that owns an allocated `String` but does not borrow an `&Url`.
68
138
    pub fn to_owned(&self) -> Host<String> {
69
138
        match *self {
70
110
            Host::Domain(domain) => Host::Domain(domain.to_owned()),
71
10
            Host::Ipv4(address) => Host::Ipv4(address),
72
18
            Host::Ipv6(address) => Host::Ipv6(address),
73
        }
74
138
    }
75
}
76
77
impl Host<String> {
78
    /// Parse a host: either an IPv6 address in [] square brackets, or a domain.
79
    ///
80
    /// <https://url.spec.whatwg.org/#host-parsing>
81
1.66k
    pub fn parse(input: &str) -> Result<Self, ParseError> {
82
1.66k
        Host::<Cow<str>>::parse_cow(input.into()).map(|i| i.into_owned())
83
1.66k
    }
84
85
    /// <https://url.spec.whatwg.org/#concept-opaque-host-parser>
86
0
    pub fn parse_opaque(input: &str) -> Result<Self, ParseError> {
87
0
        Host::<Cow<str>>::parse_opaque_cow(input.into()).map(|i| i.into_owned())
88
0
    }
89
}
90
91
impl<'a> Host<Cow<'a, str>> {
92
23.6k
    pub(crate) fn parse_cow(input: Cow<'a, str>) -> Result<Self, ParseError> {
93
23.6k
        if input.starts_with('[') {
94
730
            if !input.ends_with(']') {
95
52
                return Err(ParseError::InvalidIpv6Address);
96
678
            }
97
678
            return parse_ipv6addr(&input[1..input.len() - 1]).map(Host::Ipv6);
98
22.9k
        }
99
22.9k
        let domain: Cow<'_, [u8]> = percent_decode(input.as_bytes()).into();
100
22.9k
        let domain: Cow<'a, [u8]> = match domain {
101
1.04k
            Cow::Owned(v) => Cow::Owned(v),
102
            // if borrowed then we can use the original cow
103
21.8k
            Cow::Borrowed(_) => match input {
104
20.6k
                Cow::Borrowed(input) => Cow::Borrowed(input.as_bytes()),
105
1.22k
                Cow::Owned(input) => Cow::Owned(input.into_bytes()),
106
            },
107
        };
108
109
22.9k
        let domain = idna::domain_to_ascii_from_cow(domain, idna::AsciiDenyList::URL)?;
110
111
17.0k
        if domain.is_empty() {
112
26
            return Err(ParseError::EmptyHost);
113
16.9k
        }
114
115
16.9k
        if ends_in_a_number(&domain) {
116
1.27k
            let address = parse_ipv4addr(&domain)?;
117
876
            Ok(Host::Ipv4(address))
118
        } else {
119
15.7k
            Ok(Host::Domain(domain))
120
        }
121
23.6k
    }
122
123
2.89k
    pub(crate) fn parse_opaque_cow(input: Cow<'a, str>) -> Result<Self, ParseError> {
124
2.89k
        if input.starts_with('[') {
125
104
            if !input.ends_with(']') {
126
23
                return Err(ParseError::InvalidIpv6Address);
127
81
            }
128
81
            return parse_ipv6addr(&input[1..input.len() - 1]).map(Host::Ipv6);
129
2.78k
        }
130
131
47.2M
        let is_invalid_host_char = |c| {
132
47.2M
            matches!(
133
47.2M
                c,
134
                '\0' | '\t'
135
                    | '\n'
136
                    | '\r'
137
                    | ' '
138
                    | '#'
139
                    | '/'
140
                    | ':'
141
                    | '<'
142
                    | '>'
143
                    | '?'
144
                    | '@'
145
                    | '['
146
                    | '\\'
147
                    | ']'
148
                    | '^'
149
                    | '|'
150
            )
151
47.2M
        };
152
153
2.78k
        if input.find(is_invalid_host_char).is_some() {
154
75
            return Err(ParseError::InvalidDomainCharacter);
155
2.71k
        }
156
157
        // Call utf8_percent_encode and use the result.
158
        // Note: This returns Cow::Borrowed for single-item results (either from input
159
        // or from the static encoding table), and Cow::Owned for multi-item results.
160
        // We cannot distinguish between "borrowed from input" vs "borrowed from static table"
161
        // based on the Cow variant alone.
162
        Ok(Host::Domain(
163
2.71k
            match utf8_percent_encode(&input, CONTROLS).into() {
164
929
                Cow::Owned(v) => Cow::Owned(v),
165
                // If we're borrowing, we need to check if it's the same as the input
166
1.78k
                Cow::Borrowed(v) => {
167
1.78k
                    if v == &*input {
168
1.67k
                        input // No encoding happened, reuse original
169
                    } else {
170
111
                        Cow::Owned(v.to_owned()) // Borrowed from static table, need to own it
171
                    }
172
                }
173
            },
174
        ))
175
2.89k
    }
176
177
1.51k
    pub(crate) fn into_owned(self) -> Host<String> {
178
1.51k
        match self {
179
1.42k
            Host::Domain(s) => Host::Domain(s.into_owned()),
180
41
            Host::Ipv4(ip) => Host::Ipv4(ip),
181
46
            Host::Ipv6(ip) => Host::Ipv6(ip),
182
        }
183
1.51k
    }
184
}
185
186
impl<S: AsRef<str>> fmt::Display for Host<S> {
187
18.4k
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
188
18.4k
        match *self {
189
17.1k
            Self::Domain(ref domain) => domain.as_ref().fmt(f),
190
845
            Self::Ipv4(ref addr) => addr.fmt(f),
191
466
            Self::Ipv6(ref addr) => {
192
466
                f.write_str("[")?;
193
466
                write_ipv6(addr, f)?;
194
466
                f.write_str("]")
195
            }
196
        }
197
18.4k
    }
<url::host::Host<alloc::borrow::Cow<str>> as core::fmt::Display>::fmt
Line
Count
Source
187
18.2k
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
188
18.2k
        match *self {
189
16.9k
            Self::Domain(ref domain) => domain.as_ref().fmt(f),
190
835
            Self::Ipv4(ref addr) => addr.fmt(f),
191
448
            Self::Ipv6(ref addr) => {
192
448
                f.write_str("[")?;
193
448
                write_ipv6(addr, f)?;
194
448
                f.write_str("]")
195
            }
196
        }
197
18.2k
    }
<url::host::Host as core::fmt::Display>::fmt
Line
Count
Source
187
138
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
188
138
        match *self {
189
110
            Self::Domain(ref domain) => domain.as_ref().fmt(f),
190
10
            Self::Ipv4(ref addr) => addr.fmt(f),
191
18
            Self::Ipv6(ref addr) => {
192
18
                f.write_str("[")?;
193
18
                write_ipv6(addr, f)?;
194
18
                f.write_str("]")
195
            }
196
        }
197
138
    }
198
}
199
200
impl<S, T> PartialEq<Host<T>> for Host<S>
201
where
202
    S: PartialEq<T>,
203
{
204
1.42k
    fn eq(&self, other: &Host<T>) -> bool {
205
1.42k
        match (self, other) {
206
1.33k
            (Self::Domain(a), Host::Domain(b)) => a == b,
207
0
            (Self::Ipv4(a), Host::Ipv4(b)) => a == b,
208
0
            (Self::Ipv6(a), Host::Ipv6(b)) => a == b,
209
98
            (_, _) => false,
210
        }
211
1.42k
    }
Unexecuted instantiation: <url::host::Host<alloc::borrow::Cow<str>> as core::cmp::PartialEq<url::host::Host>>::eq
<url::host::Host<&str> as core::cmp::PartialEq>::eq
Line
Count
Source
204
1.42k
    fn eq(&self, other: &Host<T>) -> bool {
205
1.42k
        match (self, other) {
206
1.33k
            (Self::Domain(a), Host::Domain(b)) => a == b,
207
0
            (Self::Ipv4(a), Host::Ipv4(b)) => a == b,
208
0
            (Self::Ipv6(a), Host::Ipv6(b)) => a == b,
209
98
            (_, _) => false,
210
        }
211
1.42k
    }
212
}
213
214
466
fn write_ipv6(addr: &Ipv6Addr, f: &mut Formatter<'_>) -> fmt::Result {
215
466
    let segments = addr.segments();
216
466
    let (compress_start, compress_end) = longest_zero_sequence(&segments);
217
466
    let mut i = 0;
218
1.78k
    while i < 8 {
219
1.58k
        if i == compress_start {
220
437
            f.write_str(":")?;
221
437
            if i == 0 {
222
172
                f.write_str(":")?;
223
265
            }
224
437
            if compress_end < 8 {
225
173
                i = compress_end;
226
173
            } else {
227
264
                break;
228
            }
229
1.14k
        }
230
1.32k
        write!(f, "{:x}", segments[i as usize])?;
231
1.32k
        if i < 7 {
232
1.12k
            f.write_str(":")?;
233
202
        }
234
1.32k
        i += 1;
235
    }
236
466
    Ok(())
237
466
}
238
239
// https://url.spec.whatwg.org/#concept-ipv6-serializer step 2 and 3
240
466
fn longest_zero_sequence(pieces: &[u16; 8]) -> (isize, isize) {
241
466
    let mut longest = -1;
242
466
    let mut longest_length = -1;
243
466
    let mut start = -1;
244
    macro_rules! finish_sequence(
245
        ($end: expr) => {
246
            if start >= 0 {
247
                let length = $end - start;
248
                if length > longest_length {
249
                    longest = start;
250
                    longest_length = length;
251
                }
252
            }
253
        };
254
    );
255
4.19k
    for i in 0..8 {
256
3.72k
        if pieces[i as usize] == 0 {
257
2.57k
            if start < 0 {
258
582
                start = i;
259
1.98k
            }
260
        } else {
261
1.15k
            finish_sequence!(i);
262
1.15k
            start = -1;
263
        }
264
    }
265
466
    finish_sequence!(8);
266
    // https://url.spec.whatwg.org/#concept-ipv6-serializer
267
    // step 3: ignore lone zeroes
268
466
    if longest_length < 2 {
269
29
        (-1, -2)
270
    } else {
271
437
        (longest, longest + longest_length)
272
    }
273
466
}
274
275
/// <https://url.spec.whatwg.org/#ends-in-a-number-checker>
276
16.9k
fn ends_in_a_number(input: &str) -> bool {
277
16.9k
    let mut parts = input.rsplit('.');
278
16.9k
    let last = parts.next().unwrap();
279
16.9k
    let last = if last.is_empty() {
280
2.08k
        if let Some(last) = parts.next() {
281
2.08k
            last
282
        } else {
283
0
            return false;
284
        }
285
    } else {
286
14.8k
        last
287
    };
288
1.16M
    if !last.is_empty() && last.as_bytes().iter().all(|c| c.is_ascii_digit()) {
289
1.13k
        return true;
290
15.8k
    }
291
292
15.8k
    parse_ipv4number(last).is_ok()
293
16.9k
}
294
295
/// <https://url.spec.whatwg.org/#ipv4-number-parser>
296
/// Ok(None) means the input is a valid number, but it overflows a `u32`.
297
17.8k
fn parse_ipv4number(mut input: &str) -> Result<Option<u32>, ()> {
298
17.8k
    if input.is_empty() {
299
914
        return Err(());
300
16.9k
    }
301
302
16.9k
    let mut r = 10;
303
16.9k
    if input.starts_with("0x") || input.starts_with("0X") {
304
339
        input = &input[2..];
305
339
        r = 16;
306
16.6k
    } else if input.len() >= 2 && input.starts_with('0') {
307
341
        input = &input[1..];
308
341
        r = 8;
309
16.2k
    }
310
311
16.9k
    if input.is_empty() {
312
38
        return Ok(Some(0));
313
16.9k
    }
314
315
16.9k
    let valid_number = match r {
316
1.09M
        8 => input.as_bytes().iter().all(|c| (b'0'..=b'7').contains(c)),
317
158k
        10 => input.as_bytes().iter().all(|c| c.is_ascii_digit()),
318
417k
        16 => input.as_bytes().iter().all(|c| c.is_ascii_hexdigit()),
319
0
        _ => false,
320
    };
321
16.9k
    if !valid_number {
322
14.8k
        return Err(());
323
2.04k
    }
324
325
2.04k
    match u32::from_str_radix(input, r) {
326
2.01k
        Ok(num) => Ok(Some(num)),
327
31
        Err(_) => Ok(None), // The only possible error kind here is an integer overflow.
328
                            // The validity of the chars in the input is checked above.
329
    }
330
17.8k
}
331
332
/// <https://url.spec.whatwg.org/#concept-ipv4-parser>
333
1.27k
fn parse_ipv4addr(input: &str) -> ParseResult<Ipv4Addr> {
334
1.27k
    let mut parts: Vec<&str> = input.split('.').collect();
335
1.27k
    if parts.last() == Some(&"") {
336
63
        parts.pop();
337
1.20k
    }
338
1.27k
    if parts.len() > 4 {
339
276
        return Err(ParseError::InvalidIpv4Address);
340
995
    }
341
995
    let mut numbers: Vec<u32> = Vec::new();
342
2.91k
    for part in parts {
343
2.02k
        match parse_ipv4number(part) {
344
1.92k
            Ok(Some(n)) => numbers.push(n),
345
19
            Ok(None) => return Err(ParseError::InvalidIpv4Address), // u32 overflow
346
83
            Err(()) => return Err(ParseError::InvalidIpv4Address),
347
        };
348
    }
349
893
    let mut ipv4 = numbers.pop().expect("a non-empty list of numbers");
350
    // Equivalent to: ipv4 >= 256 ** (4 − numbers.len())
351
893
    if ipv4 > u32::MAX >> (8 * numbers.len() as u32) {
352
11
        return Err(ParseError::InvalidIpv4Address);
353
882
    }
354
996
    if numbers.iter().any(|x| *x > 255) {
355
6
        return Err(ParseError::InvalidIpv4Address);
356
876
    }
357
990
    for (counter, n) in numbers.iter().enumerate() {
358
990
        ipv4 += n << (8 * (3 - counter as u32))
359
    }
360
876
    Ok(Ipv4Addr::from(ipv4))
361
1.27k
}
362
363
/// <https://url.spec.whatwg.org/#concept-ipv6-parser>
364
759
fn parse_ipv6addr(input: &str) -> ParseResult<Ipv6Addr> {
365
759
    let input = input.as_bytes();
366
759
    let len = input.len();
367
759
    let mut is_ip_v4 = false;
368
759
    let mut pieces = [0, 0, 0, 0, 0, 0, 0, 0];
369
759
    let mut piece_pointer = 0;
370
759
    let mut compress_pointer = None;
371
759
    let mut i = 0;
372
373
759
    if len < 2 {
374
23
        return Err(ParseError::InvalidIpv6Address);
375
736
    }
376
377
736
    if input[0] == b':' {
378
210
        if input[1] != b':' {
379
12
            return Err(ParseError::InvalidIpv6Address);
380
198
        }
381
198
        i = 2;
382
198
        piece_pointer = 1;
383
198
        compress_pointer = Some(1);
384
526
    }
385
386
2.55k
    while i < len {
387
2.04k
        if piece_pointer == 8 {
388
2
            return Err(ParseError::InvalidIpv6Address);
389
2.04k
        }
390
2.04k
        if input[i] == b':' {
391
309
            if compress_pointer.is_some() {
392
4
                return Err(ParseError::InvalidIpv6Address);
393
305
            }
394
305
            i += 1;
395
305
            piece_pointer += 1;
396
305
            compress_pointer = Some(piece_pointer);
397
305
            continue;
398
1.73k
        }
399
1.73k
        let start = i;
400
1.73k
        let end = cmp::min(len, start + 4);
401
1.73k
        let mut value = 0u16;
402
4.20k
        while i < end {
403
3.83k
            match (input[i] as char).to_digit(16) {
404
2.46k
                Some(digit) => {
405
2.46k
                    value = value * 0x10 + digit as u16;
406
2.46k
                    i += 1;
407
2.46k
                }
408
1.36k
                None => break,
409
            }
410
        }
411
1.73k
        if i < len {
412
1.50k
            match input[i] {
413
                b'.' => {
414
134
                    if i == start {
415
2
                        return Err(ParseError::InvalidIpv6Address);
416
132
                    }
417
132
                    i = start;
418
132
                    if piece_pointer > 6 {
419
2
                        return Err(ParseError::InvalidIpv6Address);
420
130
                    }
421
130
                    is_ip_v4 = true;
422
                }
423
                b':' => {
424
1.29k
                    i += 1;
425
1.29k
                    if i == len {
426
4
                        return Err(ParseError::InvalidIpv6Address);
427
1.29k
                    }
428
                }
429
75
                _ => return Err(ParseError::InvalidIpv6Address),
430
            }
431
233
        }
432
1.65k
        if is_ip_v4 {
433
130
            break;
434
1.52k
        }
435
1.52k
        pieces[piece_pointer] = value;
436
1.52k
        piece_pointer += 1;
437
    }
438
439
635
    if is_ip_v4 {
440
130
        if piece_pointer > 6 {
441
0
            return Err(ParseError::InvalidIpv6Address);
442
130
        }
443
130
        let mut numbers_seen = 0;
444
344
        while i < len {
445
328
            if numbers_seen > 0 {
446
198
                if numbers_seen < 4 && (i < len && input[i] == b'.') {
447
155
                    i += 1
448
                } else {
449
43
                    return Err(ParseError::InvalidIpv6Address);
450
                }
451
130
            }
452
453
285
            let mut ipv4_piece = None;
454
630
            while i < len {
455
585
                let digit = match input[i] {
456
393
                    c @ b'0'..=b'9' => c - b'0',
457
225
                    _ => break,
458
                };
459
360
                match ipv4_piece {
460
229
                    None => ipv4_piece = Some(digit as u16),
461
3
                    Some(0) => return Err(ParseError::InvalidIpv6Address), // No leading zero
462
128
                    Some(ref mut v) => {
463
128
                        *v = *v * 10 + digit as u16;
464
128
                        if *v > 255 {
465
12
                            return Err(ParseError::InvalidIpv6Address);
466
116
                        }
467
                    }
468
                }
469
345
                i += 1;
470
            }
471
472
270
            pieces[piece_pointer] = if let Some(v) = ipv4_piece {
473
214
                pieces[piece_pointer] * 0x100 + v
474
            } else {
475
56
                return Err(ParseError::InvalidIpv6Address);
476
            };
477
214
            numbers_seen += 1;
478
479
214
            if numbers_seen == 2 || numbers_seen == 4 {
480
71
                piece_pointer += 1;
481
143
            }
482
        }
483
484
16
        if numbers_seen != 4 {
485
13
            return Err(ParseError::InvalidIpv6Address);
486
3
        }
487
505
    }
488
489
508
    if i < len {
490
0
        return Err(ParseError::InvalidIpv6Address);
491
508
    }
492
493
508
    match compress_pointer {
494
473
        Some(compress_pointer) => {
495
473
            let mut swaps = piece_pointer - compress_pointer;
496
473
            piece_pointer = 7;
497
958
            while swaps > 0 {
498
485
                pieces.swap(piece_pointer, compress_pointer + swaps - 1);
499
485
                swaps -= 1;
500
485
                piece_pointer -= 1;
501
485
            }
502
        }
503
        _ => {
504
35
            if piece_pointer != 8 {
505
14
                return Err(ParseError::InvalidIpv6Address);
506
21
            }
507
        }
508
    }
509
494
    Ok(Ipv6Addr::new(
510
494
        pieces[0], pieces[1], pieces[2], pieces[3], pieces[4], pieces[5], pieces[6], pieces[7],
511
494
    ))
512
759
}