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 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}