parser.rs 23 KB

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