uts46.rs 17 KB

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