uts46.rs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726
  1. // Copyright 2013-2014 The rust-url developers.
  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. //! [*Unicode IDNA Compatibility Processing*
  9. //! (Unicode Technical Standard #46)](http://www.unicode.org/reports/tr46/)
  10. use self::Mapping::*;
  11. use crate::punycode;
  12. use std::{error::Error as StdError, fmt};
  13. use unicode_bidi::{bidi_class, BidiClass};
  14. use unicode_normalization::char::is_combining_mark;
  15. use unicode_normalization::{is_nfc, UnicodeNormalization};
  16. include!("uts46_mapping_table.rs");
  17. const PUNYCODE_PREFIX: &str = "xn--";
  18. #[derive(Debug)]
  19. struct StringTableSlice {
  20. // Store these as separate fields so the structure will have an
  21. // alignment of 1 and thus pack better into the Mapping enum, below.
  22. byte_start_lo: u8,
  23. byte_start_hi: u8,
  24. byte_len: u8,
  25. }
  26. fn decode_slice(slice: &StringTableSlice) -> &'static str {
  27. let lo = slice.byte_start_lo as usize;
  28. let hi = slice.byte_start_hi as usize;
  29. let start = (hi << 8) | lo;
  30. let len = slice.byte_len as usize;
  31. &STRING_TABLE[start..(start + len)]
  32. }
  33. #[repr(u8)]
  34. #[derive(Debug)]
  35. enum Mapping {
  36. Valid,
  37. Ignored,
  38. Mapped(StringTableSlice),
  39. Deviation(StringTableSlice),
  40. Disallowed,
  41. DisallowedStd3Valid,
  42. DisallowedStd3Mapped(StringTableSlice),
  43. DisallowedIdna2008,
  44. }
  45. fn find_char(codepoint: char) -> &'static Mapping {
  46. let idx = match TABLE.binary_search_by_key(&codepoint, |&val| val.0) {
  47. Ok(idx) => idx,
  48. Err(idx) => idx - 1,
  49. };
  50. const SINGLE_MARKER: u16 = 1 << 15;
  51. let (base, x) = TABLE[idx];
  52. let single = (x & SINGLE_MARKER) != 0;
  53. let offset = !SINGLE_MARKER & x;
  54. if single {
  55. &MAPPING_TABLE[offset as usize]
  56. } else {
  57. &MAPPING_TABLE[(offset + (codepoint as u16 - base as u16)) as usize]
  58. }
  59. }
  60. struct Mapper<'a> {
  61. chars: std::str::Chars<'a>,
  62. config: Config,
  63. errors: &'a mut Errors,
  64. slice: Option<std::str::Chars<'static>>,
  65. }
  66. impl<'a> Iterator for Mapper<'a> {
  67. type Item = char;
  68. fn next(&mut self) -> Option<Self::Item> {
  69. loop {
  70. if let Some(s) = &mut self.slice {
  71. match s.next() {
  72. Some(c) => return Some(c),
  73. None => {
  74. self.slice = None;
  75. }
  76. }
  77. }
  78. let codepoint = self.chars.next()?;
  79. if let '.' | '-' | 'a'..='z' | '0'..='9' = codepoint {
  80. return Some(codepoint);
  81. }
  82. return Some(match *find_char(codepoint) {
  83. Mapping::Valid => codepoint,
  84. Mapping::Ignored => continue,
  85. Mapping::Mapped(ref slice) => {
  86. self.slice = Some(decode_slice(slice).chars());
  87. continue;
  88. }
  89. Mapping::Deviation(ref slice) => {
  90. if self.config.transitional_processing {
  91. self.slice = Some(decode_slice(slice).chars());
  92. continue;
  93. } else {
  94. codepoint
  95. }
  96. }
  97. Mapping::Disallowed => {
  98. self.errors.disallowed_character = true;
  99. codepoint
  100. }
  101. Mapping::DisallowedStd3Valid => {
  102. if self.config.use_std3_ascii_rules {
  103. self.errors.disallowed_by_std3_ascii_rules = true;
  104. };
  105. codepoint
  106. }
  107. Mapping::DisallowedStd3Mapped(ref slice) => {
  108. if self.config.use_std3_ascii_rules {
  109. self.errors.disallowed_mapped_in_std3 = true;
  110. };
  111. self.slice = Some(decode_slice(slice).chars());
  112. continue;
  113. }
  114. Mapping::DisallowedIdna2008 => {
  115. if self.config.use_idna_2008_rules {
  116. self.errors.disallowed_in_idna_2008 = true;
  117. }
  118. codepoint
  119. }
  120. });
  121. }
  122. }
  123. }
  124. // http://tools.ietf.org/html/rfc5893#section-2
  125. fn passes_bidi(label: &str, is_bidi_domain: bool) -> bool {
  126. // Rule 0: Bidi Rules apply to Bidi Domain Names: a name with at least one RTL label. A label
  127. // is RTL if it contains at least one character of bidi class R, AL or AN.
  128. if !is_bidi_domain {
  129. return true;
  130. }
  131. let mut chars = label.chars();
  132. let first_char_class = match chars.next() {
  133. Some(c) => bidi_class(c),
  134. None => return true, // empty string
  135. };
  136. match first_char_class {
  137. // LTR label
  138. BidiClass::L => {
  139. // Rule 5
  140. for c in chars.by_ref() {
  141. if !matches!(
  142. bidi_class(c),
  143. BidiClass::L
  144. | BidiClass::EN
  145. | BidiClass::ES
  146. | BidiClass::CS
  147. | BidiClass::ET
  148. | BidiClass::ON
  149. | BidiClass::BN
  150. | BidiClass::NSM
  151. ) {
  152. return false;
  153. }
  154. }
  155. // Rule 6
  156. // must end in L or EN followed by 0 or more NSM
  157. let mut rev_chars = label.chars().rev();
  158. let mut last_non_nsm = rev_chars.next();
  159. loop {
  160. match last_non_nsm {
  161. Some(c) if bidi_class(c) == BidiClass::NSM => {
  162. last_non_nsm = rev_chars.next();
  163. continue;
  164. }
  165. _ => {
  166. break;
  167. }
  168. }
  169. }
  170. match last_non_nsm {
  171. Some(c) if bidi_class(c) == BidiClass::L || bidi_class(c) == BidiClass::EN => {}
  172. Some(_) => {
  173. return false;
  174. }
  175. _ => {}
  176. }
  177. }
  178. // RTL label
  179. BidiClass::R | BidiClass::AL => {
  180. let mut found_en = false;
  181. let mut found_an = false;
  182. // Rule 2
  183. for c in chars {
  184. let char_class = bidi_class(c);
  185. if char_class == BidiClass::EN {
  186. found_en = true;
  187. } else if char_class == BidiClass::AN {
  188. found_an = true;
  189. }
  190. if !matches!(
  191. char_class,
  192. BidiClass::R
  193. | BidiClass::AL
  194. | BidiClass::AN
  195. | BidiClass::EN
  196. | BidiClass::ES
  197. | BidiClass::CS
  198. | BidiClass::ET
  199. | BidiClass::ON
  200. | BidiClass::BN
  201. | BidiClass::NSM
  202. ) {
  203. return false;
  204. }
  205. }
  206. // Rule 3
  207. let mut rev_chars = label.chars().rev();
  208. let mut last = rev_chars.next();
  209. loop {
  210. // must end in L or EN followed by 0 or more NSM
  211. match last {
  212. Some(c) if bidi_class(c) == BidiClass::NSM => {
  213. last = rev_chars.next();
  214. continue;
  215. }
  216. _ => {
  217. break;
  218. }
  219. }
  220. }
  221. match last {
  222. Some(c)
  223. if matches!(
  224. bidi_class(c),
  225. BidiClass::R | BidiClass::AL | BidiClass::EN | BidiClass::AN
  226. ) => {}
  227. _ => {
  228. return false;
  229. }
  230. }
  231. // Rule 4
  232. if found_an && found_en {
  233. return false;
  234. }
  235. }
  236. // Rule 1: Should start with L or R/AL
  237. _ => {
  238. return false;
  239. }
  240. }
  241. true
  242. }
  243. /// Check the validity criteria for the given label
  244. ///
  245. /// V1 (NFC) and V8 (Bidi) are checked inside `processing()` to prevent doing duplicate work.
  246. ///
  247. /// http://www.unicode.org/reports/tr46/#Validity_Criteria
  248. fn check_validity(label: &str, config: Config, errors: &mut Errors) {
  249. let first_char = label.chars().next();
  250. if first_char == None {
  251. // Empty string, pass
  252. return;
  253. }
  254. // V2: No U+002D HYPHEN-MINUS in both third and fourth positions.
  255. //
  256. // NOTE: Spec says that the label must not contain a HYPHEN-MINUS character in both the
  257. // third and fourth positions. But nobody follows this criteria. See the spec issue below:
  258. // https://github.com/whatwg/url/issues/53
  259. // V3: neither begin nor end with a U+002D HYPHEN-MINUS
  260. if config.check_hyphens && (label.starts_with('-') || label.ends_with('-')) {
  261. errors.check_hyphens = true;
  262. return;
  263. }
  264. // V4: not contain a U+002E FULL STOP
  265. //
  266. // Here, label can't contain '.' since the input is from .split('.')
  267. // V5: not begin with a GC=Mark
  268. if is_combining_mark(first_char.unwrap()) {
  269. errors.start_combining_mark = true;
  270. return;
  271. }
  272. // V6: Check against Mapping Table
  273. if label.chars().any(|c| match *find_char(c) {
  274. Mapping::Valid | Mapping::DisallowedIdna2008 => false,
  275. Mapping::Deviation(_) => config.transitional_processing,
  276. Mapping::DisallowedStd3Valid => config.use_std3_ascii_rules,
  277. _ => true,
  278. }) {
  279. errors.invalid_mapping = true;
  280. }
  281. // V7: ContextJ rules
  282. //
  283. // TODO: Implement rules and add *CheckJoiners* flag.
  284. // V8: Bidi rules are checked inside `processing()`
  285. }
  286. /// http://www.unicode.org/reports/tr46/#Processing
  287. fn processing(
  288. domain: &str,
  289. config: Config,
  290. normalized: &mut String,
  291. output: &mut String,
  292. ) -> Errors {
  293. // Weed out the simple cases: only allow all lowercase ASCII characters and digits where none
  294. // of the labels start with PUNYCODE_PREFIX and labels don't start or end with hyphen.
  295. let (mut prev, mut simple, mut puny_prefix) = ('?', !domain.is_empty(), 0);
  296. for c in domain.chars() {
  297. if c == '.' {
  298. if prev == '-' {
  299. simple = false;
  300. break;
  301. }
  302. puny_prefix = 0;
  303. continue;
  304. } else if puny_prefix == 0 && c == '-' {
  305. simple = false;
  306. break;
  307. } else if puny_prefix < 5 {
  308. if c == ['x', 'n', '-', '-'][puny_prefix] {
  309. puny_prefix += 1;
  310. if puny_prefix == 4 {
  311. simple = false;
  312. break;
  313. }
  314. } else {
  315. puny_prefix = 5;
  316. }
  317. }
  318. if !c.is_ascii_lowercase() && !c.is_ascii_digit() {
  319. simple = false;
  320. break;
  321. }
  322. prev = c;
  323. }
  324. if simple {
  325. output.push_str(domain);
  326. return Errors::default();
  327. }
  328. normalized.clear();
  329. let mut errors = Errors::default();
  330. let offset = output.len();
  331. let iter = Mapper {
  332. chars: domain.chars(),
  333. config,
  334. errors: &mut errors,
  335. slice: None,
  336. };
  337. normalized.extend(iter.nfc());
  338. let mut decoder = punycode::Decoder::default();
  339. let non_transitional = config.transitional_processing(false);
  340. let (mut first, mut has_bidi_labels) = (true, false);
  341. for label in normalized.split('.') {
  342. if !first {
  343. output.push('.');
  344. }
  345. first = false;
  346. if let Some(remainder) = label.strip_prefix(PUNYCODE_PREFIX) {
  347. match decoder.decode(remainder) {
  348. Ok(decode) => {
  349. let start = output.len();
  350. output.extend(decode);
  351. let decoded_label = &output[start..];
  352. if !has_bidi_labels {
  353. has_bidi_labels |= is_bidi_domain(decoded_label);
  354. }
  355. if !errors.is_err() {
  356. if !is_nfc(decoded_label) {
  357. errors.nfc = true;
  358. } else {
  359. check_validity(decoded_label, non_transitional, &mut errors);
  360. }
  361. }
  362. }
  363. Err(()) => {
  364. has_bidi_labels = true;
  365. errors.punycode = true;
  366. }
  367. }
  368. } else {
  369. if !has_bidi_labels {
  370. has_bidi_labels |= is_bidi_domain(label);
  371. }
  372. // `normalized` is already `NFC` so we can skip that check
  373. check_validity(label, config, &mut errors);
  374. output.push_str(label)
  375. }
  376. }
  377. for label in output[offset..].split('.') {
  378. // V8: Bidi rules
  379. //
  380. // TODO: Add *CheckBidi* flag
  381. if !passes_bidi(label, has_bidi_labels) {
  382. errors.check_bidi = true;
  383. break;
  384. }
  385. }
  386. errors
  387. }
  388. #[derive(Default)]
  389. pub struct Idna {
  390. config: Config,
  391. normalized: String,
  392. output: String,
  393. }
  394. impl Idna {
  395. pub fn new(config: Config) -> Self {
  396. Self {
  397. config,
  398. normalized: String::new(),
  399. output: String::new(),
  400. }
  401. }
  402. /// http://www.unicode.org/reports/tr46/#ToASCII
  403. #[allow(clippy::wrong_self_convention)]
  404. pub fn to_ascii<'a>(&'a mut self, domain: &str, out: &mut String) -> Result<(), Errors> {
  405. let mut errors = processing(domain, self.config, &mut self.normalized, &mut self.output);
  406. let mut first = true;
  407. for label in self.output.split('.') {
  408. if !first {
  409. out.push('.');
  410. }
  411. first = false;
  412. if label.is_ascii() {
  413. out.push_str(label);
  414. } else {
  415. let offset = out.len();
  416. out.push_str(PUNYCODE_PREFIX);
  417. if let Err(()) = punycode::encode_into(label.chars(), out) {
  418. errors.punycode = true;
  419. out.truncate(offset);
  420. }
  421. }
  422. }
  423. if self.config.verify_dns_length {
  424. let domain = if out.ends_with('.') {
  425. &out[..out.len() - 1]
  426. } else {
  427. &*out
  428. };
  429. if domain.is_empty() || domain.split('.').any(|label| label.is_empty()) {
  430. errors.too_short_for_dns = true;
  431. }
  432. if domain.len() > 253 || domain.split('.').any(|label| label.len() > 63) {
  433. errors.too_long_for_dns = true;
  434. }
  435. }
  436. errors.into()
  437. }
  438. /// http://www.unicode.org/reports/tr46/#ToUnicode
  439. #[allow(clippy::wrong_self_convention)]
  440. pub fn to_unicode<'a>(&'a mut self, domain: &str, out: &mut String) -> Result<(), Errors> {
  441. processing(domain, self.config, &mut self.normalized, out).into()
  442. }
  443. }
  444. #[derive(Clone, Copy)]
  445. pub struct Config {
  446. use_std3_ascii_rules: bool,
  447. transitional_processing: bool,
  448. verify_dns_length: bool,
  449. check_hyphens: bool,
  450. use_idna_2008_rules: bool,
  451. }
  452. /// The defaults are that of https://url.spec.whatwg.org/#idna
  453. impl Default for Config {
  454. fn default() -> Self {
  455. Config {
  456. use_std3_ascii_rules: false,
  457. transitional_processing: false,
  458. check_hyphens: false,
  459. // check_bidi: true,
  460. // check_joiners: true,
  461. // Only use for to_ascii, not to_unicode
  462. verify_dns_length: false,
  463. use_idna_2008_rules: false,
  464. }
  465. }
  466. }
  467. impl Config {
  468. #[inline]
  469. pub fn use_std3_ascii_rules(mut self, value: bool) -> Self {
  470. self.use_std3_ascii_rules = value;
  471. self
  472. }
  473. #[inline]
  474. pub fn transitional_processing(mut self, value: bool) -> Self {
  475. self.transitional_processing = value;
  476. self
  477. }
  478. #[inline]
  479. pub fn verify_dns_length(mut self, value: bool) -> Self {
  480. self.verify_dns_length = value;
  481. self
  482. }
  483. #[inline]
  484. pub fn check_hyphens(mut self, value: bool) -> Self {
  485. self.check_hyphens = value;
  486. self
  487. }
  488. #[inline]
  489. pub fn use_idna_2008_rules(mut self, value: bool) -> Self {
  490. self.use_idna_2008_rules = value;
  491. self
  492. }
  493. /// http://www.unicode.org/reports/tr46/#ToASCII
  494. pub fn to_ascii(self, domain: &str) -> Result<String, Errors> {
  495. let mut result = String::new();
  496. let mut codec = Idna::new(self);
  497. codec.to_ascii(domain, &mut result).map(|()| result)
  498. }
  499. /// http://www.unicode.org/reports/tr46/#ToUnicode
  500. pub fn to_unicode(self, domain: &str) -> (String, Result<(), Errors>) {
  501. let mut codec = Idna::new(self);
  502. let mut out = String::with_capacity(domain.len());
  503. let result = codec.to_unicode(domain, &mut out);
  504. (out, result)
  505. }
  506. }
  507. fn is_bidi_domain(s: &str) -> bool {
  508. for c in s.chars() {
  509. if c.is_ascii_graphic() {
  510. continue;
  511. }
  512. match bidi_class(c) {
  513. BidiClass::R | BidiClass::AL | BidiClass::AN => return true,
  514. _ => {}
  515. }
  516. }
  517. false
  518. }
  519. /// Errors recorded during UTS #46 processing.
  520. ///
  521. /// This is opaque for now, indicating what types of errors have been encountered at least once.
  522. /// More details may be exposed in the future.
  523. #[derive(Default)]
  524. pub struct Errors {
  525. punycode: bool,
  526. check_hyphens: bool,
  527. check_bidi: bool,
  528. start_combining_mark: bool,
  529. invalid_mapping: bool,
  530. nfc: bool,
  531. disallowed_by_std3_ascii_rules: bool,
  532. disallowed_mapped_in_std3: bool,
  533. disallowed_character: bool,
  534. too_long_for_dns: bool,
  535. too_short_for_dns: bool,
  536. disallowed_in_idna_2008: bool,
  537. }
  538. impl Errors {
  539. fn is_err(&self) -> bool {
  540. let Errors {
  541. punycode,
  542. check_hyphens,
  543. check_bidi,
  544. start_combining_mark,
  545. invalid_mapping,
  546. nfc,
  547. disallowed_by_std3_ascii_rules,
  548. disallowed_mapped_in_std3,
  549. disallowed_character,
  550. too_long_for_dns,
  551. too_short_for_dns,
  552. disallowed_in_idna_2008,
  553. } = *self;
  554. punycode
  555. || check_hyphens
  556. || check_bidi
  557. || start_combining_mark
  558. || invalid_mapping
  559. || nfc
  560. || disallowed_by_std3_ascii_rules
  561. || disallowed_mapped_in_std3
  562. || disallowed_character
  563. || too_long_for_dns
  564. || too_short_for_dns
  565. || disallowed_in_idna_2008
  566. }
  567. }
  568. impl fmt::Debug for Errors {
  569. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  570. let Errors {
  571. punycode,
  572. check_hyphens,
  573. check_bidi,
  574. start_combining_mark,
  575. invalid_mapping,
  576. nfc,
  577. disallowed_by_std3_ascii_rules,
  578. disallowed_mapped_in_std3,
  579. disallowed_character,
  580. too_long_for_dns,
  581. too_short_for_dns,
  582. disallowed_in_idna_2008,
  583. } = *self;
  584. let fields = [
  585. ("punycode", punycode),
  586. ("check_hyphens", check_hyphens),
  587. ("check_bidi", check_bidi),
  588. ("start_combining_mark", start_combining_mark),
  589. ("invalid_mapping", invalid_mapping),
  590. ("nfc", nfc),
  591. (
  592. "disallowed_by_std3_ascii_rules",
  593. disallowed_by_std3_ascii_rules,
  594. ),
  595. ("disallowed_mapped_in_std3", disallowed_mapped_in_std3),
  596. ("disallowed_character", disallowed_character),
  597. ("too_long_for_dns", too_long_for_dns),
  598. ("too_short_for_dns", too_short_for_dns),
  599. ("disallowed_in_idna_2008", disallowed_in_idna_2008),
  600. ];
  601. let mut empty = true;
  602. f.write_str("Errors { ")?;
  603. for (name, val) in &fields {
  604. if *val {
  605. if !empty {
  606. f.write_str(", ")?;
  607. }
  608. f.write_str(*name)?;
  609. empty = false;
  610. }
  611. }
  612. if !empty {
  613. f.write_str(" }")
  614. } else {
  615. f.write_str("}")
  616. }
  617. }
  618. }
  619. impl From<Errors> for Result<(), Errors> {
  620. fn from(e: Errors) -> Result<(), Errors> {
  621. if !e.is_err() {
  622. Ok(())
  623. } else {
  624. Err(e)
  625. }
  626. }
  627. }
  628. impl StdError for Errors {}
  629. impl fmt::Display for Errors {
  630. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  631. fmt::Debug::fmt(self, f)
  632. }
  633. }
  634. #[cfg(test)]
  635. mod tests {
  636. use super::{find_char, Mapping};
  637. #[test]
  638. fn mapping_fast_path() {
  639. assert_matches!(find_char('-'), &Mapping::Valid);
  640. assert_matches!(find_char('.'), &Mapping::Valid);
  641. for c in &['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] {
  642. assert_matches!(find_char(*c), &Mapping::Valid);
  643. }
  644. for c in &[
  645. 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q',
  646. 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
  647. ] {
  648. assert_matches!(find_char(*c), &Mapping::Valid);
  649. }
  650. }
  651. }