uts46.rs 21 KB

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