parser.rs 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668
  1. // Copyright 2013-2014 Simon Sapin.
  2. //
  3. // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
  4. // http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
  5. // <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
  6. // option. This file may not be copied, modified, or distributed
  7. // except according to those terms.
  8. use std::ascii::StrAsciiExt;
  9. use encoding;
  10. use encoding::EncodingRef;
  11. use encoding::all::UTF_8;
  12. use super::{
  13. ParseResult, ErrorHandler, Url, RelativeSchemeData, OtherSchemeData,
  14. SchemeRelativeUrl, UserInfo, Host, Domain,
  15. utf8_percent_encode, percent_encode_byte,
  16. SimpleEncodeSet, DefaultEncodeSet, UserInfoEncodeSet};
  17. macro_rules! is_match(
  18. ($value:expr, $($pattern:pat)|+) => (
  19. match $value { $($pattern)|+ => true, _ => false }
  20. );
  21. )
  22. pub fn parse_url(input: &str, base_url: Option<&Url>, parse_error: ErrorHandler)
  23. -> ParseResult<Url> {
  24. let input = input.trim_chars(&[' ', '\t', '\n', '\r', '\x0C']);
  25. match parse_scheme(input) {
  26. Some((scheme, remaining)) => {
  27. if scheme.as_slice() == "file" {
  28. // Relative state?
  29. match base_url {
  30. Some(base) if scheme == base.scheme => {
  31. try!(parse_error("Relative URL with a scheme"));
  32. parse_relative_url(scheme, remaining, base, parse_error)
  33. },
  34. _ => parse_relative_url(scheme, remaining, &Url {
  35. scheme: String::new(), query: None, fragment: None,
  36. scheme_data: RelativeSchemeData(SchemeRelativeUrl {
  37. userinfo: None, host: Domain(String::new()),
  38. port: String::new(), path: Vec::new()
  39. })
  40. }, parse_error),
  41. }
  42. } else if is_relative_scheme(scheme.as_slice()) {
  43. match base_url {
  44. Some(base) if scheme == base.scheme => {
  45. // Relative or authority state
  46. if remaining.starts_with("//") {
  47. parse_absolute_url(scheme, remaining, parse_error)
  48. } else {
  49. try!(parse_error("Relative URL with a scheme"));
  50. parse_relative_url(scheme, remaining, base, parse_error)
  51. }
  52. },
  53. _ => parse_absolute_url(scheme, remaining, parse_error),
  54. }
  55. } else {
  56. // Scheme data state
  57. let (scheme_data, remaining) = try!(parse_scheme_data(remaining, parse_error));
  58. let (query, fragment) = try!(parse_query_and_fragment(remaining, parse_error));
  59. Ok(Url { scheme: scheme, scheme_data: OtherSchemeData(scheme_data),
  60. query: query, fragment: fragment })
  61. }
  62. },
  63. // No-scheme state
  64. None => match base_url {
  65. None => Err("Relative URL without a base"),
  66. Some(base) => parse_relative_url(base.scheme.clone(), input, base, parse_error)
  67. }
  68. }
  69. }
  70. fn parse_scheme<'a>(input: &'a str) -> Option<(String, &'a str)> {
  71. if !input.is_empty() && starts_with_ascii_alpha(input) {
  72. for (i, c) in input.char_indices() {
  73. match c {
  74. 'a'..'z' | 'A'..'Z' | '0'..'9' | '+' | '-' | '.' => (),
  75. ':' => return Some((
  76. input.slice_to(i).to_ascii_lower(),
  77. input.slice_from(i + 1),
  78. )),
  79. _ => break,
  80. }
  81. }
  82. }
  83. None
  84. }
  85. fn parse_absolute_url<'a>(scheme: String, input: &'a str, parse_error: ErrorHandler)
  86. -> ParseResult<Url> {
  87. // Authority first slash state
  88. let remaining = try!(skip_slashes(input, parse_error));
  89. // Authority state
  90. let (userinfo, remaining) = try!(parse_userinfo(remaining, parse_error));
  91. // Host state
  92. let (host, port, remaining) = try!(parse_hostname(remaining, scheme.as_slice(), parse_error));
  93. let (path, remaining) = try!(parse_path_start(
  94. remaining,
  95. /* full_url= */ true,
  96. /* in_file_scheme= */ false,
  97. parse_error));
  98. let scheme_data = RelativeSchemeData(SchemeRelativeUrl { userinfo: userinfo, host: host, port: port, path: path });
  99. let (query, fragment) = try!(parse_query_and_fragment(remaining, parse_error));
  100. Ok(Url { scheme: scheme, scheme_data: scheme_data, query: query, fragment: fragment })
  101. }
  102. fn parse_relative_url<'a>(scheme: String, input: &'a str, base: &Url, parse_error: ErrorHandler)
  103. -> ParseResult<Url> {
  104. match base.scheme_data {
  105. OtherSchemeData(_) => Err("Relative URL with a non-relative-scheme base"),
  106. RelativeSchemeData(ref base_scheme_data) => if input.is_empty() {
  107. Ok(Url { scheme: scheme, scheme_data: base.scheme_data.clone(),
  108. query: base.query.clone(), fragment: None })
  109. } else {
  110. let in_file_scheme = scheme.as_slice() == "file";
  111. match input.char_at(0) {
  112. '/' | '\\' => {
  113. // Relative slash state
  114. if input.len() > 1 && is_match!(input.char_at(1), '/' | '\\') {
  115. if in_file_scheme {
  116. let remaining = input.slice_from(2);
  117. let (host, remaining) = if remaining.len() >= 2
  118. && starts_with_ascii_alpha(remaining)
  119. && is_match!(remaining.char_at(1), ':' | '|')
  120. && (remaining.len() == 2
  121. || is_match!(remaining.char_at(2),
  122. '/' | '\\' | '?' | '#'))
  123. {
  124. // Windows drive letter quirk
  125. (Domain(String::new()), remaining)
  126. } else {
  127. // File host state
  128. try!(parse_file_host(remaining, parse_error))
  129. };
  130. let (path, remaining) = try!(parse_path_start(
  131. remaining, /* full_url= */ true,
  132. in_file_scheme, parse_error));
  133. let scheme_data = RelativeSchemeData(SchemeRelativeUrl {
  134. userinfo: None, host: host, port: String::new(), path: path });
  135. let (query, fragment) = try!(parse_query_and_fragment(
  136. remaining, parse_error));
  137. Ok(Url { scheme: scheme, scheme_data: scheme_data,
  138. query: query, fragment: fragment })
  139. } else {
  140. parse_absolute_url(scheme, input, parse_error)
  141. }
  142. } else {
  143. // Relative path state
  144. let (path, remaining) = try!(parse_path(
  145. Vec::new(), input.slice_from(1), /* full_url= */ true,
  146. in_file_scheme, parse_error));
  147. let scheme_data = RelativeSchemeData(if in_file_scheme {
  148. SchemeRelativeUrl {
  149. userinfo: None, host: Domain(String::new()),
  150. port: String::new(), path: path
  151. }
  152. } else {
  153. SchemeRelativeUrl {
  154. userinfo: base_scheme_data.userinfo.clone(),
  155. host: base_scheme_data.host.clone(),
  156. port: base_scheme_data.port.clone(),
  157. path: path
  158. }
  159. });
  160. let (query, fragment) = try!(
  161. parse_query_and_fragment(remaining, parse_error));
  162. Ok(Url { scheme: scheme, scheme_data: scheme_data,
  163. query: query, fragment: fragment })
  164. }
  165. },
  166. '?' => {
  167. let (query, fragment) = try!(parse_query_and_fragment(input, parse_error));
  168. Ok(Url { scheme: scheme, scheme_data: base.scheme_data.clone(),
  169. query: query, fragment: fragment })
  170. },
  171. '#' => {
  172. Ok(Url { scheme: scheme, scheme_data: base.scheme_data.clone(),
  173. query: base.query.clone(),
  174. fragment: Some(try!(
  175. parse_fragment(input.slice_from(1), parse_error))) })
  176. }
  177. _ => {
  178. let (scheme_data, remaining) = if in_file_scheme
  179. && input.len() >= 2
  180. && starts_with_ascii_alpha(input)
  181. && is_match!(input.char_at(1), ':' | '|')
  182. && (input.len() == 2
  183. || is_match!(input.char_at(2), '/' | '\\' | '?' | '#'))
  184. {
  185. // Windows drive letter quirk
  186. let (path, remaining) = try!(parse_path(
  187. Vec::new(), input, /* full_url= */ true,
  188. in_file_scheme, parse_error));
  189. (RelativeSchemeData(SchemeRelativeUrl {
  190. userinfo: None,
  191. host: Domain(String::new()),
  192. port: String::new(),
  193. path: path
  194. }), remaining)
  195. } else {
  196. let base_path = base_scheme_data.path.as_slice();
  197. let initial_path = Vec::from_slice(
  198. base_path.slice_to(base_path.len() - 1));
  199. // Relative path state
  200. let (path, remaining) = try!(parse_path(
  201. initial_path, input, /* full_url= */ true,
  202. in_file_scheme, parse_error));
  203. (RelativeSchemeData(SchemeRelativeUrl {
  204. userinfo: base_scheme_data.userinfo.clone(),
  205. host: base_scheme_data.host.clone(),
  206. port: base_scheme_data.port.clone(),
  207. path: path
  208. }), remaining)
  209. };
  210. let (query, fragment) = try!(parse_query_and_fragment(remaining, parse_error));
  211. Ok(Url { scheme: scheme, scheme_data: scheme_data,
  212. query: query, fragment: fragment })
  213. }
  214. }
  215. }
  216. }
  217. }
  218. fn skip_slashes<'a>(input: &'a str, parse_error: ErrorHandler) -> ParseResult<&'a str> {
  219. let first_non_slash = input.find(|c| !is_match!(c, '/' | '\\')).unwrap_or(input.len());
  220. if input.slice_to(first_non_slash) != "//" {
  221. try!(parse_error("Expected two slashes"));
  222. }
  223. Ok(input.slice_from(first_non_slash))
  224. }
  225. fn parse_userinfo<'a>(input: &'a str, parse_error: ErrorHandler)
  226. -> ParseResult<(Option<UserInfo>, &'a str)> {
  227. let mut last_at = None;
  228. for (i, c) in input.char_indices() {
  229. match c {
  230. '@' => last_at = Some(i),
  231. '/' | '\\' | '?' | '#' => break,
  232. _ => (),
  233. }
  234. }
  235. Ok(match last_at {
  236. None => (None, input),
  237. Some(at) => (Some(try!(parse_userinfo_inner(input.slice_to(at), parse_error))),
  238. input.slice_from(at + 1))
  239. })
  240. }
  241. fn parse_userinfo_inner(input: &str, parse_error: ErrorHandler) -> ParseResult<UserInfo> {
  242. let mut username = String::new();
  243. for (i, c) in input.char_indices() {
  244. match c {
  245. ':' => return parse_userinfo_password(input.slice_from(i + 1), username, parse_error),
  246. '\t' | '\n' | '\r' => try!(parse_error("Invalid character")),
  247. _ => {
  248. if c == '%' {
  249. if !starts_with_2_hex(input.slice_from(i + 1)) {
  250. try!(parse_error("Invalid percent-encoded sequence"));
  251. }
  252. } else if !is_url_code_point(c) {
  253. try!(parse_error("Non-URL code point"));
  254. }
  255. utf8_percent_encode(input.slice(i, i + c.len_utf8_bytes()),
  256. UserInfoEncodeSet, &mut username);
  257. }
  258. }
  259. }
  260. Ok(UserInfo { username: username, password: None })
  261. }
  262. fn parse_userinfo_password(input: &str, username: String, parse_error: ErrorHandler)
  263. -> ParseResult<UserInfo> {
  264. let mut password = String::new();
  265. for (i, c) in input.char_indices() {
  266. match c {
  267. '\t' | '\n' | '\r' => try!(parse_error("Invalid character")),
  268. _ => {
  269. if c == '%' {
  270. if !starts_with_2_hex(input.slice_from(i + 1)) {
  271. try!(parse_error("Invalid percent-encoded sequence"));
  272. }
  273. } else if !is_url_code_point(c) {
  274. try!(parse_error("Non-URL code point"));
  275. }
  276. utf8_percent_encode(input.slice(i, i + c.len_utf8_bytes()),
  277. UserInfoEncodeSet, &mut password);
  278. }
  279. }
  280. }
  281. Ok(UserInfo { username: username, password: Some(password) })
  282. }
  283. fn parse_hostname<'a>(input: &'a str, scheme: &str, parse_error: ErrorHandler)
  284. -> ParseResult<(Host, String, &'a str)> {
  285. let mut inside_square_brackets = false;
  286. let mut host_input = String::new();
  287. let mut end = input.len();
  288. for (i, c) in input.char_indices() {
  289. match c {
  290. ':' if !inside_square_brackets => {
  291. let host = try!(Host::parse(host_input.as_slice()));
  292. let (port, remaining) = try!(
  293. parse_port(input.slice_from(i + 1), scheme, parse_error));
  294. return Ok((host, port, remaining))
  295. },
  296. '/' | '\\' | '?' | '#' => {
  297. end = i;
  298. break
  299. },
  300. '\t' | '\n' | '\r' => try!(parse_error("Invalid character")),
  301. c => {
  302. match c {
  303. '[' => inside_square_brackets = true,
  304. ']' => inside_square_brackets = false,
  305. _ => (),
  306. }
  307. host_input.push_char(c)
  308. }
  309. }
  310. }
  311. let host = try!(Host::parse(host_input.as_slice()));
  312. Ok((host, String::new(), input.slice_from(end)))
  313. }
  314. fn parse_port<'a>(input: &'a str, scheme: &str, parse_error: ErrorHandler)
  315. -> ParseResult<(String, &'a str)> {
  316. let mut port = String::new();
  317. let mut has_initial_zero = false;
  318. let mut end = input.len();
  319. for (i, c) in input.char_indices() {
  320. match c {
  321. '1'..'9' => port.push_char(c),
  322. '0' => {
  323. if port.is_empty() {
  324. has_initial_zero = true
  325. } else {
  326. port.push_char(c)
  327. }
  328. },
  329. '/' | '\\' | '?' | '#' => {
  330. end = i;
  331. break
  332. },
  333. '\t' | '\n' | '\r' => try!(parse_error("Invalid character")),
  334. _ => return Err("Invalid port number")
  335. }
  336. }
  337. if port.is_empty() && has_initial_zero {
  338. port.push_str("0")
  339. }
  340. match (scheme, port.as_slice()) {
  341. ("ftp", "21") | ("gopher", "70") | ("http", "80") |
  342. ("https", "443") | ("ws", "80") | ("wss", "443")
  343. => port = String::new(),
  344. _ => (),
  345. }
  346. return Ok((port, input.slice_from(end)))
  347. }
  348. fn parse_file_host<'a>(input: &'a str, parse_error: ErrorHandler) -> ParseResult<(Host, &'a str)> {
  349. let mut host_input = String::new();
  350. let mut end = input.len();
  351. for (i, c) in input.char_indices() {
  352. match c {
  353. '/' | '\\' | '?' | '#' => {
  354. end = i;
  355. break
  356. },
  357. '\t' | '\n' | '\r' => try!(parse_error("Invalid character")),
  358. _ => host_input.push_char(c)
  359. }
  360. }
  361. let host = if host_input.is_empty() {
  362. Domain(String::new())
  363. } else {
  364. try!(Host::parse(host_input.as_slice()))
  365. };
  366. Ok((host, input.slice_from(end)))
  367. }
  368. fn parse_path_start<'a>(input: &'a str, full_url: bool, in_file_scheme: bool,
  369. parse_error: ErrorHandler)
  370. -> ParseResult<(Vec<String>, &'a str)> {
  371. let mut i = 0;
  372. // Relative path start state
  373. if !input.is_empty() {
  374. match input.char_at(0) {
  375. '/' => i = 1,
  376. '\\' => {
  377. try!(parse_error("Backslash"));
  378. i = 1;
  379. },
  380. _ => ()
  381. }
  382. }
  383. parse_path(Vec::new(), input.slice_from(i), full_url, in_file_scheme, parse_error)
  384. }
  385. fn parse_path<'a>(base_path: Vec<String>, input: &'a str, full_url: bool, in_file_scheme: bool,
  386. parse_error: ErrorHandler)
  387. -> ParseResult<(Vec<String>, &'a str)> {
  388. // Relative path state
  389. let mut path = base_path;
  390. let mut iter = input.char_indices();
  391. let mut end;
  392. loop {
  393. let mut path_part = String::new();
  394. let mut ends_with_slash = false;
  395. end = input.len();
  396. for (i, c) in iter {
  397. match c {
  398. '/' => {
  399. ends_with_slash = true;
  400. end = i;
  401. break
  402. },
  403. '\\' => {
  404. try!(parse_error("Backslash"));
  405. ends_with_slash = true;
  406. end = i;
  407. break
  408. },
  409. '?' | '#' if full_url => {
  410. end = i;
  411. break
  412. },
  413. '\t' | '\n' | '\r' => try!(parse_error("Invalid character")),
  414. _ => {
  415. if c == '%' {
  416. if !starts_with_2_hex(input.slice_from(i + 1)) {
  417. try!(parse_error("Invalid percent-encoded sequence"));
  418. }
  419. } else if !is_url_code_point(c) {
  420. try!(parse_error("Non-URL code point"));
  421. }
  422. utf8_percent_encode(input.slice(i, i + c.len_utf8_bytes()),
  423. DefaultEncodeSet, &mut path_part);
  424. }
  425. }
  426. }
  427. match path_part.as_slice() {
  428. ".." | ".%2e" | ".%2E" | "%2e." | "%2E." |
  429. "%2e%2e" | "%2E%2e" | "%2e%2E" | "%2E%2E" => {
  430. path.pop();
  431. if !ends_with_slash {
  432. path.push(String::new());
  433. }
  434. },
  435. "." | "%2e" | "%2E" => {
  436. if !ends_with_slash {
  437. path.push(String::new());
  438. }
  439. },
  440. _ => {
  441. if in_file_scheme
  442. && path.is_empty()
  443. && path_part.len() == 2
  444. && starts_with_ascii_alpha(path_part.as_slice())
  445. && path_part.as_slice().char_at(1) == '|' {
  446. // Windows drive letter quirk
  447. unsafe {
  448. *path_part.as_mut_vec().get_mut(1) = b':'
  449. }
  450. }
  451. path.push(path_part)
  452. }
  453. }
  454. if !ends_with_slash {
  455. break
  456. }
  457. }
  458. Ok((path, input.slice_from(end)))
  459. }
  460. fn parse_scheme_data<'a>(input: &'a str, parse_error: ErrorHandler)
  461. -> ParseResult<(String, &'a str)> {
  462. let mut scheme_data = String::new();
  463. let mut end = input.len();
  464. for (i, c) in input.char_indices() {
  465. match c {
  466. '?' | '#' => {
  467. end = i;
  468. break
  469. },
  470. '\t' | '\n' | '\r' => try!(parse_error("Invalid character")),
  471. _ => {
  472. if c == '%' {
  473. if !starts_with_2_hex(input.slice_from(i + 1)) {
  474. try!(parse_error("Invalid percent-encoded sequence"));
  475. }
  476. } else if !is_url_code_point(c) {
  477. try!(parse_error("Non-URL code point"));
  478. }
  479. utf8_percent_encode(input.slice(i, i + c.len_utf8_bytes()),
  480. SimpleEncodeSet, &mut scheme_data);
  481. }
  482. }
  483. }
  484. Ok((scheme_data, input.slice_from(end)))
  485. }
  486. fn parse_query_and_fragment(input: &str, parse_error: ErrorHandler)
  487. -> ParseResult<(Option<String>, Option<String>)> {
  488. Ok(if input.is_empty() {
  489. (None, None)
  490. } else {
  491. match input.char_at(0) {
  492. '#' => (None, Some(try!(parse_fragment(input.slice_from(1), parse_error)))),
  493. '?' => {
  494. let (query, remaining) = try!(parse_query(
  495. input.slice_from(1),
  496. UTF_8 as EncodingRef, // TODO
  497. /* full_url = */ true,
  498. parse_error));
  499. (Some(query), match remaining {
  500. Some(remaining) => Some(try!(parse_fragment(remaining, parse_error))),
  501. None => None
  502. })
  503. },
  504. _ => fail!("Programming error")
  505. }
  506. })
  507. }
  508. fn parse_query<'a>(input: &'a str, encoding_override: EncodingRef, full_url: bool,
  509. parse_error: ErrorHandler)
  510. -> ParseResult<(String, Option<&'a str>)> {
  511. let mut query = String::new();
  512. let mut remaining = None;
  513. for (i, c) in input.char_indices() {
  514. match c {
  515. '#' if full_url => {
  516. remaining = Some(input.slice_from(i + 1));
  517. break
  518. },
  519. '\t' | '\n' | '\r' => try!(parse_error("Invalid character")),
  520. _ => {
  521. if c == '%' {
  522. if !starts_with_2_hex(input.slice_from(i + 1)) {
  523. try!(parse_error("Invalid percent-encoded sequence"));
  524. }
  525. } else if !is_url_code_point(c) {
  526. try!(parse_error("Non-URL code point"));
  527. }
  528. query.push_char(c);
  529. }
  530. }
  531. }
  532. let query_bytes = encoding_override.encode(query.as_slice(), encoding::EncodeReplace).unwrap();
  533. let mut query_encoded = String::new();
  534. for &byte in query_bytes.iter() {
  535. match byte {
  536. b'\x00'.. b' ' | b'"' | b'#' | b'<' | b'>' | b'`' | b'~'..b'\xFF'
  537. => percent_encode_byte(byte, &mut query_encoded),
  538. _
  539. => unsafe { query_encoded.push_byte(byte) }
  540. }
  541. }
  542. Ok((query_encoded, remaining))
  543. }
  544. fn parse_fragment<'a>(input: &'a str, parse_error: ErrorHandler) -> ParseResult<String> {
  545. let mut fragment = String::new();
  546. for (i, c) in input.char_indices() {
  547. match c {
  548. '\t' | '\n' | '\r' => try!(parse_error("Invalid character")),
  549. _ => {
  550. if c == '%' {
  551. if !starts_with_2_hex(input.slice_from(i + 1)) {
  552. try!(parse_error("Invalid percent-encoded sequence"));
  553. }
  554. } else if !is_url_code_point(c) {
  555. try!(parse_error("Non-URL code point"));
  556. }
  557. utf8_percent_encode(input.slice(i, i + c.len_utf8_bytes()),
  558. SimpleEncodeSet, &mut fragment);
  559. }
  560. }
  561. }
  562. Ok(fragment)
  563. }
  564. #[inline]
  565. fn starts_with_ascii_alpha(string: &str) -> bool {
  566. match string.char_at(0) {
  567. 'a'..'z' | 'A'..'Z' => true,
  568. _ => false,
  569. }
  570. }
  571. #[inline]
  572. fn is_ascii_hex_digit(byte: u8) -> bool {
  573. match byte {
  574. b'a'..b'f' | b'A'..b'F' | b'0'..b'9' => true,
  575. _ => false,
  576. }
  577. }
  578. #[inline]
  579. fn starts_with_2_hex(input: &str) -> bool {
  580. input.len() >= 2
  581. && is_ascii_hex_digit(input.as_bytes()[0])
  582. && is_ascii_hex_digit(input.as_bytes()[1])
  583. }
  584. #[inline]
  585. fn is_url_code_point(c: char) -> bool {
  586. match c {
  587. 'a'..'z' |
  588. 'A'..'Z' |
  589. '0'..'9' |
  590. '!' | '$' | '&' | '\'' | '(' | ')' | '*' | '+' | ',' | '-' |
  591. '.' | '/' | ':' | ';' | '=' | '?' | '@' | '_' | '~' |
  592. '\u00A0'..'\uD7FF' | '\uE000'..'\uFDCF' | '\uFDF0'..'\uFFEF' |
  593. '\U00010000'..'\U0001FFFD' | '\U00020000'..'\U0002FFFD' |
  594. '\U00030000'..'\U0003FFFD' | '\U00040000'..'\U0004FFFD' |
  595. '\U00050000'..'\U0005FFFD' | '\U00060000'..'\U0006FFFD' |
  596. '\U00070000'..'\U0007FFFD' | '\U00080000'..'\U0008FFFD' |
  597. '\U00090000'..'\U0009FFFD' | '\U000A0000'..'\U000AFFFD' |
  598. '\U000B0000'..'\U000BFFFD' | '\U000C0000'..'\U000CFFFD' |
  599. '\U000D0000'..'\U000DFFFD' | '\U000E1000'..'\U000EFFFD' |
  600. '\U000F0000'..'\U000FFFFD' | '\U00100000'..'\U0010FFFD' => true,
  601. _ => false
  602. }
  603. }
  604. // Non URL code points:
  605. // U+0000 to U+0020 (space)
  606. // " # % < > [ \ ] ^ ` { | }
  607. // U+007F to U+009F
  608. // surrogates
  609. // U+FDD0 to U+FDEF
  610. // U+FFF0 to U+FFFF
  611. // Last two of each plane: U+__FFFE to U+__FFFF for __ in 01 to 10 hex
  612. fn is_relative_scheme(scheme: &str) -> bool {
  613. is_match!(scheme, "ftp" | "file" | "gopher" | "http" | "https" | "ws" | "wss")
  614. }