nodejs_resolver/
parse.rs

1use crate::kind::PathKind;
2use crate::Resolver;
3
4#[derive(Clone, Debug)]
5pub struct Request {
6    target: Box<str>,
7    query: Option<Box<str>>,
8    fragment: Option<Box<str>>,
9    kind: PathKind,
10    is_directory: bool,
11}
12
13impl Default for Request {
14    fn default() -> Self {
15        Self {
16            target: "".into(),
17            query: None,
18            fragment: None,
19            kind: PathKind::Relative,
20            is_directory: false,
21        }
22    }
23}
24
25impl std::fmt::Display for Request {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        write!(f, "{}{}{}", self.target(), self.query(), self.fragment())
28    }
29}
30
31impl Request {
32    #[must_use]
33    pub fn from_request(request: &str) -> Self {
34        let (target, query, fragment) = Self::parse_identifier(request);
35        let is_directory = Self::is_target_directory(&target);
36        let target = if is_directory {
37            target[0..target.len() - 1].into()
38        } else {
39            target
40        };
41        Request {
42            kind: Resolver::get_target_kind(&target),
43            target,
44            query,
45            fragment,
46            is_directory,
47        }
48    }
49
50    pub fn target(&self) -> &str {
51        &self.target
52    }
53
54    pub fn query(&self) -> &str {
55        self.query.as_ref().map_or("", |query| query.as_ref())
56    }
57
58    pub fn fragment(&self) -> &str {
59        self.fragment
60            .as_ref()
61            .map_or("", |fragment| fragment.as_ref())
62    }
63
64    pub fn kind(&self) -> PathKind {
65        self.kind
66    }
67
68    pub fn is_directory(&self) -> bool {
69        self.is_directory
70    }
71
72    pub fn with_target(self, target: &str) -> Self {
73        let is_directory = Self::is_target_directory(target);
74        Self {
75            kind: Resolver::get_target_kind(target),
76            target: target.into(),
77            is_directory,
78            ..self
79        }
80    }
81
82    pub fn with_query(self, query: &str) -> Self {
83        Self {
84            query: (!query.is_empty()).then(|| query.into()),
85            ..self
86        }
87    }
88
89    pub fn with_fragment(self, fragment: &str) -> Self {
90        Self {
91            fragment: (!fragment.is_empty()).then(|| fragment.into()),
92            ..self
93        }
94    }
95
96    fn parse_identifier(ident: &str) -> (Box<str>, Option<Box<str>>, Option<Box<str>>) {
97        let mut query: Option<usize> = None;
98        let mut fragment: Option<usize> = None;
99        let mut stats = ParseStats::Start;
100        for (index, c) in ident.as_bytes().iter().enumerate() {
101            match c {
102                b'#' => match stats {
103                    ParseStats::Request | ParseStats::Query => {
104                        stats = ParseStats::Fragment;
105                        fragment = Some(index);
106                    }
107                    ParseStats::Start => {
108                        stats = ParseStats::Request;
109                    }
110                    ParseStats::Fragment => (),
111                },
112                b'?' => match stats {
113                    ParseStats::Request | ParseStats::Query | ParseStats::Start => {
114                        stats = ParseStats::Query;
115                        query = Some(index);
116                    }
117                    ParseStats::Fragment => (),
118                },
119                _ => {
120                    if let ParseStats::Start = stats {
121                        stats = ParseStats::Request;
122                    }
123                }
124            }
125        }
126
127        match (query, fragment) {
128            (None, None) => (ident.into(), None, None),
129            (None, Some(j)) => (ident[0..j].into(), None, Some(ident[j..].into())),
130            (Some(i), None) => (ident[0..i].into(), Some(ident[i..].into()), None),
131            (Some(i), Some(j)) => (
132                ident[0..i].into(),
133                Some(ident[i..j].into()),
134                Some(ident[j..].into()),
135            ),
136        }
137    }
138
139    #[inline]
140    fn is_target_directory(target: &str) -> bool {
141        target.ends_with('/')
142    }
143}
144
145impl Resolver {
146    #[must_use]
147    pub(crate) fn parse(request: &str) -> Request {
148        Request::from_request(request)
149    }
150}
151
152enum ParseStats {
153    Request,
154    Query,
155    Fragment,
156    Start,
157}
158
159#[test]
160fn parse_identifier_test() {
161    fn should_parsed(input: &str, t: &str, q: &str, f: &str) {
162        let (target, query, fragment) = Request::parse_identifier(input);
163        assert_eq!(&*target, t);
164        assert_eq!(query.as_ref().map_or("", |q| q.as_ref()), q);
165        assert_eq!(fragment.as_ref().map_or("", |f| f.as_ref()), f);
166    }
167
168    should_parsed("path/abc", "path/abc", "", "");
169    should_parsed("path/#", "path/", "", "#");
170    should_parsed("path/as/?", "path/as/", "?", "");
171    should_parsed("path/#/?", "path/", "", "#/?");
172    should_parsed("path/#repo#hash", "path/", "", "#repo#hash");
173    should_parsed("path/#r#hash", "path/", "", "#r#hash");
174    should_parsed("path/#repo/#repo2#hash", "path/", "", "#repo/#repo2#hash");
175    should_parsed("path/#r/#r#hash", "path/", "", "#r/#r#hash");
176    should_parsed(
177        "path/#/not/a/hash?not-a-query",
178        "path/",
179        "",
180        "#/not/a/hash?not-a-query",
181    );
182    should_parsed("#a?b#c?d", "#a", "?b", "#c?d");
183
184    // windows like
185    should_parsed("path\\#", "path\\", "", "#");
186    should_parsed("C:path\\as\\?", "C:path\\as\\", "?", "");
187    should_parsed("path\\#\\?", "path\\", "", "#\\?");
188    should_parsed("path\\#repo#hash", "path\\", "", "#repo#hash");
189    should_parsed("path\\#r#hash", "path\\", "", "#r#hash");
190    should_parsed(
191        "path\\#/not/a/hash?not-a-query",
192        "path\\",
193        "",
194        "#/not/a/hash?not-a-query",
195    );
196}