uts46.rs 21 KB

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