uts46.rs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  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 punycode;
  12. #[allow(unused_imports, deprecated)]
  13. use std::ascii::AsciiExt;
  14. use std::cmp::Ordering::{Equal, Less, Greater};
  15. use unicode_bidi::{BidiClass, bidi_class};
  16. use unicode_normalization::UnicodeNormalization;
  17. use unicode_normalization::char::is_combining_mark;
  18. include!("uts46_mapping_table.rs");
  19. pub static PUNYCODE_PREFIX: &'static str = "xn--";
  20. #[derive(Debug)]
  21. struct StringTableSlice {
  22. // Store these as separate fields so the structure will have an
  23. // alignment of 1 and thus pack better into the Mapping enum, below.
  24. byte_start_lo: u8,
  25. byte_start_hi: u8,
  26. byte_len: u8,
  27. }
  28. fn decode_slice(slice: &StringTableSlice) -> &'static str {
  29. let lo = slice.byte_start_lo as usize;
  30. let hi = slice.byte_start_hi as usize;
  31. let start = (hi << 8) | lo;
  32. let len = slice.byte_len as usize;
  33. &STRING_TABLE[start..(start + len)]
  34. }
  35. #[repr(u8)]
  36. #[derive(Debug)]
  37. enum Mapping {
  38. Valid,
  39. Ignored,
  40. Mapped(StringTableSlice),
  41. Deviation(StringTableSlice),
  42. Disallowed,
  43. DisallowedStd3Valid,
  44. DisallowedStd3Mapped(StringTableSlice),
  45. }
  46. struct Range {
  47. from: char,
  48. to: char,
  49. }
  50. fn find_char(codepoint: char) -> &'static Mapping {
  51. let r = TABLE.binary_search_by(|ref range| {
  52. if codepoint > range.to {
  53. Less
  54. } else if codepoint < range.from {
  55. Greater
  56. } else {
  57. Equal
  58. }
  59. });
  60. r.ok().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. }).unwrap()
  71. }
  72. fn map_char(codepoint: char, config: Config, output: &mut String, errors: &mut Vec<Error>) {
  73. match *find_char(codepoint) {
  74. Mapping::Valid => output.push(codepoint),
  75. Mapping::Ignored => {},
  76. Mapping::Mapped(ref slice) => output.push_str(decode_slice(slice)),
  77. Mapping::Deviation(ref slice) => {
  78. if config.transitional_processing {
  79. output.push_str(decode_slice(slice))
  80. } else {
  81. output.push(codepoint)
  82. }
  83. }
  84. Mapping::Disallowed => {
  85. errors.push(Error::DissallowedCharacter);
  86. output.push(codepoint);
  87. }
  88. Mapping::DisallowedStd3Valid => {
  89. if config.use_std3_ascii_rules {
  90. errors.push(Error::DissallowedByStd3AsciiRules);
  91. }
  92. output.push(codepoint)
  93. }
  94. Mapping::DisallowedStd3Mapped(ref slice) => {
  95. if config.use_std3_ascii_rules {
  96. errors.push(Error::DissallowedMappedInStd3);
  97. }
  98. output.push_str(decode_slice(slice))
  99. }
  100. }
  101. }
  102. // http://tools.ietf.org/html/rfc5893#section-2
  103. fn passes_bidi(label: &str, is_bidi_domain: bool) -> bool {
  104. // Rule 0: Bidi Rules apply to Bidi Domain Names: a name with at least one RTL label. A label
  105. // is RTL if it contains at least one character of bidi class R, AL or AN.
  106. if !is_bidi_domain {
  107. return true;
  108. }
  109. let mut chars = label.chars();
  110. let first_char_class = match chars.next() {
  111. Some(c) => bidi_class(c),
  112. None => return true, // empty string
  113. };
  114. match first_char_class {
  115. // LTR label
  116. BidiClass::L => {
  117. // Rule 5
  118. loop {
  119. match chars.next() {
  120. Some(c) => {
  121. if !matches!(bidi_class(c),
  122. BidiClass::L | BidiClass::EN |
  123. BidiClass::ES | BidiClass::CS |
  124. BidiClass::ET | BidiClass::ON |
  125. BidiClass::BN | BidiClass::NSM
  126. ) {
  127. return false;
  128. }
  129. },
  130. None => { break; },
  131. }
  132. }
  133. // Rule 6
  134. // must end in L or EN followed by 0 or more NSM
  135. let mut rev_chars = label.chars().rev();
  136. let mut last_non_nsm = rev_chars.next();
  137. loop {
  138. match last_non_nsm {
  139. Some(c) if bidi_class(c) == BidiClass::NSM => {
  140. last_non_nsm = rev_chars.next();
  141. continue;
  142. }
  143. _ => { break; },
  144. }
  145. }
  146. match last_non_nsm {
  147. Some(c) if bidi_class(c) == BidiClass::L
  148. || bidi_class(c) == BidiClass::EN => {},
  149. Some(_) => { return false; },
  150. _ => {}
  151. }
  152. }
  153. // RTL label
  154. BidiClass::R | BidiClass::AL => {
  155. let mut found_en = false;
  156. let mut found_an = false;
  157. // Rule 2
  158. loop {
  159. match chars.next() {
  160. Some(c) => {
  161. let char_class = bidi_class(c);
  162. if char_class == BidiClass::EN {
  163. found_en = true;
  164. }
  165. if char_class == BidiClass::AN {
  166. found_an = true;
  167. }
  168. if !matches!(char_class, BidiClass::R | BidiClass::AL |
  169. BidiClass::AN | BidiClass::EN |
  170. BidiClass::ES | BidiClass::CS |
  171. BidiClass::ET | BidiClass::ON |
  172. BidiClass::BN | BidiClass::NSM) {
  173. return false;
  174. }
  175. },
  176. None => { break; },
  177. }
  178. }
  179. // Rule 3
  180. let mut rev_chars = label.chars().rev();
  181. let mut last = rev_chars.next();
  182. loop { // must end in L or EN followed by 0 or more NSM
  183. match last {
  184. Some(c) if bidi_class(c) == BidiClass::NSM => {
  185. last = rev_chars.next();
  186. continue;
  187. }
  188. _ => { break; },
  189. }
  190. }
  191. match last {
  192. Some(c) if matches!(bidi_class(c), BidiClass::R | BidiClass::AL |
  193. BidiClass::EN | BidiClass::AN) => {},
  194. _ => { return false; }
  195. }
  196. // Rule 4
  197. if found_an && found_en {
  198. return false;
  199. }
  200. }
  201. // Rule 1: Should start with L or R/AL
  202. _ => {
  203. return false;
  204. }
  205. }
  206. return true;
  207. }
  208. /// http://www.unicode.org/reports/tr46/#Validity_Criteria
  209. fn validate_full(label: &str, is_bidi_domain: bool, config: Config, errors: &mut Vec<Error>) {
  210. // V1: Must be in NFC form.
  211. if label.nfc().ne(label.chars()) {
  212. errors.push(Error::ValidityCriteria);
  213. } else {
  214. validate(label, is_bidi_domain, config, errors);
  215. }
  216. }
  217. fn validate(label: &str, is_bidi_domain: bool, config: Config, errors: &mut Vec<Error>) {
  218. let first_char = label.chars().next();
  219. if first_char == None {
  220. // Empty string, pass
  221. }
  222. // V2: No U+002D HYPHEN-MINUS in both third and fourth positions.
  223. //
  224. // NOTE: Spec says that the label must not contain a HYPHEN-MINUS character in both the
  225. // third and fourth positions. But nobody follows this criteria. See the spec issue below:
  226. // https://github.com/whatwg/url/issues/53
  227. // V3: neither begin nor end with a U+002D HYPHEN-MINUS
  228. else if config.check_hyphens && (label.starts_with("-") || label.ends_with("-")) {
  229. errors.push(Error::ValidityCriteria);
  230. }
  231. // V4: not contain a U+002E FULL STOP
  232. //
  233. // Here, label can't contain '.' since the input is from .split('.')
  234. // V5: not begin with a GC=Mark
  235. else if is_combining_mark(first_char.unwrap()) {
  236. errors.push(Error::ValidityCriteria);
  237. }
  238. // V6: Check against Mapping Table
  239. else if label.chars().any(|c| match *find_char(c) {
  240. Mapping::Valid => false,
  241. Mapping::Deviation(_) => config.transitional_processing,
  242. Mapping::DisallowedStd3Valid => config.use_std3_ascii_rules,
  243. _ => true,
  244. }) {
  245. errors.push(Error::ValidityCriteria);
  246. }
  247. // V7: ContextJ rules
  248. //
  249. // TODO: Implement rules and add *CheckJoiners* flag.
  250. // V8: Bidi rules
  251. //
  252. // TODO: Add *CheckBidi* flag
  253. else if !passes_bidi(label, is_bidi_domain)
  254. {
  255. errors.push(Error::ValidityCriteria);
  256. }
  257. }
  258. /// http://www.unicode.org/reports/tr46/#Processing
  259. fn processing(domain: &str, config: Config, errors: &mut Vec<Error>) -> String {
  260. let mut mapped = String::with_capacity(domain.len());
  261. for c in domain.chars() {
  262. map_char(c, config, &mut mapped, errors)
  263. }
  264. let mut normalized = String::with_capacity(mapped.len());
  265. normalized.extend(mapped.nfc());
  266. // Find out if it's a Bidi Domain Name
  267. //
  268. // First, check for literal bidi chars
  269. let mut is_bidi_domain = domain.chars().any(|c|
  270. matches!(bidi_class(c), BidiClass::R | BidiClass::AL | BidiClass::AN)
  271. );
  272. if !is_bidi_domain {
  273. // Then check for punycode-encoded bidi chars
  274. for label in normalized.split('.') {
  275. if label.starts_with(PUNYCODE_PREFIX) {
  276. match punycode::decode_to_string(&label[PUNYCODE_PREFIX.len()..]) {
  277. Some(decoded_label) => {
  278. if decoded_label.chars().any(|c|
  279. matches!(bidi_class(c), BidiClass::R | BidiClass::AL | BidiClass::AN)
  280. ) {
  281. is_bidi_domain = true;
  282. }
  283. }
  284. None => {
  285. is_bidi_domain = true;
  286. }
  287. }
  288. }
  289. }
  290. }
  291. let mut validated = String::new();
  292. let mut first = true;
  293. for label in normalized.split('.') {
  294. if !first {
  295. validated.push('.');
  296. }
  297. first = false;
  298. if label.starts_with(PUNYCODE_PREFIX) {
  299. match punycode::decode_to_string(&label[PUNYCODE_PREFIX.len()..]) {
  300. Some(decoded_label) => {
  301. let config = config.transitional_processing(false);
  302. validate_full(&decoded_label, is_bidi_domain, config, errors);
  303. validated.push_str(&decoded_label)
  304. }
  305. None => errors.push(Error::PunycodeError)
  306. }
  307. } else {
  308. // `normalized` is already `NFC` so we can skip that check
  309. validate(label, is_bidi_domain, config, errors);
  310. validated.push_str(label)
  311. }
  312. }
  313. validated
  314. }
  315. #[derive(Clone, Copy, Default)]
  316. pub struct Config {
  317. use_std3_ascii_rules: bool,
  318. transitional_processing: bool,
  319. verify_dns_length: bool,
  320. check_hyphens: bool,
  321. }
  322. impl Config {
  323. #[inline]
  324. pub fn use_std3_ascii_rules(mut self, value: bool) -> Self {
  325. self.use_std3_ascii_rules = value;
  326. self
  327. }
  328. #[inline]
  329. pub fn transitional_processing(mut self, value: bool) -> Self {
  330. self.transitional_processing = value;
  331. self
  332. }
  333. #[inline]
  334. pub fn verify_dns_length(mut self, value: bool) -> Self {
  335. self.verify_dns_length = value;
  336. self
  337. }
  338. #[inline]
  339. pub fn check_hyphens(mut self, value: bool) -> Self {
  340. self.check_hyphens = value;
  341. self
  342. }
  343. /// http://www.unicode.org/reports/tr46/#ToASCII
  344. pub fn to_ascii(self, domain: &str) -> Result<String, Errors> {
  345. let mut errors = Vec::new();
  346. let mut result = String::new();
  347. let mut first = true;
  348. for label in processing(domain, self, &mut errors).split('.') {
  349. if !first {
  350. result.push('.');
  351. }
  352. first = false;
  353. if label.is_ascii() {
  354. result.push_str(label);
  355. } else {
  356. match punycode::encode_str(label) {
  357. Some(x) => {
  358. result.push_str(PUNYCODE_PREFIX);
  359. result.push_str(&x);
  360. },
  361. None => errors.push(Error::PunycodeError)
  362. }
  363. }
  364. }
  365. if self.verify_dns_length {
  366. let domain = if result.ends_with(".") { &result[..result.len()-1] } else { &*result };
  367. if domain.len() < 1 || domain.split('.').any(|label| label.len() < 1) {
  368. errors.push(Error::TooShortForDns)
  369. }
  370. if domain.len() > 253 || domain.split('.').any(|label| label.len() > 63) {
  371. errors.push(Error::TooLongForDns)
  372. }
  373. }
  374. if errors.is_empty() {
  375. Ok(result)
  376. } else {
  377. Err(Errors(errors))
  378. }
  379. }
  380. /// http://www.unicode.org/reports/tr46/#ToUnicode
  381. pub fn to_unicode(self, domain: &str) -> (String, Result<(), Errors>) {
  382. let mut errors = Vec::new();
  383. let domain = processing(domain, self, &mut errors);
  384. let errors = if errors.is_empty() {
  385. Ok(())
  386. } else {
  387. Err(Errors(errors))
  388. };
  389. (domain, errors)
  390. }
  391. }
  392. #[derive(PartialEq, Eq, Clone, Copy, Debug)]
  393. enum Error {
  394. PunycodeError,
  395. ValidityCriteria,
  396. DissallowedByStd3AsciiRules,
  397. DissallowedMappedInStd3,
  398. DissallowedCharacter,
  399. TooLongForDns,
  400. TooShortForDns,
  401. }
  402. /// Errors recorded during UTS #46 processing.
  403. ///
  404. /// This is opaque for now, only indicating the presence of at least one error.
  405. /// More details may be exposed in the future.
  406. #[derive(Debug)]
  407. pub struct Errors(Vec<Error>);