turbopack_core/resolve/
parse.rs

1use std::sync::LazyLock;
2
3use anyhow::{Ok, Result};
4use regex::Regex;
5use turbo_rcstr::{RcStr, rcstr};
6use turbo_tasks::{ResolvedVc, TryJoinIterExt, ValueToString, Vc};
7
8use super::pattern::Pattern;
9
10#[turbo_tasks::value(shared)]
11#[derive(Hash, Clone, Debug)]
12pub enum Request {
13    Raw {
14        path: Pattern,
15        query: RcStr,
16        force_in_lookup_dir: bool,
17        fragment: RcStr,
18    },
19    Relative {
20        path: Pattern,
21        query: RcStr,
22        force_in_lookup_dir: bool,
23        fragment: RcStr,
24    },
25    Module {
26        module: RcStr,
27        path: Pattern,
28        query: RcStr,
29        fragment: RcStr,
30    },
31    ServerRelative {
32        path: Pattern,
33        query: RcStr,
34        fragment: RcStr,
35    },
36    Windows {
37        path: Pattern,
38        query: RcStr,
39        fragment: RcStr,
40    },
41    Empty,
42    PackageInternal {
43        path: Pattern,
44    },
45    Uri {
46        protocol: RcStr,
47        remainder: RcStr,
48        query: RcStr,
49        fragment: RcStr,
50    },
51    DataUri {
52        media_type: RcStr,
53        encoding: RcStr,
54        data: ResolvedVc<RcStr>,
55    },
56    Unknown {
57        path: Pattern,
58    },
59    Dynamic,
60    Alternatives {
61        requests: Vec<ResolvedVc<Request>>,
62    },
63}
64
65/// Splits a string like `foo?bar#baz` into `(Pattern::Constant('foo'), '?bar', '#baz')`
66///
67/// If the hash or query portion are missing they will be empty strings otherwise they will be
68/// non-empty along with their prepender characters
69fn split_off_query_fragment(mut raw: &str) -> (Pattern, RcStr, RcStr) {
70    // Per the URI spec fragments can contain `?` characters, so we should trim it off first
71    // https://datatracker.ietf.org/doc/html/rfc3986#section-3.5
72
73    let hash = match raw.as_bytes().iter().position(|&b| b == b'#') {
74        Some(pos) => {
75            let (prefix, hash) = raw.split_at(pos);
76            raw = prefix;
77            RcStr::from(hash)
78        }
79        None => RcStr::default(),
80    };
81
82    let query = match raw.as_bytes().iter().position(|&b| b == b'?') {
83        Some(pos) => {
84            let (prefix, query) = raw.split_at(pos);
85            raw = prefix;
86            RcStr::from(query)
87        }
88        None => RcStr::default(),
89    };
90    (Pattern::Constant(RcStr::from(raw)), query, hash)
91}
92
93static WINDOWS_PATH: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^[A-Za-z]:\\|\\\\").unwrap());
94static URI_PATH: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^([^/\\:]+:)(.+)$").unwrap());
95static DATA_URI_REMAINDER: LazyLock<Regex> =
96    LazyLock::new(|| Regex::new(r"^([^;,]*)(?:;([^,]+))?,(.*)$").unwrap());
97static MODULE_PATH: LazyLock<Regex> =
98    LazyLock::new(|| Regex::new(r"^((?:@[^/]+/)?[^/]+)(.*)$").unwrap());
99
100impl Request {
101    /// Turns the request into a string.
102    ///
103    /// Note that this is only returns something for the most basic and
104    /// fully constant patterns.
105    pub fn request(&self) -> Option<RcStr> {
106        Some(match self {
107            Request::Raw {
108                path: Pattern::Constant(path),
109                ..
110            } => path.clone(),
111            Request::Relative {
112                path: Pattern::Constant(path),
113                ..
114            } => path.clone(),
115            Request::Module {
116                module,
117                path: Pattern::Constant(path),
118                ..
119            } => format!("{module}{path}").into(),
120            Request::ServerRelative {
121                path: Pattern::Constant(path),
122                ..
123            } => path.clone(),
124            Request::Windows {
125                path: Pattern::Constant(path),
126                ..
127            } => path.clone(),
128            Request::Empty => rcstr!(""),
129            Request::PackageInternal {
130                path: Pattern::Constant(path),
131                ..
132            } => path.clone(),
133            Request::Uri {
134                protocol,
135                remainder,
136                ..
137            } => format!("{protocol}{remainder}").into(),
138            Request::Unknown {
139                path: Pattern::Constant(path),
140            } => path.clone(),
141            _ => return None,
142        })
143    }
144
145    /// Internal construction function.  Should only be called with normalized patterns, or
146    /// recursively. Most users should call [Self::parse] instead.
147    fn parse_ref(request: Pattern) -> Self {
148        match request {
149            Pattern::Dynamic => Request::Dynamic,
150            Pattern::Constant(r) => Request::parse_constant_pattern(r),
151            Pattern::Concatenation(list) => Request::parse_concatenation_pattern(list),
152            Pattern::Alternatives(_) => panic!(
153                "request should be normalized and alternatives should have already been handled.",
154            ),
155        }
156    }
157
158    fn parse_constant_pattern(r: RcStr) -> Self {
159        if r.is_empty() {
160            return Request::Empty;
161        }
162
163        if let Some(remainder) = r.strip_prefix("//") {
164            return Request::Uri {
165                protocol: rcstr!("//"),
166                remainder: remainder.into(),
167                query: RcStr::default(),
168                fragment: RcStr::default(),
169            };
170        }
171
172        if r.starts_with('/') {
173            let (path, query, fragment) = split_off_query_fragment(&r);
174
175            return Request::ServerRelative {
176                path,
177                query,
178                fragment,
179            };
180        }
181
182        if r.starts_with('#') {
183            return Request::PackageInternal {
184                path: Pattern::Constant(r),
185            };
186        }
187
188        if r.starts_with("./") || r.starts_with("../") || r == "." || r == ".." {
189            let (path, query, fragment) = split_off_query_fragment(&r);
190
191            return Request::Relative {
192                path,
193                force_in_lookup_dir: false,
194                query,
195                fragment,
196            };
197        }
198
199        if WINDOWS_PATH.is_match(&r) {
200            let (path, query, fragment) = split_off_query_fragment(&r);
201
202            return Request::Windows {
203                path,
204                query,
205                fragment,
206            };
207        }
208
209        if let Some(caps) = URI_PATH.captures(&r)
210            && let (Some(protocol), Some(remainder)) = (caps.get(1), caps.get(2))
211        {
212            if let Some(caps) = DATA_URI_REMAINDER.captures(remainder.as_str()) {
213                let media_type = caps.get(1).map_or(RcStr::default(), |m| m.as_str().into());
214                let encoding = caps.get(2).map_or(RcStr::default(), |e| e.as_str().into());
215                let data = caps.get(3).map_or(RcStr::default(), |d| d.as_str().into());
216
217                return Request::DataUri {
218                    media_type,
219                    encoding,
220                    data: ResolvedVc::cell(data),
221                };
222            }
223
224            return Request::Uri {
225                protocol: protocol.as_str().into(),
226                remainder: remainder.as_str().into(),
227                query: RcStr::default(),
228                fragment: RcStr::default(),
229            };
230        }
231
232        if let Some((module, path)) = MODULE_PATH
233            .captures(&r)
234            .and_then(|caps| caps.get(1).zip(caps.get(2)))
235        {
236            let (path, query, fragment) = split_off_query_fragment(path.as_str());
237
238            return Request::Module {
239                module: module.as_str().into(),
240                path,
241                query,
242                fragment,
243            };
244        }
245
246        Request::Unknown {
247            path: Pattern::Constant(r),
248        }
249    }
250
251    fn parse_concatenation_pattern(list: Vec<Pattern>) -> Self {
252        if list.is_empty() {
253            return Request::Empty;
254        }
255
256        let mut result = Self::parse_ref(list[0].clone());
257
258        for item in list.into_iter().skip(1) {
259            match &mut result {
260                Request::Raw { path, .. } => {
261                    path.push(item);
262                }
263                Request::Relative { path, .. } => {
264                    path.push(item);
265                }
266                Request::Module { path, .. } => {
267                    path.push(item);
268                }
269                Request::ServerRelative { path, .. } => {
270                    path.push(item);
271                }
272                Request::Windows { path, .. } => {
273                    path.push(item);
274                }
275                Request::Empty => {
276                    result = Self::parse_ref(item);
277                }
278                Request::PackageInternal { path } => {
279                    path.push(item);
280                }
281                Request::DataUri { .. } => {
282                    result = Request::Dynamic;
283                }
284                Request::Uri { .. } => {
285                    result = Request::Dynamic;
286                }
287                Request::Unknown { path } => {
288                    path.push(item);
289                }
290                Request::Dynamic => {}
291                Request::Alternatives { .. } => unreachable!(),
292            };
293        }
294
295        result
296    }
297
298    pub fn parse_string(request: RcStr) -> Vc<Self> {
299        Self::parse(request.into())
300    }
301
302    pub fn parse(mut request: Pattern) -> Vc<Self> {
303        // Call normalize before parse_inner to improve cache hits.
304        request.normalize();
305        Self::parse_inner(request)
306    }
307}
308
309#[turbo_tasks::value_impl]
310impl Request {
311    #[turbo_tasks::function]
312    async fn parse_inner(request: Pattern) -> Result<Vc<Self>> {
313        // Because we are normalized, we should handle alternatives here
314        if let Pattern::Alternatives(alts) = request {
315            Ok(Self::cell(Self::Alternatives {
316                requests: alts
317                    .into_iter()
318                    // We can call parse_inner directly because these patterns are already
319                    // normalized.  We don't call `Self::parse_ref` so we can try to get a cache hit
320                    // on the sub-patterns
321                    .map(|p| Self::parse_inner(p).to_resolved())
322                    .try_join()
323                    .await?,
324            }))
325        } else {
326            Ok(Self::cell(Self::parse_ref(request)))
327        }
328    }
329
330    #[turbo_tasks::function]
331    pub fn raw(
332        request: Pattern,
333        query: RcStr,
334        fragment: RcStr,
335        force_in_lookup_dir: bool,
336    ) -> Vc<Self> {
337        Self::cell(Request::Raw {
338            path: request,
339            force_in_lookup_dir,
340            query,
341            fragment,
342        })
343    }
344
345    #[turbo_tasks::function]
346    pub fn relative(
347        request: Pattern,
348        query: RcStr,
349        fragment: RcStr,
350        force_in_lookup_dir: bool,
351    ) -> Vc<Self> {
352        Self::cell(Request::Relative {
353            path: request,
354            force_in_lookup_dir,
355            query,
356            fragment,
357        })
358    }
359
360    #[turbo_tasks::function]
361    pub fn module(module: RcStr, path: Pattern, query: RcStr, fragment: RcStr) -> Vc<Self> {
362        Self::cell(Request::Module {
363            module,
364            path,
365            query,
366            fragment,
367        })
368    }
369
370    #[turbo_tasks::function]
371    pub async fn as_relative(self: Vc<Self>) -> Result<Vc<Self>> {
372        let this = self.await?;
373        Ok(match &*this {
374            Request::Empty
375            | Request::Raw { .. }
376            | Request::ServerRelative { .. }
377            | Request::Windows { .. }
378            | Request::Relative { .. }
379            | Request::DataUri { .. }
380            | Request::Uri { .. }
381            | Request::Dynamic => self,
382            Request::Module {
383                module,
384                path,
385                query: _,
386                fragment: _,
387            } => {
388                let mut pat = Pattern::Constant(format!("./{module}").into());
389                pat.push(path.clone());
390                // TODO add query
391                Self::parse(pat)
392            }
393            Request::PackageInternal { path } => {
394                let mut pat = Pattern::Constant(rcstr!("./"));
395                pat.push(path.clone());
396                Self::parse(pat)
397            }
398            Request::Unknown { path } => {
399                let mut pat = Pattern::Constant(rcstr!("./"));
400                pat.push(path.clone());
401                Self::parse(pat)
402            }
403            Request::Alternatives { requests } => {
404                let requests = requests
405                    .iter()
406                    .copied()
407                    .map(|v| *v)
408                    .map(Request::as_relative)
409                    .map(|v| async move { v.to_resolved().await })
410                    .try_join()
411                    .await?;
412                Request::Alternatives { requests }.cell()
413            }
414        })
415    }
416
417    #[turbo_tasks::function]
418    pub async fn with_query(self: Vc<Self>, query: RcStr) -> Result<Vc<Self>> {
419        Ok(match &*self.await? {
420            Request::Raw {
421                path,
422                query: _,
423                force_in_lookup_dir,
424                fragment,
425            } => Request::raw(path.clone(), query, fragment.clone(), *force_in_lookup_dir),
426            Request::Relative {
427                path,
428                query: _,
429                force_in_lookup_dir,
430                fragment,
431            } => Request::relative(path.clone(), query, fragment.clone(), *force_in_lookup_dir),
432            Request::Module {
433                module,
434                path,
435                query: _,
436                fragment,
437            } => Request::module(module.clone(), path.clone(), query, fragment.clone()),
438            Request::ServerRelative {
439                path,
440                query: _,
441                fragment,
442            } => Request::ServerRelative {
443                path: path.clone(),
444                query,
445                fragment: fragment.clone(),
446            }
447            .cell(),
448            Request::Windows {
449                path,
450                query: _,
451                fragment,
452            } => Request::Windows {
453                path: path.clone(),
454                query,
455                fragment: fragment.clone(),
456            }
457            .cell(),
458            Request::Empty => self,
459            Request::PackageInternal { .. } => self,
460            Request::DataUri { .. } => self,
461            Request::Uri { .. } => self,
462            Request::Unknown { .. } => self,
463            Request::Dynamic => self,
464            Request::Alternatives { requests } => {
465                let requests = requests
466                    .iter()
467                    .copied()
468                    .map(|req| req.with_query(query.clone()))
469                    .map(|v| async move { v.to_resolved().await })
470                    .try_join()
471                    .await?;
472                Request::Alternatives { requests }.cell()
473            }
474        })
475    }
476
477    #[turbo_tasks::function]
478    pub async fn with_fragment(self: Vc<Self>, fragment: RcStr) -> Result<Vc<Self>> {
479        Ok(match &*self.await? {
480            Request::Raw {
481                path,
482                query,
483                force_in_lookup_dir,
484                fragment: _,
485            } => Request::Raw {
486                path: path.clone(),
487                query: query.clone(),
488                force_in_lookup_dir: *force_in_lookup_dir,
489                fragment,
490            }
491            .cell(),
492            Request::Relative {
493                path,
494                query,
495                force_in_lookup_dir,
496                fragment: _,
497            } => Request::Relative {
498                path: path.clone(),
499                query: query.clone(),
500                force_in_lookup_dir: *force_in_lookup_dir,
501                fragment,
502            }
503            .cell(),
504            Request::Module {
505                module,
506                path,
507                query,
508                fragment: _,
509            } => Request::Module {
510                module: module.clone(),
511                path: path.clone(),
512                query: query.clone(),
513                fragment,
514            }
515            .cell(),
516            Request::ServerRelative {
517                path,
518                query,
519                fragment: _,
520            } => Request::ServerRelative {
521                path: path.clone(),
522                query: query.clone(),
523                fragment,
524            }
525            .cell(),
526            Request::Windows {
527                path,
528                query,
529                fragment: _,
530            } => Request::Windows {
531                path: path.clone(),
532                query: query.clone(),
533                fragment,
534            }
535            .cell(),
536            Request::Empty => self,
537            Request::PackageInternal { .. } => self,
538            Request::DataUri { .. } => self,
539            Request::Uri { .. } => self,
540            Request::Unknown { .. } => self,
541            Request::Dynamic => self,
542            Request::Alternatives { requests } => {
543                let requests = requests
544                    .iter()
545                    .copied()
546                    .map(|req| req.with_fragment(fragment.clone()))
547                    .map(|v| async move { v.to_resolved().await })
548                    .try_join()
549                    .await?;
550                Request::Alternatives { requests }.cell()
551            }
552        })
553    }
554
555    #[turbo_tasks::function]
556    pub async fn append_path(self: Vc<Self>, suffix: RcStr) -> Result<Vc<Self>> {
557        Ok(match &*self.await? {
558            Request::Raw {
559                path,
560                query,
561                force_in_lookup_dir,
562                fragment,
563            } => {
564                let mut pat = Pattern::concat([path.clone(), suffix.into()]);
565                pat.normalize();
566                Self::raw(pat, query.clone(), fragment.clone(), *force_in_lookup_dir)
567            }
568            Request::Relative {
569                path,
570                query,
571                force_in_lookup_dir,
572                fragment,
573            } => {
574                let mut pat = Pattern::concat([path.clone(), suffix.into()]);
575                pat.normalize();
576                Self::relative(pat, query.clone(), fragment.clone(), *force_in_lookup_dir)
577            }
578            Request::Module {
579                module,
580                path,
581                query,
582                fragment,
583            } => {
584                let mut pat = Pattern::concat([path.clone(), suffix.into()]);
585                pat.normalize();
586                Self::module(module.clone(), pat, query.clone(), fragment.clone())
587            }
588            Request::ServerRelative {
589                path,
590                query,
591                fragment,
592            } => {
593                let mut pat = Pattern::concat([path.clone(), suffix.into()]);
594                pat.normalize();
595                Self::ServerRelative {
596                    path: pat,
597                    query: query.clone(),
598                    fragment: fragment.clone(),
599                }
600                .cell()
601            }
602            Request::Windows {
603                path,
604                query,
605                fragment,
606            } => {
607                let mut pat = Pattern::concat([path.clone(), suffix.into()]);
608                pat.normalize();
609                Self::Windows {
610                    path: pat,
611                    query: query.clone(),
612                    fragment: fragment.clone(),
613                }
614                .cell()
615            }
616            Request::Empty => Self::parse(suffix.into()),
617            Request::PackageInternal { path } => {
618                let mut pat = Pattern::concat([path.clone(), suffix.into()]);
619                pat.normalize();
620                Self::PackageInternal { path: pat }.cell()
621            }
622            Request::DataUri {
623                media_type,
624                encoding,
625                data,
626            } => {
627                let data = ResolvedVc::cell(format!("{}{}", data.await?, suffix).into());
628                Self::DataUri {
629                    media_type: media_type.clone(),
630                    encoding: encoding.clone(),
631                    data,
632                }
633                .cell()
634            }
635            Request::Uri {
636                protocol,
637                remainder,
638                query,
639                fragment,
640            } => {
641                let remainder = format!("{remainder}{suffix}").into();
642                Self::Uri {
643                    protocol: protocol.clone(),
644                    remainder,
645                    query: query.clone(),
646                    fragment: fragment.clone(),
647                }
648                .cell()
649            }
650            Request::Unknown { path } => {
651                let mut pat = Pattern::concat([path.clone(), suffix.into()]);
652                pat.normalize();
653                Self::Unknown { path: pat }.cell()
654            }
655            Request::Dynamic => self,
656            Request::Alternatives { requests } => {
657                let requests = requests
658                    .iter()
659                    .map(|req| async { req.append_path(suffix.clone()).to_resolved().await })
660                    .try_join()
661                    .await?;
662                Request::Alternatives { requests }.cell()
663            }
664        })
665    }
666
667    #[turbo_tasks::function]
668    pub fn query(&self) -> Vc<RcStr> {
669        Vc::cell(match self {
670            Request::Windows { query, .. }
671            | Request::ServerRelative { query, .. }
672            | Request::Module { query, .. }
673            | Request::Relative { query, .. }
674            | Request::Raw { query, .. } => query.clone(),
675            Request::Dynamic
676            | Request::Unknown { .. }
677            | Request::Uri { .. }
678            | Request::DataUri { .. }
679            | Request::PackageInternal { .. }
680            | Request::Empty => RcStr::default(),
681            // TODO: is this correct, should we return the first one instead?
682            Request::Alternatives { .. } => RcStr::default(),
683        })
684    }
685
686    /// Turns the request into a pattern, similar to [Request::request()] but
687    /// more complete.
688    #[turbo_tasks::function]
689    pub async fn request_pattern(self: Vc<Self>) -> Result<Vc<Pattern>> {
690        Ok(Pattern::new(match &*self.await? {
691            Request::Raw { path, .. } => path.clone(),
692            Request::Relative { path, .. } => path.clone(),
693            Request::Module { module, path, .. } => {
694                let mut path = path.clone();
695                path.push_front(Pattern::Constant(module.clone()));
696                path.normalize();
697                path
698            }
699            Request::ServerRelative { path, .. } => path.clone(),
700            Request::Windows { path, .. } => path.clone(),
701            Request::Empty => Pattern::Constant(rcstr!("")),
702            Request::PackageInternal { path } => path.clone(),
703            Request::DataUri {
704                media_type,
705                encoding,
706                data,
707            } => Pattern::Constant(
708                stringify_data_uri(media_type, encoding, *data)
709                    .await?
710                    .into(),
711            ),
712            Request::Uri {
713                protocol,
714                remainder,
715                ..
716            } => Pattern::Constant(format!("{protocol}{remainder}").into()),
717            Request::Unknown { path } => path.clone(),
718            Request::Dynamic => Pattern::Dynamic,
719            Request::Alternatives { requests } => Pattern::Alternatives(
720                requests
721                    .iter()
722                    .map(async |r: &ResolvedVc<Request>| -> Result<Pattern> {
723                        Ok(r.request_pattern().owned().await?)
724                    })
725                    .try_join()
726                    .await?,
727            ),
728        }))
729    }
730}
731
732#[turbo_tasks::value_impl]
733impl ValueToString for Request {
734    #[turbo_tasks::function]
735    async fn to_string(&self) -> Result<Vc<RcStr>> {
736        Ok(Vc::cell(match self {
737            Request::Raw {
738                path,
739                force_in_lookup_dir,
740                ..
741            } => {
742                if *force_in_lookup_dir {
743                    format!("in-lookup-dir {path}").into()
744                } else {
745                    format!("{path}").into()
746                }
747            }
748            Request::Relative {
749                path,
750                force_in_lookup_dir,
751                ..
752            } => {
753                if *force_in_lookup_dir {
754                    format!("relative-in-lookup-dir {path}").into()
755                } else {
756                    format!("relative {path}").into()
757                }
758            }
759            Request::Module { module, path, .. } => {
760                if path.could_match_others("") {
761                    format!("module \"{module}\" with subpath {path}").into()
762                } else {
763                    format!("module \"{module}\"").into()
764                }
765            }
766            Request::ServerRelative { path, .. } => format!("server relative {path}").into(),
767            Request::Windows { path, .. } => format!("windows {path}").into(),
768            Request::Empty => rcstr!("empty"),
769            Request::PackageInternal { path } => format!("package internal {path}").into(),
770            Request::DataUri {
771                media_type,
772                encoding,
773                data,
774            } => format!(
775                "data uri \"{media_type}\" \"{encoding}\" \"{}\"",
776                data.await?
777            )
778            .into(),
779            Request::Uri {
780                protocol,
781                remainder,
782                ..
783            } => format!("uri \"{protocol}\" \"{remainder}\"").into(),
784            Request::Unknown { path } => format!("unknown {path}").into(),
785            Request::Dynamic => rcstr!("dynamic"),
786            Request::Alternatives { requests } => {
787                let vec = requests.iter().map(|i| i.to_string()).try_join().await?;
788                vec.iter()
789                    .map(|r| r.as_str())
790                    .collect::<Vec<_>>()
791                    .join(" or ")
792                    .into()
793            }
794        }))
795    }
796}
797
798pub async fn stringify_data_uri(
799    media_type: &RcStr,
800    encoding: &RcStr,
801    data: ResolvedVc<RcStr>,
802) -> Result<String> {
803    Ok(format!(
804        "data:{media_type}{}{encoding},{}",
805        if encoding.is_empty() { "" } else { ";" },
806        data.await?
807    ))
808}
809
810#[cfg(test)]
811mod tests {
812    use super::*;
813
814    #[test]
815    fn test_split_query_fragment() {
816        assert_eq!(
817            (
818                Pattern::Constant(rcstr!("foo")),
819                RcStr::default(),
820                RcStr::default()
821            ),
822            split_off_query_fragment("foo")
823        );
824        // These two cases are a bit odd, but it is important to treat `import './foo?'` differently
825        // from `import './foo'`, ditto for fragments.
826        assert_eq!(
827            (
828                Pattern::Constant(rcstr!("foo")),
829                rcstr!("?"),
830                RcStr::default()
831            ),
832            split_off_query_fragment("foo?")
833        );
834        assert_eq!(
835            (
836                Pattern::Constant(rcstr!("foo")),
837                RcStr::default(),
838                rcstr!("#")
839            ),
840            split_off_query_fragment("foo#")
841        );
842        assert_eq!(
843            (
844                Pattern::Constant(rcstr!("foo")),
845                rcstr!("?bar=baz"),
846                RcStr::default()
847            ),
848            split_off_query_fragment("foo?bar=baz")
849        );
850        assert_eq!(
851            (
852                Pattern::Constant(rcstr!("foo")),
853                RcStr::default(),
854                rcstr!("#stuff?bar=baz")
855            ),
856            split_off_query_fragment("foo#stuff?bar=baz")
857        );
858
859        assert_eq!(
860            (
861                Pattern::Constant(rcstr!("foo")),
862                rcstr!("?bar=baz"),
863                rcstr!("#stuff")
864            ),
865            split_off_query_fragment("foo?bar=baz#stuff")
866        );
867    }
868}