Coverage Report

Created: 2026-06-01 06:40

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/http/src/uri/path.rs
Line
Count
Source
1
use std::convert::TryFrom;
2
use std::str::FromStr;
3
use std::{cmp, fmt, hash, str};
4
5
use bytes::Bytes;
6
7
use super::{ErrorKind, InvalidUri};
8
use crate::byte_str::ByteStr;
9
10
/// Represents the path component of a URI
11
#[derive(Clone)]
12
pub struct PathAndQuery {
13
    pub(super) data: ByteStr,
14
    pub(super) query: u16,
15
}
16
17
const NONE: u16 = u16::MAX;
18
19
impl PathAndQuery {
20
    // Not public while `bytes` is unstable.
21
299
    pub(super) fn from_shared(mut src: Bytes) -> Result<Self, InvalidUri> {
22
        let Scanned {
23
217
            query,
24
217
            fragment,
25
217
            is_maybe_not_utf8,
26
299
        } = scan_path_and_query(&src)?;
27
28
217
        if let Some(i) = fragment {
29
33
            src.truncate(i as usize);
30
184
        }
31
32
217
        let data = if is_maybe_not_utf8 {
33
60
            ByteStr::from_utf8(src).map_err(|_| ErrorKind::InvalidUriChar)?
34
        } else {
35
157
            unsafe { ByteStr::from_utf8_unchecked(src) }
36
        };
37
38
160
        Ok(PathAndQuery { data, query })
39
299
    }
40
41
    /// Convert a `PathAndQuery` from a static string.
42
    ///
43
    /// This function will not perform any copying, however the string is
44
    /// checked to ensure that it is valid.
45
    ///
46
    /// # Panics
47
    ///
48
    /// This function panics if the argument is an invalid path and query.
49
    ///
50
    /// # Examples
51
    ///
52
    /// ```
53
    /// # use http::uri::*;
54
    /// let v = PathAndQuery::from_static("/hello?world");
55
    ///
56
    /// assert_eq!(v.path(), "/hello");
57
    /// assert_eq!(v.query(), Some("world"));
58
    /// ```
59
    #[inline]
60
    pub const fn from_static(src: &'static str) -> Self {
61
        match scan_path_and_query(src.as_bytes()) {
62
            Ok(Scanned {
63
                query,
64
                fragment: None,
65
                is_maybe_not_utf8: false,
66
            }) => PathAndQuery {
67
                data: ByteStr::from_static(src),
68
                query,
69
            },
70
            // Yes, we reject fragments and non-utf8
71
            _ => panic!("static str is not valid path"),
72
        }
73
    }
74
75
    /// Attempt to convert a `Bytes` buffer to a `PathAndQuery`.
76
    ///
77
    /// This will try to prevent a copy if the type passed is the type used
78
    /// internally, and will copy the data if it is not.
79
    pub fn from_maybe_shared<T>(src: T) -> Result<Self, InvalidUri>
80
    where
81
        T: AsRef<[u8]> + 'static,
82
    {
83
        if_downcast_into!(T, Bytes, src, {
84
            return PathAndQuery::from_shared(src);
85
        });
86
87
        PathAndQuery::try_from(src.as_ref())
88
    }
89
90
1.06k
    pub(super) fn empty() -> Self {
91
1.06k
        PathAndQuery {
92
1.06k
            data: ByteStr::new(),
93
1.06k
            query: NONE,
94
1.06k
        }
95
1.06k
    }
96
97
6.12k
    pub(super) fn slash() -> Self {
98
6.12k
        PathAndQuery {
99
6.12k
            data: ByteStr::from_static("/"),
100
6.12k
            query: NONE,
101
6.12k
        }
102
6.12k
    }
103
104
13
    pub(super) fn star() -> Self {
105
13
        PathAndQuery {
106
13
            data: ByteStr::from_static("*"),
107
13
            query: NONE,
108
13
        }
109
13
    }
110
111
    /// Returns the path component
112
    ///
113
    /// The path component is **case sensitive**.
114
    ///
115
    /// ```notrust
116
    /// abc://username:password@example.com:123/path/data?key=value&key2=value2#fragid1
117
    ///                                        |--------|
118
    ///                                             |
119
    ///                                           path
120
    /// ```
121
    ///
122
    /// If the URI is `*` then the path component is equal to `*`.
123
    ///
124
    /// # Examples
125
    ///
126
    /// ```
127
    /// # use http::uri::*;
128
    ///
129
    /// let path_and_query: PathAndQuery = "/hello/world".parse().unwrap();
130
    ///
131
    /// assert_eq!(path_and_query.path(), "/hello/world");
132
    /// ```
133
    #[inline]
134
0
    pub fn path(&self) -> &str {
135
0
        let ret = if self.query == NONE {
136
0
            &self.data[..]
137
        } else {
138
0
            &self.data[..self.query as usize]
139
        };
140
141
0
        if ret.is_empty() {
142
0
            return "/";
143
0
        }
144
145
0
        ret
146
0
    }
147
148
    /// Returns the query string component
149
    ///
150
    /// The query component contains non-hierarchical data that, along with data
151
    /// in the path component, serves to identify a resource within the scope of
152
    /// the URI's scheme and naming authority (if any). The query component is
153
    /// indicated by the first question mark ("?") character and terminated by a
154
    /// number sign ("#") character or by the end of the URI.
155
    ///
156
    /// ```notrust
157
    /// abc://username:password@example.com:123/path/data?key=value&key2=value2#fragid1
158
    ///                                                   |-------------------|
159
    ///                                                             |
160
    ///                                                           query
161
    /// ```
162
    ///
163
    /// # Examples
164
    ///
165
    /// With a query string component
166
    ///
167
    /// ```
168
    /// # use http::uri::*;
169
    /// let path_and_query: PathAndQuery = "/hello/world?key=value&foo=bar".parse().unwrap();
170
    ///
171
    /// assert_eq!(path_and_query.query(), Some("key=value&foo=bar"));
172
    /// ```
173
    ///
174
    /// Without a query string component
175
    ///
176
    /// ```
177
    /// # use http::uri::*;
178
    /// let path_and_query: PathAndQuery = "/hello/world".parse().unwrap();
179
    ///
180
    /// assert!(path_and_query.query().is_none());
181
    /// ```
182
    #[inline]
183
0
    pub fn query(&self) -> Option<&str> {
184
0
        if self.query == NONE {
185
0
            None
186
        } else {
187
0
            let i = self.query + 1;
188
0
            Some(&self.data[i as usize..])
189
        }
190
0
    }
191
192
    /// Returns the path and query as a string component.
193
    ///
194
    /// # Examples
195
    ///
196
    /// With a query string component
197
    ///
198
    /// ```
199
    /// # use http::uri::*;
200
    /// let path_and_query: PathAndQuery = "/hello/world?key=value&foo=bar".parse().unwrap();
201
    ///
202
    /// assert_eq!(path_and_query.as_str(), "/hello/world?key=value&foo=bar");
203
    /// ```
204
    ///
205
    /// Without a query string component
206
    ///
207
    /// ```
208
    /// # use http::uri::*;
209
    /// let path_and_query: PathAndQuery = "/hello/world".parse().unwrap();
210
    ///
211
    /// assert_eq!(path_and_query.as_str(), "/hello/world");
212
    /// ```
213
    #[inline]
214
    pub fn as_str(&self) -> &str {
215
        let ret = &self.data[..];
216
        if ret.is_empty() {
217
            return "/";
218
        }
219
        ret
220
    }
221
}
222
223
impl TryFrom<&[u8]> for PathAndQuery {
224
    type Error = InvalidUri;
225
    #[inline]
226
    fn try_from(s: &[u8]) -> Result<Self, Self::Error> {
227
        PathAndQuery::from_shared(Bytes::copy_from_slice(s))
228
    }
229
}
230
231
impl TryFrom<&str> for PathAndQuery {
232
    type Error = InvalidUri;
233
    #[inline]
234
    fn try_from(s: &str) -> Result<Self, Self::Error> {
235
        TryFrom::try_from(s.as_bytes())
236
    }
237
}
238
239
impl TryFrom<Vec<u8>> for PathAndQuery {
240
    type Error = InvalidUri;
241
    #[inline]
242
    fn try_from(vec: Vec<u8>) -> Result<Self, Self::Error> {
243
        PathAndQuery::from_shared(vec.into())
244
    }
245
}
246
247
impl TryFrom<String> for PathAndQuery {
248
    type Error = InvalidUri;
249
    #[inline]
250
    fn try_from(s: String) -> Result<Self, Self::Error> {
251
        PathAndQuery::from_shared(s.into())
252
    }
253
}
254
255
impl TryFrom<&String> for PathAndQuery {
256
    type Error = InvalidUri;
257
    #[inline]
258
    fn try_from(s: &String) -> Result<Self, Self::Error> {
259
        TryFrom::try_from(s.as_bytes())
260
    }
261
}
262
263
impl FromStr for PathAndQuery {
264
    type Err = InvalidUri;
265
    #[inline]
266
    fn from_str(s: &str) -> Result<Self, InvalidUri> {
267
        TryFrom::try_from(s)
268
    }
269
}
270
271
impl fmt::Debug for PathAndQuery {
272
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
273
0
        fmt::Display::fmt(self, f)
274
0
    }
