urlpattern/
quirks.rs

1//! This module contains functions required to integrate this library into
2//! browsers. If you are not building a browser, you can ignore this module.
3
4use serde::Deserialize;
5use serde::Serialize;
6use url::Url;
7
8use crate::component::Component;
9use crate::parser::RegexSyntax;
10use crate::regexp::RegExp;
11pub use crate::Error;
12use crate::UrlPatternOptions;
13
14#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
15pub struct UrlPatternInit {
16  #[serde(skip_serializing_if = "Option::is_none")]
17  pub protocol: Option<String>,
18  #[serde(skip_serializing_if = "Option::is_none")]
19  pub username: Option<String>,
20  #[serde(skip_serializing_if = "Option::is_none")]
21  pub password: Option<String>,
22  #[serde(skip_serializing_if = "Option::is_none")]
23  pub hostname: Option<String>,
24  #[serde(skip_serializing_if = "Option::is_none")]
25  pub port: Option<String>,
26  #[serde(skip_serializing_if = "Option::is_none")]
27  pub pathname: Option<String>,
28  #[serde(skip_serializing_if = "Option::is_none")]
29  pub search: Option<String>,
30  #[serde(skip_serializing_if = "Option::is_none")]
31  pub hash: Option<String>,
32  #[serde(rename = "baseURL", skip_serializing_if = "Option::is_none")]
33  pub base_url: Option<String>,
34}
35
36#[allow(clippy::large_enum_variant)]
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(untagged)]
39pub enum StringOrInit {
40  String(String),
41  Init(UrlPatternInit),
42}
43
44/// This function constructs a UrlPattern given a string or UrlPatternInit and
45/// optionally a base url.
46pub fn process_construct_pattern_input(
47  input: StringOrInit,
48  base_url: Option<&str>,
49) -> Result<crate::UrlPatternInit, Error> {
50  let init = match input {
51    StringOrInit::String(pattern) => {
52      let base_url =
53        base_url.map(Url::parse).transpose().map_err(Error::Url)?;
54      crate::UrlPatternInit::parse_constructor_string::<regex::Regex>(
55        &pattern, base_url,
56      )?
57    }
58    StringOrInit::Init(init) => {
59      if base_url.is_some() {
60        return Err(Error::BaseUrlWithInit);
61      }
62      let base_url = init
63        .base_url
64        .map(|s| Url::parse(&s))
65        .transpose()
66        .map_err(Error::Url)?;
67      crate::UrlPatternInit {
68        protocol: init.protocol,
69        username: init.username,
70        password: init.password,
71        hostname: init.hostname,
72        port: init.port,
73        pathname: init.pathname,
74        search: init.search,
75        hash: init.hash,
76        base_url,
77      }
78    }
79  };
80  Ok(init)
81}
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
83#[serde(rename_all = "camelCase")]
84pub struct UrlPattern {
85  pub protocol: UrlPatternComponent,
86  pub username: UrlPatternComponent,
87  pub password: UrlPatternComponent,
88  pub hostname: UrlPatternComponent,
89  pub port: UrlPatternComponent,
90  pub pathname: UrlPatternComponent,
91  pub search: UrlPatternComponent,
92  pub hash: UrlPatternComponent,
93  pub has_regexp_groups: bool,
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
97#[serde(rename_all = "camelCase")]
98pub struct UrlPatternComponent {
99  pub pattern_string: String,
100  pub regexp_string: String,
101  pub matcher: Matcher,
102  pub group_name_list: Vec<String>,
103}
104
105impl From<Component<EcmaRegexp>> for UrlPatternComponent {
106  fn from(component: Component<EcmaRegexp>) -> Self {
107    Self {
108      pattern_string: component.pattern_string,
109      regexp_string: component.regexp.unwrap().0,
110      matcher: component.matcher.into(),
111      group_name_list: component.group_name_list,
112    }
113  }
114}
115
116#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
117#[serde(rename_all = "camelCase")]
118pub struct Matcher {
119  pub prefix: String,
120  pub suffix: String,
121  #[serde(flatten)]
122  pub inner: InnerMatcher,
123}
124
125#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
126#[serde(rename_all = "camelCase", tag = "kind")]
127pub enum InnerMatcher {
128  Literal {
129    literal: String,
130  },
131  #[serde(rename_all = "camelCase")]
132  SingleCapture {
133    filter: Option<char>,
134    allow_empty: bool,
135  },
136  RegExp {
137    regexp: String,
138  },
139}
140
141impl From<crate::matcher::Matcher<EcmaRegexp>> for Matcher {
142  fn from(matcher: crate::matcher::Matcher<EcmaRegexp>) -> Self {
143    Self {
144      prefix: matcher.prefix,
145      suffix: matcher.suffix,
146      inner: matcher.inner.into(),
147    }
148  }
149}
150
151impl From<crate::matcher::InnerMatcher<EcmaRegexp>> for InnerMatcher {
152  fn from(inner: crate::matcher::InnerMatcher<EcmaRegexp>) -> Self {
153    match inner {
154      crate::matcher::InnerMatcher::Literal { literal } => {
155        Self::Literal { literal }
156      }
157      crate::matcher::InnerMatcher::SingleCapture {
158        filter,
159        allow_empty,
160      } => Self::SingleCapture {
161        filter,
162        allow_empty,
163      },
164      crate::matcher::InnerMatcher::RegExp { regexp } => Self::RegExp {
165        regexp: regexp.unwrap().0,
166      },
167    }
168  }
169}
170
171struct EcmaRegexp(String, String);
172
173impl RegExp for EcmaRegexp {
174  fn syntax() -> RegexSyntax {
175    RegexSyntax::EcmaScript
176  }
177
178  fn parse(pattern: &str, flags: &str) -> Result<Self, ()> {
179    Ok(EcmaRegexp(pattern.to_string(), flags.to_string()))
180  }
181
182  fn matches<'a>(&self, text: &'a str) -> Option<Vec<Option<&'a str>>> {
183    let regexp = regex::Regex::parse(&self.0, &self.1).ok()?;
184    regexp.matches(text)
185  }
186}
187
188/// Parse a pattern into its components.
189pub fn parse_pattern(
190  init: crate::UrlPatternInit,
191  options: UrlPatternOptions,
192) -> Result<UrlPattern, Error> {
193  let pattern =
194    crate::UrlPattern::<EcmaRegexp>::parse_internal(init, false, options)?;
195  let urlpattern = UrlPattern {
196    has_regexp_groups: pattern.has_regexp_groups(),
197    protocol: pattern.protocol.into(),
198    username: pattern.username.into(),
199    password: pattern.password.into(),
200    hostname: pattern.hostname.into(),
201    port: pattern.port.into(),
202    pathname: pattern.pathname.into(),
203    search: pattern.search.into(),
204    hash: pattern.hash.into(),
205  };
206  Ok(urlpattern)
207}
208
209pub type Inputs = (StringOrInit, Option<String>);
210
211pub fn process_match_input(
212  input: StringOrInit,
213  base_url_str: Option<&str>,
214) -> Result<Option<(crate::UrlPatternMatchInput, Inputs)>, Error> {
215  let mut inputs = (input.clone(), None);
216  let init = match input {
217    StringOrInit::String(url) => {
218      let base_url = if let Some(base_url_str) = base_url_str {
219        match Url::parse(base_url_str) {
220          Ok(base_url) => {
221            inputs.1 = Some(base_url_str.to_string());
222            Some(base_url)
223          }
224          Err(_) => return Ok(None),
225        }
226      } else {
227        None
228      };
229      match Url::options().base_url(base_url.as_ref()).parse(&url) {
230        Ok(url) => crate::UrlPatternMatchInput::Url(url),
231        Err(_) => return Ok(None),
232      }
233    }
234    StringOrInit::Init(init) => {
235      if base_url_str.is_some() {
236        return Err(Error::BaseUrlWithInit);
237      }
238      let base_url = match init.base_url.map(|s| Url::parse(&s)).transpose() {
239        Ok(base_url) => base_url,
240        Err(_) => return Ok(None),
241      };
242      let init = crate::UrlPatternInit {
243        protocol: init.protocol,
244        username: init.username,
245        password: init.password,
246        hostname: init.hostname,
247        port: init.port,
248        pathname: init.pathname,
249        search: init.search,
250        hash: init.hash,
251        base_url,
252      };
253      crate::UrlPatternMatchInput::Init(init)
254    }
255  };
256
257  Ok(Some((init, inputs)))
258}
259
260#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
261pub struct MatchInput {
262  pub protocol: String,
263  pub username: String,
264  pub password: String,
265  pub hostname: String,
266  pub port: String,
267  pub pathname: String,
268  pub search: String,
269  pub hash: String,
270}
271
272pub fn parse_match_input(
273  input: crate::UrlPatternMatchInput,
274) -> Option<MatchInput> {
275  let mut i = MatchInput::default();
276  match input {
277    crate::UrlPatternMatchInput::Init(init) => {
278      if let Ok(apply_result) = init.process(
279        crate::canonicalize_and_process::ProcessType::Url,
280        Some(i.protocol),
281        Some(i.username),
282        Some(i.password),
283        Some(i.hostname),
284        Some(i.port),
285        Some(i.pathname),
286        Some(i.search),
287        Some(i.hash),
288      ) {
289        i.protocol = apply_result.protocol.unwrap();
290        i.username = apply_result.username.unwrap();
291        i.password = apply_result.password.unwrap();
292        i.hostname = apply_result.hostname.unwrap();
293        i.port = apply_result.port.unwrap();
294        i.pathname = apply_result.pathname.unwrap();
295        i.search = apply_result.search.unwrap();
296        i.hash = apply_result.hash.unwrap();
297      } else {
298        return None;
299      }
300    }
301    crate::UrlPatternMatchInput::Url(url) => {
302      i.protocol = url.scheme().to_string();
303      i.username = url.username().to_string();
304      i.password = url.password().unwrap_or_default().to_string();
305      i.hostname = url.host_str().unwrap_or_default().to_string();
306      i.port = url::quirks::port(&url).to_string();
307      i.pathname = url::quirks::pathname(&url).to_string();
308      i.search = url.query().unwrap_or_default().to_string();
309      i.hash = url.fragment().unwrap_or_default().to_string();
310    }
311  }
312
313  Some(i)
314}