275
}
276
277
impl fmt::Display for PathAndQuery {
278
0
    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
279
0
        if !self.data.is_empty() {
280
0
            match self.data.as_bytes()[0] {
281
0
                b'/' | b'*' => write!(fmt, "{}", &self.data[..]),
282
0
                _ => write!(fmt, "/{}", &self.data[..]),
283
            }
284
        } else {
285
0
            write!(fmt, "/")
286
        }
287
0
    }
288
}
289
290
impl hash::Hash for PathAndQuery {
291
    fn hash<H: hash::Hasher>(&self, state: &mut H) {
292
        self.data.hash(state);
293
    }
294
}
295
296
// ===== PartialEq / PartialOrd =====
297
298
impl PartialEq for PathAndQuery {
299
    #[inline]
300
    fn eq(&self, other: &PathAndQuery) -> bool {
301
        self.data == other.data
302
    }
303
}
304
305
impl Eq for PathAndQuery {}
306
307
impl PartialEq<str> for PathAndQuery {
308
    #[inline]
309
    fn eq(&self, other: &str) -> bool {
310
        self.as_str() == other
311
    }
312
}
313
314
impl PartialEq<PathAndQuery> for &str {
315
    #[inline]
316
    fn eq(&self, other: &PathAndQuery) -> bool {
317
        self == &other.as_str()
318
    }
319
}
320
321
impl PartialEq<&str> for PathAndQuery {
322
    #[inline]
323
    fn eq(&self, other: &&str) -> bool {
324
        self.as_str() == *other
325
    }
326
}
327
328
impl PartialEq<PathAndQuery> for str {
329
    #[inline]
330
    fn eq(&self, other: &PathAndQuery) -> bool {
331
        self == other.as_str()
332
    }
333
}
334
335
impl PartialEq<String> for PathAndQuery {
336
    #[inline]
337
    fn eq(&self, other: &String) -> bool {
338
        self.as_str() == other.as_str()
339
    }
340
}
341
342
impl PartialEq<PathAndQuery> for String {
343
    #[inline]
344
    fn eq(&self, other: &PathAndQuery) -> bool {
345
        self.as_str() == other.as_str()
346
    }
347
}
348
349
impl PartialOrd for PathAndQuery {
350
    #[inline]
351
    fn partial_cmp(&self, other: &PathAndQuery) -> Option<cmp::Ordering> {
352
        self.as_str().partial_cmp(other.as_str())
353
    }
354
}
355
356
impl PartialOrd<str> for PathAndQuery {
357
    #[inline]
358
    fn partial_cmp(&self, other: &str) -> Option<cmp::Ordering> {
359
        self.as_str().partial_cmp(other)
360
    }
361
}
362
363
impl PartialOrd<PathAndQuery> for str {
364
    #[inline]
365
    fn partial_cmp(&self, other: &PathAndQuery) -> Option<cmp::Ordering> {
366
        self.partial_cmp(other.as_str())
367
    }
368
}
369
370
impl PartialOrd<&str> for PathAndQuery {
371
    #[inline]
372
    fn partial_cmp(&self, other: &&str) -> Option<cmp::Ordering> {
373
        self.as_str().partial_cmp(*other)
374
    }
375
}
376
377
impl PartialOrd<PathAndQuery> for &str {
378
    #[inline]
379
    fn partial_cmp(&self, other: &PathAndQuery) -> Option<cmp::Ordering> {
380
        self.partial_cmp(&other.as_str())
381
    }
382
}
383
384
impl PartialOrd<String> for PathAndQuery {
385
    #[inline]
386
    fn partial_cmp(&self, other: &String) -> Option<cmp::Ordering> {
387
        self.as_str().partial_cmp(other.as_str())
388
    }
389
}
390
391
impl PartialOrd<PathAndQuery> for String {
392
    #[inline]
393
    fn partial_cmp(&self, other: &PathAndQuery) -> Option<cmp::Ordering> {
394
        self.as_str().partial_cmp(other.as_str())
395
    }
396
}
397
398
// Scanner implementation that is `const fn`, usable by both `from_static`
399
// and `from_shared`.
400
// =====
401
402
struct Scanned {
403
    query: u16,
404
    fragment: Option<u16>,
405
    is_maybe_not_utf8: bool,
406
}
407
408
299
const fn scan_path_and_query(bytes: &[u8]) -> Result<Scanned, ErrorKind> {
409
299
    let mut i = 0;
410
299
    let mut query = NONE;
411
299
    let mut fragment = None;
412
413
299
    let mut is_maybe_not_utf8 = false;
414
415
299
    if bytes.is_empty() {
416
0
        return Err(ErrorKind::Empty);
417
299
    }
418
419
299
    if !matches!(bytes[0], b'/' | b'?' | b'#') {
420
0
        return Err(ErrorKind::PathDoesNotStartWithSlash);
421
299
    }
422
423
860k
    while i < bytes.len() {
424
        // See https://url.spec.whatwg.org/#path-state
425
860k
        match bytes[i] {
426
            b'?' => {
427
110
                debug_assert!(query == NONE);
428
110
                query = i as u16;
429
110
                i += 1;
430
110
                break;
431
            }
432
            b'#' => {
433
22
                fragment = Some(i as u16);
434
22
                break;
435
            }
436
437
            // This is the range of bytes that don't need to be
438
            // percent-encoded in the path. If it should have been
439
            // percent-encoded, then error.
440
            #[rustfmt::skip]
441
            0x21 |
442
858k
            0x24..=0x3B |
443
            0x3D |
444
857k
            0x40..=0x5F |
445
857k
            0x61..=0x7A |
446
            0x7C |
447
856k
            0x7E => {}
448
449
            // potentially utf8, might not, should check
450
3.00k
            0x7F..=0xFF => {
451
3.00k
                is_maybe_not_utf8 = true;
452
3.00k
            }
453
454
            // These are code points that are supposed to be
455
            // percent-encoded in the path but there are clients
456
            // out there sending them as is and httparse accepts
457
            // to parse those requests, so they are allowed here
458
            // for parity.
459
            //
460
            // For reference, those are code points that are used
461
            // to send requests with JSON directly embedded in
462
            // the URI path. Yes, those things happen for real.
463
            #[rustfmt::skip]
464
            b'"' |
465
835
            b'{' | b'}' => {}
466
467
43
            _ => return Err(ErrorKind::InvalidUriChar),
468
        }
469
860k
        i += 1;
470
    }
471
472
    // query ...
473
256
    if query != NONE {
474
34.1k
        while i < bytes.len() {
475
34.0k
            match bytes[i] {
476
                // While queries *should* be percent-encoded, most
477
                // bytes are actually allowed...
478
                // See https://url.spec.whatwg.org/#query-state
479
                //
480
                // Allowed: 0x21 / 0x24 - 0x3B / 0x3D / 0x3F - 0x7E
481
                #[rustfmt::skip]
482
                0x21 |
483
33.4k
                0x24..=0x3B |
484
                0x3D |
485
32.5k
                0x3F..=0x7E => {}
486
487
4.28k
                0x7F..=0xFF => {
488
4.28k
                    is_maybe_not_utf8 = true;
489
4.28k
                }
490
491
                b'#' => {
492
11
                    fragment = Some(i as u16);
493
11
                    break;
494
                }
495
496
39
                _ => return Err(ErrorKind::InvalidUriChar),
497
            }
498
34.0k
            i += 1;
499
        }
500
146
    }
501
502
217
    Ok(Scanned {
503
217
        query,
504
217
        fragment,
505
217
        is_maybe_not_utf8,
506
217
    })
507
299
}
508
509
#[cfg(test)]
510
mod tests {
511
    use super::*;
512
513
    #[test]
514
    fn equal_to_self_of_same_path() {
515
        let p1: PathAndQuery = "/hello/world&foo=bar".parse().unwrap();
516
        let p2: PathAndQuery = "/hello/world&foo=bar".parse().unwrap();
517
        assert_eq!(p1, p2);
518
        assert_eq!(p2, p1);
519
    }
520
521
    #[test]
522
    fn not_equal_to_self_of_different_path() {
523
        let p1: PathAndQuery = "/hello/world&foo=bar".parse().unwrap();
524
        let p2: PathAndQuery = "/world&foo=bar".parse().unwrap();
525
        assert_ne!(p1, p2);
526
        assert_ne!(p2, p1);
527
    }
528
529
    #[test]
530
    fn equates_with_a_str() {
531
        let path_and_query: PathAndQuery = "/hello/world&foo=bar".parse().unwrap();
532
        assert_eq!(&path_and_query, "/hello/world&foo=bar");
533
        assert_eq!("/hello/world&foo=bar", &path_and_query);
534
        assert_eq!(path_and_query, "/hello/world&foo=bar");
535
        assert_eq!("/hello/world&foo=bar", path_and_query);
536
    }
537
538
    #[test]
539
    fn not_equal_with_a_str_of_a_different_path() {
540
        let path_and_query: PathAndQuery = "/hello/world&foo=bar".parse().unwrap();
541
        // as a reference
542
        assert_ne!(&path_and_query, "/hello&foo=bar");
543
        assert_ne!("/hello&foo=bar", &path_and_query);
544
        // without reference
545
        assert_ne!(path_and_query, "/hello&foo=bar");
546
        assert_ne!("/hello&foo=bar", path_and_query);
547
    }
548
549
    #[test]
550
    fn equates_with_a_string() {
551
        let path_and_query: PathAndQuery = "/hello/world&foo=bar".parse().unwrap();
552
        assert_eq!(path_and_query, "/hello/world&foo=bar".to_string());
553
        assert_eq!("/hello/world&foo=bar".to_string(), path_and_query);
554
    }
555
556
    #[test]
557
    fn not_equal_with_a_string_of_a_different_path() {
558
        let path_and_query: PathAndQuery = "/hello/world&foo=bar".parse().unwrap();
559
        assert_ne!(path_and_query, "/hello&foo=bar".to_string());
560
        assert_ne!("/hello&foo=bar".to_string(), path_and_query);
561
    }
562
563
    #[test]
564
    fn compares_to_self() {
565
        let p1: PathAndQuery = "/a/world&foo=bar".parse().unwrap();
566
        let p2: PathAndQuery = "/b/world&foo=bar".parse().unwrap();
567
        assert!(p1 < p2);
568
        assert!(p2 > p1);
569
    }
570
571
    #[test]
572
    fn compares_with_a_str() {
573
        let path_and_query: PathAndQuery = "/b/world&foo=bar".parse().unwrap();
574
        // by ref
575
        assert!(&path_and_query < "/c/world&foo=bar");
576
        assert!("/c/world&foo=bar" > &path_and_query);
577
        assert!(&path_and_query > "/a/world&foo=bar");
578
        assert!("/a/world&foo=bar" < &path_and_query);
579
580
        // by val
581
        assert!(path_and_query < "/c/world&foo=bar");
582
        assert!("/c/world&foo=bar" > path_and_query);
583
        assert!(path_and_query > "/a/world&foo=bar");
584
        assert!("/a/world&foo=bar" < path_and_query);
585
    }
586
587
    #[test]
588
    fn compares_with_a_string() {
589
        let path_and_query: PathAndQuery = "/b/world&foo=bar".parse().unwrap();
590
        assert!(path_and_query < "/c/world&foo=bar".to_string());
591
        assert!("/c/world&foo=bar".to_string() > path_and_query);
592
        assert!(path_and_query > "/a/world&foo=bar".to_string());
593
        assert!("/a/world&foo=bar".to_string() < path_and_query);
594
    }
595
596
    #[test]
597
    fn ignores_valid_percent_encodings() {
598
        assert_eq!("/a%20b", pq("/a%20b?r=1").path());
599
        assert_eq!("qr=%31", pq("/a/b?qr=%31").query().unwrap());
600
    }
601
602
    #[test]
603
    fn ignores_invalid_percent_encodings() {
604
        assert_eq!("/a%%b", pq("/a%%b?r=1").path());
605
        assert_eq!("/aaa%", pq("/aaa%").path());
606
        assert_eq!("/aaa%", pq("/aaa%?r=1").path());
607
        assert_eq!("/aa%2", pq("/aa%2").path());
608
        assert_eq!("/aa%2", pq("/aa%2?r=1").path());
609
        assert_eq!("qr=%3", pq("/a/b?qr=%3").query().unwrap());
610
    }
611
612
    #[test]
613
    fn allow_utf8_in_path() {
614
        assert_eq!("/🍕", pq("/🍕").path());
615
    }
616
617
    #[test]
618
    fn allow_utf8_in_query() {
619
        assert_eq!(Some("pizza=🍕"), pq("/test?pizza=🍕").query());
620
    }
621
622
    #[test]
623
    fn rejects_invalid_utf8_in_path() {
624
        PathAndQuery::try_from(&[b'/', 0xFF][..]).expect_err("reject invalid utf8");
625
    }
626
627
    #[test]
628
    fn rejects_invalid_utf8_in_query() {
629
        PathAndQuery::try_from(&[b'/', b'a', b'?', 0xFF][..]).expect_err("reject invalid utf8");
630
    }
631
632
    #[test]
633
    fn rejects_empty_string() {
634
        PathAndQuery::try_from("").expect_err("reject empty str");
635
    }
636
637
    #[test]
638
    fn requires_starting_with_slash() {
639
        PathAndQuery::try_from("sneaky").expect_err("reject missing slash");
640
    }
641
642
    #[test]
643
    fn json_is_fine() {
644
        assert_eq!(
645
            r#"/{"bread":"baguette"}"#,
646
            pq(r#"/{"bread":"baguette"}"#).path()
647
        );
648
    }
649
650
    fn pq(s: &str) -> PathAndQuery {
651
        s.parse().expect(&format!("parsing {}", s))
652
    }
653
}