uts46.rs 76 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768
  1. // Copyright 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. //! This module provides the lower-level API for UTS 46.
  9. //!
  10. //! [`Uts46::process`] is the core that the other convenience
  11. //! methods build on.
  12. //!
  13. //! UTS 46 flags map to this API as follows:
  14. //!
  15. //! * _CheckHyphens_ - _true_: [`Hyphens::Check`], _false_: [`Hyphens::Allow`]; the WHATWG URL Standard sets this to _false_ for normal (non-conformance-checker) user agents.
  16. //! * _CheckBidi_ - Always _true_; cannot be configured, since this flag is _true_ even when WHATWG URL Standard _beStrict_ is _false_.
  17. //! * _CheckJoiners_ - Always _true_; cannot be configured, since this flag is _true_ even when WHATWG URL Standard _beStrict_ is _false_.
  18. //! * _UseSTD3ASCIIRules_ - _true_: [`AsciiDenyList::STD3`], _false_: [`AsciiDenyList::EMPTY`]; however, the check the WHATWG URL Standard performs right after the UTS 46 invocation corresponds to [`AsciiDenyList::URL`].
  19. //! * _Transitional_Processing_ - Always _false_ but could be implemented as a preprocessing step. This flag is deprecated and for Web purposes the transition is over in the sense that all of Firefox, Safari, or Chrome set this flag to _false_.
  20. //! * _VerifyDnsLength_ - _true_: [`DnsLength::Verify`], _false_: [`DnsLength::Ignore`]; the WHATWG URL Standard sets this to _false_ for normal (non-conformance-checker) user agents.
  21. //! * _IgnoreInvalidPunycode_ - Always _false_; cannot be configured. (Not yet covered by the WHATWG URL Standard, but 2 out of 3 major browser clearly behave as if this was _false_).
  22. use crate::punycode::Decoder;
  23. use crate::punycode::InternalCaller;
  24. use alloc::borrow::Cow;
  25. use alloc::string::String;
  26. use core::fmt::Write;
  27. use icu_normalizer::properties::CanonicalCombiningClassMap;
  28. use icu_normalizer::uts46::Uts46Mapper;
  29. use icu_properties::maps::CodePointMapDataBorrowed;
  30. use icu_properties::BidiClass;
  31. use icu_properties::CanonicalCombiningClass;
  32. use icu_properties::GeneralCategory;
  33. use icu_properties::JoiningType;
  34. use smallvec::SmallVec;
  35. use utf8_iter::Utf8CharsEx;
  36. /// ICU4C-compatible constraint.
  37. /// https://unicode-org.atlassian.net/browse/ICU-13727
  38. const PUNYCODE_DECODE_MAX_INPUT_LENGTH: usize = 2000;
  39. /// ICU4C-compatible constraint. (Note: ICU4C measures
  40. /// UTF-16 and we measure UTF-32. This means that we
  41. /// allow longer non-BMP inputs. For this implementation,
  42. /// the denial-of-service scaling does not depend on BMP vs.
  43. /// non-BMP: only the scalar values matter.)
  44. ///
  45. /// https://unicode-org.atlassian.net/browse/ICU-13727
  46. const PUNYCODE_ENCODE_MAX_INPUT_LENGTH: usize = 1000;
  47. /// For keeping track of what kind of numerals have been
  48. /// seen in an RTL label.
  49. #[derive(Debug, PartialEq, Eq)]
  50. enum RtlNumeralState {
  51. Undecided,
  52. European,
  53. Arabic,
  54. }
  55. /// Computes the mask for upper-case ASCII.
  56. const fn upper_case_mask() -> u128 {
  57. let mut accu = 0u128;
  58. let mut b = 0u8;
  59. while b < 128 {
  60. if (b >= b'A') && (b <= b'Z') {
  61. accu |= 1u128 << b;
  62. }
  63. b += 1;
  64. }
  65. accu
  66. }
  67. /// Bit set for upper-case ASCII.
  68. const UPPER_CASE_MASK: u128 = upper_case_mask();
  69. /// Computes the mask for glyphless ASCII.
  70. const fn glyphless_mask() -> u128 {
  71. let mut accu = 0u128;
  72. let mut b = 0u8;
  73. while b < 128 {
  74. if (b <= b' ') || (b == 0x7F) {
  75. accu |= 1u128 << b;
  76. }
  77. b += 1;
  78. }
  79. accu
  80. }
  81. /// Bit set for glyphless ASCII.
  82. const GLYPHLESS_MASK: u128 = glyphless_mask();
  83. /// The mask for the ASCII dot.
  84. const DOT_MASK: u128 = 1 << b'.';
  85. /// Computes the ASCII deny list for STD3 ASCII rules.
  86. const fn ldh_mask() -> u128 {
  87. let mut accu = 0u128;
  88. let mut b = 0u8;
  89. while b < 128 {
  90. if !((b >= b'a' && b <= b'z') || (b >= b'0' && b <= b'9') || b == b'-' || b == b'.') {
  91. accu |= 1u128 << b;
  92. }
  93. b += 1;
  94. }
  95. accu
  96. }
  97. /// Turns a joining type into a mask for comparing with multiple type at once.
  98. const fn joining_type_to_mask(jt: JoiningType) -> u32 {
  99. 1u32 << jt.0
  100. }
  101. /// Mask for checking for both left and dual joining.
  102. const LEFT_OR_DUAL_JOINING_MASK: u32 =
  103. joining_type_to_mask(JoiningType::LeftJoining) | joining_type_to_mask(JoiningType::DualJoining);
  104. /// Mask for checking for both left and dual joining.
  105. const RIGHT_OR_DUAL_JOINING_MASK: u32 = joining_type_to_mask(JoiningType::RightJoining)
  106. | joining_type_to_mask(JoiningType::DualJoining);
  107. /// Turns a bidi class into a mask for comparing with multiple classes at once.
  108. const fn bidi_class_to_mask(bc: BidiClass) -> u32 {
  109. 1u32 << bc.0
  110. }
  111. /// Mask for checking if the domain is a bidi domain.
  112. const RTL_MASK: u32 = bidi_class_to_mask(BidiClass::RightToLeft)
  113. | bidi_class_to_mask(BidiClass::ArabicLetter)
  114. | bidi_class_to_mask(BidiClass::ArabicNumber);
  115. /// Mask for allowable bidi classes in the first character of a label
  116. /// (either LTR or RTL) in a bidi domain.
  117. const FIRST_BC_MASK: u32 = bidi_class_to_mask(BidiClass::LeftToRight)
  118. | bidi_class_to_mask(BidiClass::RightToLeft)
  119. | bidi_class_to_mask(BidiClass::ArabicLetter);
  120. // Mask for allowable bidi classes of the last (non-Non-Spacing Mark)
  121. // character in an LTR label in a bidi domain.
  122. const LAST_LTR_MASK: u32 =
  123. bidi_class_to_mask(BidiClass::LeftToRight) | bidi_class_to_mask(BidiClass::EuropeanNumber);
  124. // Mask for allowable bidi classes of the last (non-Non-Spacing Mark)
  125. // character in an RTL label in a bidi domain.
  126. const LAST_RTL_MASK: u32 = bidi_class_to_mask(BidiClass::RightToLeft)
  127. | bidi_class_to_mask(BidiClass::ArabicLetter)
  128. | bidi_class_to_mask(BidiClass::EuropeanNumber)
  129. | bidi_class_to_mask(BidiClass::ArabicNumber);
  130. // Mask for allowable bidi classes of the middle characters in an LTR label in a bidi domain.
  131. const MIDDLE_LTR_MASK: u32 = bidi_class_to_mask(BidiClass::LeftToRight)
  132. | bidi_class_to_mask(BidiClass::EuropeanNumber)
  133. | bidi_class_to_mask(BidiClass::EuropeanSeparator)
  134. | bidi_class_to_mask(BidiClass::CommonSeparator)
  135. | bidi_class_to_mask(BidiClass::EuropeanTerminator)
  136. | bidi_class_to_mask(BidiClass::OtherNeutral)
  137. | bidi_class_to_mask(BidiClass::BoundaryNeutral)
  138. | bidi_class_to_mask(BidiClass::NonspacingMark);
  139. // Mask for allowable bidi classes of the middle characters in an RTL label in a bidi domain.
  140. const MIDDLE_RTL_MASK: u32 = bidi_class_to_mask(BidiClass::RightToLeft)
  141. | bidi_class_to_mask(BidiClass::ArabicLetter)
  142. | bidi_class_to_mask(BidiClass::ArabicNumber)
  143. | bidi_class_to_mask(BidiClass::EuropeanNumber)
  144. | bidi_class_to_mask(BidiClass::EuropeanSeparator)
  145. | bidi_class_to_mask(BidiClass::CommonSeparator)
  146. | bidi_class_to_mask(BidiClass::EuropeanTerminator)
  147. | bidi_class_to_mask(BidiClass::OtherNeutral)
  148. | bidi_class_to_mask(BidiClass::BoundaryNeutral)
  149. | bidi_class_to_mask(BidiClass::NonspacingMark);
  150. /// Turns a genecal category into a mask for comparing with multiple categories at once.
  151. const fn general_category_to_mask(gc: GeneralCategory) -> u32 {
  152. 1 << (gc as u32)
  153. }
  154. /// Mask for the disallowed general categories of the first character in a label.
  155. const MARK_MASK: u32 = general_category_to_mask(GeneralCategory::NonspacingMark)
  156. | general_category_to_mask(GeneralCategory::SpacingMark)
  157. | general_category_to_mask(GeneralCategory::EnclosingMark);
  158. const PUNYCODE_PREFIX: u32 =
  159. ((b'-' as u32) << 24) | ((b'-' as u32) << 16) | ((b'N' as u32) << 8) | b'X' as u32;
  160. const PUNYCODE_PREFIX_MASK: u32 = (0xFF << 24) | (0xFF << 16) | (0xDF << 8) | 0xDF;
  161. fn write_punycode_label<W: Write + ?Sized>(
  162. label: &[char],
  163. sink: &mut W,
  164. ) -> Result<(), ProcessingError> {
  165. sink.write_str("xn--")?;
  166. crate::punycode::encode_into::<_, _, InternalCaller>(label.iter().copied(), sink)?;
  167. Ok(())
  168. }
  169. #[inline(always)]
  170. fn has_punycode_prefix(slice: &[u8]) -> bool {
  171. if slice.len() < 4 {
  172. return false;
  173. }
  174. // Sadly, the optimizer doesn't figure out that more idiomatic code
  175. // should compile to masking on 32-bit value.
  176. let a = slice[0];
  177. let b = slice[1];
  178. let c = slice[2];
  179. let d = slice[3];
  180. let u = (u32::from(d) << 24) | (u32::from(c) << 16) | (u32::from(b) << 8) | u32::from(a);
  181. (u & PUNYCODE_PREFIX_MASK) == PUNYCODE_PREFIX
  182. }
  183. #[inline(always)]
  184. fn in_inclusive_range8(u: u8, start: u8, end: u8) -> bool {
  185. u.wrapping_sub(start) <= (end - start)
  186. }
  187. #[inline(always)]
  188. fn in_inclusive_range_char(c: char, start: char, end: char) -> bool {
  189. u32::from(c).wrapping_sub(u32::from(start)) <= (u32::from(end) - u32::from(start))
  190. }
  191. #[inline(always)]
  192. fn is_passthrough_ascii_label(label: &[u8]) -> bool {
  193. // XXX if we aren't performing _CheckHyphens_, this could
  194. // check for "xn--" and pass through YouTube CDN node names.
  195. if label.len() >= 4 && label[2] == b'-' && label[3] == b'-' {
  196. return false;
  197. }
  198. if let Some((&first, tail)) = label.split_first() {
  199. // We need to check the first and last character
  200. // more strictly in case this turns out to be a
  201. // label in a bidi domain name. This has the side
  202. // effect that this function only accepts labels
  203. // that also conform to the STD3 rules.
  204. //
  205. // XXX: If we are in the fail-fast mode (i.e. we don't need
  206. // to be able to overwrite anything with U+FFFD), we could
  207. // merely record that we've seen a digit here and error out
  208. // if we later discover that the domain name is a bidi
  209. // domain name.
  210. if !in_inclusive_range8(first, b'a', b'z') {
  211. return false;
  212. }
  213. for &b in tail {
  214. // If we used LDH_MASK, we'd have to check
  215. // the bytes for the ASCII range anyhow.
  216. if in_inclusive_range8(b, b'a', b'z') {
  217. continue;
  218. }
  219. if in_inclusive_range8(b, b'0', b'9') {
  220. continue;
  221. }
  222. if b == b'-' {
  223. continue;
  224. }
  225. return false;
  226. }
  227. label.last() != Some(&b'-')
  228. } else {
  229. // empty
  230. true
  231. }
  232. }
  233. #[inline(always)]
  234. fn split_ascii_fast_path_prefix(label: &[u8]) -> (&[u8], &[u8]) {
  235. if let Some(pos) = label.iter().position(|b| !b.is_ascii()) {
  236. if pos == 0 {
  237. // First is non-ASCII
  238. (&[], label)
  239. } else {
  240. // Leave one ASCII character in the suffix
  241. // in case it's a letter that a combining
  242. // character combines with.
  243. let (head, tail) = label.split_at(pos - 1);
  244. (head, tail)
  245. }
  246. } else {
  247. // All ASCII
  248. (label, &[])
  249. }
  250. }
  251. // Input known to be lower-case, but may contain non-ASCII.
  252. #[inline(always)]
  253. fn apply_ascii_deny_list_to_lower_cased_unicode(c: char, deny_list: u128) -> char {
  254. if let Some(shifted) = 1u128.checked_shl(u32::from(c)) {
  255. if (deny_list & shifted) == 0 {
  256. c
  257. } else {
  258. '\u{FFFD}'
  259. }
  260. } else {
  261. c
  262. }
  263. }
  264. // Input known to be ASCII, but may contain upper case ASCII.
  265. #[inline(always)]
  266. fn apply_ascii_deny_list_to_potentially_upper_case_ascii(b: u8, deny_list: u128) -> char {
  267. if (deny_list & (1u128 << b)) == 0 {
  268. return char::from(b);
  269. }
  270. if in_inclusive_range8(b, b'A', b'Z') {
  271. return char::from(b + 0x20);
  272. }
  273. '\u{FFFD}'
  274. }
  275. #[inline(always)]
  276. fn is_ascii(label: &[char]) -> bool {
  277. for c in label.iter() {
  278. if !c.is_ascii() {
  279. return false;
  280. }
  281. }
  282. true
  283. }
  284. #[derive(PartialEq, Eq, Copy, Clone)]
  285. enum PunycodeClassification {
  286. Ascii,
  287. Unicode,
  288. Error,
  289. }
  290. #[inline(always)]
  291. fn classify_for_punycode(label: &[char]) -> PunycodeClassification {
  292. let mut iter = label.iter().copied();
  293. loop {
  294. if let Some(c) = iter.next() {
  295. if c.is_ascii() {
  296. continue;
  297. }
  298. if c == '\u{FFFD}' {
  299. return PunycodeClassification::Error;
  300. }
  301. for c in iter {
  302. if c == '\u{FFFD}' {
  303. return PunycodeClassification::Error;
  304. }
  305. }
  306. return PunycodeClassification::Unicode;
  307. }
  308. return PunycodeClassification::Ascii;
  309. }
  310. }
  311. /// The ASCII deny list to be applied.
  312. #[derive(PartialEq, Eq, Copy, Clone)]
  313. #[repr(transparent)]
  314. pub struct AsciiDenyList {
  315. bits: u128,
  316. }
  317. impl AsciiDenyList {
  318. /// Computes (preferably at compile time) an ASCII deny list.
  319. ///
  320. /// Setting `deny_glyphless` to `true` denies U+0020 SPACE and below
  321. /// as well as U+007F DELETE for convenience without having to list
  322. /// these characters in the `deny_list` string.
  323. ///
  324. /// `deny_list` is the list of ASCII characters to deny. This
  325. /// list must not contain any of:
  326. /// * Letters
  327. /// * Digits
  328. /// * Hyphen
  329. /// * Dot (period / full-stop)
  330. /// * Non-ASCII
  331. ///
  332. /// # Panics
  333. ///
  334. /// If the deny list contains characters listed as prohibited above.
  335. pub const fn new(deny_glyphless: bool, deny_list: &str) -> Self {
  336. let mut bits = UPPER_CASE_MASK;
  337. if deny_glyphless {
  338. bits |= GLYPHLESS_MASK;
  339. }
  340. let mut i = 0;
  341. let bytes = deny_list.as_bytes();
  342. while i < bytes.len() {
  343. let b = bytes[i];
  344. assert!(b < 0x80, "ASCII deny list must be ASCII.");
  345. // assert_ne not yet available in const context.
  346. assert!(b != b'.', "ASCII deny list must not contain the dot.");
  347. assert!(b != b'-', "ASCII deny list must not contain the hyphen.");
  348. assert!(
  349. !((b >= b'0') && (b <= b'9')),
  350. "ASCII deny list must not contain digits."
  351. );
  352. assert!(
  353. !((b >= b'a') && (b <= b'z')),
  354. "ASCII deny list must not contain letters."
  355. );
  356. assert!(
  357. !((b >= b'A') && (b <= b'Z')),
  358. "ASCII deny list must not contain letters."
  359. );
  360. bits |= 1u128 << b;
  361. i += 1;
  362. }
  363. AsciiDenyList { bits }
  364. }
  365. /// No ASCII deny list. This corresponds to _UseSTD3ASCIIRules=false_.
  366. ///
  367. /// Equivalent to `AsciiDenyList::new(false, "")`.
  368. ///
  369. /// Note: Not denying the space and control characters can result in
  370. /// strange behavior. Without a deny list provided to the UTS 46
  371. /// operation, the caller is expected perform filtering afterwards,
  372. /// but it's more efficient to use `AsciiDenyList` than post-processing,
  373. /// because the internals of this crate can optimize away checks in
  374. /// certain cases.
  375. pub const EMPTY: AsciiDenyList = AsciiDenyList::new(false, "");
  376. /// The STD3 deny list. This corresponds to _UseSTD3ASCIIRules=true_.
  377. ///
  378. /// Note that this deny list rejects the underscore, which occurs in
  379. /// pseudo-hosts used by various TXT record-based protocols, and also
  380. /// characters that may occurs in non-DNS naming, such as NetBIOS.
  381. pub const STD3: AsciiDenyList = AsciiDenyList { bits: ldh_mask() };
  382. /// [Forbidden domain code point](https://url.spec.whatwg.org/#forbidden-domain-code-point) from the WHATWG URL Standard.
  383. ///
  384. /// Equivalent to `AsciiDenyList::new(true, "%#/:<>?@[\\]^|")`.
  385. ///
  386. /// Note that this deny list rejects IPv6 addresses, so (as in URL
  387. /// parsing) you need to check for IPv6 addresses first and not
  388. /// put them through UTS 46 processing.
  389. pub const URL: AsciiDenyList = AsciiDenyList::new(true, "%#/:<>?@[\\]^|");
  390. }
  391. /// The _CheckHyphens_ mode.
  392. #[derive(PartialEq, Eq, Copy, Clone)]
  393. #[non_exhaustive] // non_exhaustive in case a middle mode that prohibits only first and last position needs to be added
  394. pub enum Hyphens {
  395. /// _CheckHyphens=false_: Do not place positional restrictions on hyphens.
  396. ///
  397. /// This mode is used by the WHATWG URL Standard for normal User Agent processing
  398. /// (i.e. not conformance checking).
  399. Allow,
  400. /// Prohibit hyphens in the first and last position in the label but allow in
  401. /// the third and fourth position.
  402. ///
  403. /// Note that this mode rejects real-world names, including some GitHub user pages.
  404. CheckFirstLast,
  405. /// _CheckHyphens=true_: Prohibit hyphens in the first, third, fourth,
  406. /// and last position in the label.
  407. ///
  408. /// Note that this mode rejects real-world names, including YouTube CDN nodes
  409. /// and some GitHub user pages.
  410. Check,
  411. }
  412. /// The UTS 46 _VerifyDNSLength_ flag.
  413. #[derive(PartialEq, Eq, Copy, Clone)]
  414. #[non_exhaustive]
  415. pub enum DnsLength {
  416. /// _VerifyDNSLength=false_. (Possibly relevant for allowing non-DNS naming systems.)
  417. Ignore,
  418. /// _VerifyDNSLength=true_ with the exception that the trailing root label dot is
  419. /// allowed.
  420. VerifyAllowRootDot,
  421. /// _VerifyDNSLength=true_. (The trailing root label dot is not allowed.)
  422. Verify,
  423. }
  424. /// Policy for customizing behavior in case of an error.
  425. #[derive(PartialEq, Eq, Copy, Clone)]
  426. #[non_exhaustive]
  427. pub enum ErrorPolicy {
  428. /// Return as early as possible without producing output in case of error.
  429. FailFast,
  430. /// In case of error, mark errors with the REPLACEMENT CHARACTER. (The output
  431. /// containing REPLACEMENT CHARACTERs may be show to the user to illustrate
  432. /// what was wrong but must not be used for naming in a network protocol.)
  433. MarkErrors,
  434. }
  435. /// The success outcome of [`Uts46::process`]
  436. #[derive(PartialEq, Eq, Copy, Clone, Debug)]
  437. pub enum ProcessingSuccess {
  438. /// There were no errors. The caller must consider the input to be the output.
  439. ///
  440. /// This asserts that the input can be safely passed to [`core::str::from_utf8_unchecked`].
  441. ///
  442. /// (Distinct from `WroteToSink` in order to allow `Cow` behavior to be implemented on top of
  443. /// [`Uts46::process`].)
  444. Passthrough,
  445. /// There were no errors. The caller must consider what was written to the sink to be the output.
  446. ///
  447. /// (Distinct from `Passthrough` in order to allow `Cow` behavior to be implemented on top of
  448. /// [`Uts46::process`].)
  449. WroteToSink,
  450. }
  451. /// The failure outcome of [`Uts46::process`]
  452. #[derive(PartialEq, Eq, Copy, Clone, Debug)]
  453. pub enum ProcessingError {
  454. /// There was a validity error according to the chosen options.
  455. ///
  456. /// In case of `Operation::ToAscii`, there is no output. Otherwise, output was written to the
  457. /// sink and the output contains at least one U+FFFD REPLACEMENT CHARACTER to denote an error.
  458. ValidityError,
  459. /// The sink emitted [`core::fmt::Error`]. The partial output written to the sink must not
  460. /// be used.
  461. SinkError,
  462. }
  463. impl From<core::fmt::Error> for ProcessingError {
  464. fn from(_: core::fmt::Error) -> Self {
  465. ProcessingError::SinkError
  466. }
  467. }
  468. impl From<crate::punycode::PunycodeEncodeError> for ProcessingError {
  469. fn from(_: crate::punycode::PunycodeEncodeError) -> Self {
  470. unreachable!(
  471. "Punycode overflows should not be possible due to PUNYCODE_ENCODE_MAX_INPUT_LENGTH"
  472. );
  473. }
  474. }
  475. #[derive(Debug, Clone, Copy)]
  476. enum AlreadyAsciiLabel<'a> {
  477. MixedCaseAscii(&'a [u8]),
  478. MixedCasePunycode(&'a [u8]),
  479. Other,
  480. }
  481. /// Performs the _VerifyDNSLength_ check on the output of the _ToASCII_ operation.
  482. ///
  483. /// If the second argument is `false`, the trailing root label dot is allowed.
  484. ///
  485. /// # Panics
  486. ///
  487. /// Panics in debug mode if the argument isn't ASCII.
  488. pub fn verify_dns_length(domain_name: &str, allow_trailing_dot: bool) -> bool {
  489. let bytes = domain_name.as_bytes();
  490. debug_assert!(bytes.is_ascii());
  491. let domain_name_without_trailing_dot = if let Some(without) = bytes.strip_suffix(b".") {
  492. if !allow_trailing_dot {
  493. return false;
  494. }
  495. without
  496. } else {
  497. bytes
  498. };
  499. if domain_name_without_trailing_dot.len() > 253 {
  500. return false;
  501. }
  502. for label in domain_name_without_trailing_dot.split(|b| *b == b'.') {
  503. if label.is_empty() {
  504. return false;
  505. }
  506. if label.len() > 63 {
  507. return false;
  508. }
  509. }
  510. true
  511. }
  512. /// An implementation of UTS #46.
  513. pub struct Uts46 {
  514. mapper: Uts46Mapper,
  515. canonical_combining_class: CanonicalCombiningClassMap,
  516. general_category: CodePointMapDataBorrowed<'static, GeneralCategory>,
  517. bidi_class: CodePointMapDataBorrowed<'static, BidiClass>,
  518. joining_type: CodePointMapDataBorrowed<'static, JoiningType>,
  519. }
  520. #[cfg(feature = "compiled_data")]
  521. impl Default for Uts46 {
  522. fn default() -> Self {
  523. Self::new()
  524. }
  525. }
  526. impl Uts46 {
  527. /// Constructor using data compiled into the binary.
  528. #[cfg(feature = "compiled_data")]
  529. pub const fn new() -> Self {
  530. Self {
  531. mapper: Uts46Mapper::new(),
  532. canonical_combining_class: CanonicalCombiningClassMap::new(),
  533. general_category: icu_properties::maps::general_category(),
  534. bidi_class: icu_properties::maps::bidi_class(),
  535. joining_type: icu_properties::maps::joining_type(),
  536. }
  537. }
  538. // XXX Should there be an `icu_provider` feature for enabling
  539. // a constructor for run-time data loading?
  540. /// Performs the [ToASCII](https://www.unicode.org/reports/tr46/#ToASCII) operation
  541. /// from UTS #46 with the options indicated.
  542. ///
  543. /// # Arguments
  544. ///
  545. /// * `domain_name` - The input domain name as UTF-8 bytes. (The UTF-8ness is checked by
  546. /// this method and input that is not well-formed UTF-8 is treated as an error. If you
  547. /// already have a `&str`, call `.as_bytes()` on it.)
  548. /// * `ascii_deny_list` - What ASCII deny list, if any, to apply. The UTS 46
  549. /// _UseSTD3ASCIIRules_ flag or the WHATWG URL Standard forbidden domain code point
  550. /// processing is handled via this argument. Most callers are probably the best off
  551. /// by using [`AsciiDenyList::URL`] here.
  552. /// * `hyphens` - The UTS 46 _CheckHyphens_ flag. Most callers are probably the best
  553. /// off by using [`Hyphens::Allow`] here.
  554. /// * `dns_length` - The UTS 46 _VerifyDNSLength_ flag.
  555. pub fn to_ascii<'a>(
  556. &self,
  557. domain_name: &'a [u8],
  558. ascii_deny_list: AsciiDenyList,
  559. hyphens: Hyphens,
  560. dns_length: DnsLength,
  561. ) -> Result<Cow<'a, str>, crate::Errors> {
  562. let mut s = String::new();
  563. match self.process(
  564. domain_name,
  565. ascii_deny_list,
  566. hyphens,
  567. ErrorPolicy::FailFast,
  568. |_, _, _| false,
  569. &mut s,
  570. None,
  571. ) {
  572. // SAFETY: `ProcessingSuccess::Passthrough` asserts that `domain_name` is ASCII.
  573. Ok(ProcessingSuccess::Passthrough) => {
  574. let cow = Cow::Borrowed(unsafe { core::str::from_utf8_unchecked(domain_name) });
  575. if dns_length != DnsLength::Ignore
  576. && !verify_dns_length(&cow, dns_length == DnsLength::VerifyAllowRootDot)
  577. {
  578. Err(crate::Errors::default())
  579. } else {
  580. Ok(cow)
  581. }
  582. }
  583. Ok(ProcessingSuccess::WroteToSink) => {
  584. let cow: Cow<'_, str> = Cow::Owned(s);
  585. if dns_length != DnsLength::Ignore
  586. && !verify_dns_length(&cow, dns_length == DnsLength::VerifyAllowRootDot)
  587. {
  588. Err(crate::Errors::default())
  589. } else {
  590. Ok(cow)
  591. }
  592. }
  593. Err(ProcessingError::ValidityError) => Err(crate::Errors::default()),
  594. Err(ProcessingError::SinkError) => unreachable!(),
  595. }
  596. }
  597. /// Performs the [ToUnicode](https://www.unicode.org/reports/tr46/#ToUnicode) operation
  598. /// from UTS #46 according to the options given. When there
  599. /// are errors, there is still output, which may be rendered user, even through
  600. /// the output must not be used in networking protocols. Errors are denoted
  601. /// by U+FFFD REPLACEMENT CHARACTERs in the output. (That is, if the second item of the
  602. /// return tuple is `Err`, the first item of the return tuple is guaranteed to contain
  603. /// at least one U+FFFD.)
  604. ///
  605. /// Most applications probably shouldn't use this method and should be using
  606. /// [`Uts46::to_user_interface`] instead.
  607. ///
  608. /// # Arguments
  609. ///
  610. /// * `domain_name` - The input domain name as UTF-8 bytes. (The UTF-8ness is checked by
  611. /// this method and input that is not well-formed UTF-8 is treated as an error. If you
  612. /// already have a `&str`, call `.as_bytes()` on it.)
  613. /// * `ascii_deny_list` - What ASCII deny list, if any, to apply. The UTS 46
  614. /// _UseSTD3ASCIIRules_ flag or the WHATWG URL Standard forbidden domain code point
  615. /// processing is handled via this argument. Most callers are probably the best off
  616. /// by using [`AsciiDenyList::URL`] here.
  617. /// * `hyphens` - The UTS 46 _CheckHyphens_ flag. Most callers are probably the best
  618. /// off by using [`Hyphens::Allow`] here.
  619. pub fn to_unicode<'a>(
  620. &self,
  621. domain_name: &'a [u8],
  622. ascii_deny_list: AsciiDenyList,
  623. hyphens: Hyphens,
  624. ) -> (Cow<'a, str>, Result<(), crate::Errors>) {
  625. self.to_user_interface(domain_name, ascii_deny_list, hyphens, |_, _, _| true)
  626. }
  627. /// Performs the [ToUnicode](https://www.unicode.org/reports/tr46/#ToUnicode) operation
  628. /// from UTS #46 according to options given with some
  629. /// error-free Unicode labels output according to
  630. /// [ToASCII](https://www.unicode.org/reports/tr46/#ToASCII) instead as decided by
  631. /// application policy implemented via the `output_as_unicode` closure. The purpose
  632. /// is to convert user-visible domains to the Unicode form in general but to render
  633. /// potentially misleading labels as Punycode.
  634. ///
  635. /// This is an imperfect security mechanism, because [the Punycode form itself may be
  636. /// resemble a user-recognizable name](https://www.unicode.org/reports/tr36/#TablePunycodeSpoofing).
  637. /// However, since this mechanism is common practice, this API provides support for The
  638. /// the mechanism.
  639. ///
  640. /// ASCII labels always pass through as ASCII and labels with errors always pass through
  641. /// as Unicode. For non-erroneous labels that contain at least one non-ASCII character
  642. /// (implies non-empty), `output_as_unicode` is called with the Unicode form of the label,
  643. /// the TLD (potentially empty), and a flag indicating whether the domain name as a whole
  644. /// is a bidi domain name. If the return value is `true`, the label passes through as
  645. /// Unicode. If the return value is `false`, the label is converted to Punycode.
  646. ///
  647. /// When there are errors, there is still output, which may be rendered user, even through
  648. /// the output must not be used in networking protocols. Errors are denoted by
  649. /// U+FFFD REPLACEMENT CHARACTERs in the output. (That is, if the second item
  650. /// of the return tuple is `Err`, the first item of the return tuple is guaranteed to contain
  651. /// at least one U+FFFD.) Labels that contain errors are not converted to Punycode.
  652. ///
  653. /// # Arguments
  654. ///
  655. /// * `domain_name` - The input domain name as UTF-8 bytes. (The UTF-8ness is checked by
  656. /// this method and input that is not well-formed UTF-8 is treated as an error. If you
  657. /// already have a `&str`, call `.as_bytes()` on it.)
  658. /// * `ascii_deny_list` - What ASCII deny list, if any, to apply. The UTS 46
  659. /// _UseSTD3ASCIIRules_ flag or the WHATWG URL Standard forbidden domain code point
  660. /// processing is handled via this argument. Most callers are probably the best off
  661. /// by using [`AsciiDenyList::URL`] here.
  662. /// * `hyphens` - The UTS 46 _CheckHyphens_ flag. Most callers are probably the best
  663. /// off by using [`Hyphens::Allow`] here.
  664. /// * `output_as_unicode` - A closure for deciding if a label should be output as Unicode
  665. /// (as opposed to Punycode). The first argument is the label for which a decision is
  666. /// needed (always non-empty slice). The second argument is the TLD (potentially empty).
  667. /// The third argument is `true` iff the domain name as a whole is a bidi domain name.
  668. /// Only non-erroneous labels that contain at least one non-ASCII character are passed
  669. /// to the closure as the first argument. The second and third argument values are
  670. /// guaranteed to remain the same during a single call to `process`, and the closure
  671. /// may cache computations derived from the second and third argument (hence the
  672. /// `FnMut` type).
  673. pub fn to_user_interface<'a, OutputUnicode: FnMut(&[char], &[char], bool) -> bool>(
  674. &self,
  675. domain_name: &'a [u8],
  676. ascii_deny_list: AsciiDenyList,
  677. hyphens: Hyphens,
  678. output_as_unicode: OutputUnicode,
  679. ) -> (Cow<'a, str>, Result<(), crate::Errors>) {
  680. let mut s = String::new();
  681. match self.process(
  682. domain_name,
  683. ascii_deny_list,
  684. hyphens,
  685. ErrorPolicy::MarkErrors,
  686. output_as_unicode,
  687. &mut s,
  688. None,
  689. ) {
  690. // SAFETY: `ProcessingSuccess::Passthrough` asserts that `domain_name` is ASCII.
  691. Ok(ProcessingSuccess::Passthrough) => (
  692. Cow::Borrowed(unsafe { core::str::from_utf8_unchecked(domain_name) }),
  693. Ok(()),
  694. ),
  695. Ok(ProcessingSuccess::WroteToSink) => (Cow::Owned(s), Ok(())),
  696. Err(ProcessingError::ValidityError) => (Cow::Owned(s), Err(crate::Errors::default())),
  697. Err(ProcessingError::SinkError) => unreachable!(),
  698. }
  699. }
  700. /// The lower-level function that [`Uts46::to_ascii`], [`Uts46::to_unicode`], and
  701. /// [`Uts46::to_user_interface`] are built on to allow support for output types other
  702. /// than `Cow<'a, str>` (e.g. string types in a non-Rust programming language).
  703. ///
  704. /// # Arguments
  705. ///
  706. /// * `domain_name` - The input domain name as UTF-8 bytes. (The UTF-8ness is checked by
  707. /// this method and input that is not well-formed UTF-8 is treated as an error. If you
  708. /// already have a `&str`, call `.as_bytes()` on it.)
  709. /// * `ascii_deny_list` - What ASCII deny list, if any, to apply. The UTS 46
  710. /// _UseSTD3ASCIIRules_ flag or the WHATWG URL Standard forbidden domain code point
  711. /// processing is handled via this argument. Most callers are probably the best off
  712. /// by using [`AsciiDenyList::URL`] here.
  713. /// * `hyphens` - The UTS 46 _CheckHyphens_ flag. Most callers are probably the best
  714. /// off by using [`Hyphens::Allow`] here.
  715. /// * `error_policy` - Whether to fail fast or to produce output that may be rendered
  716. /// for the user to examine in case of errors.
  717. /// * `output_as_unicode` - A closure for deciding if a label should be output as Unicode
  718. /// (as opposed to Punycode). The first argument is the label for which a decision is
  719. /// needed (always non-empty slice). The second argument is the TLD (potentially empty).
  720. /// The third argument is `true` iff the domain name as a whole is a bidi domain name.
  721. /// Only non-erroneous labels that contain at least one non-ASCII character are passed
  722. /// to the closure as the first argument. The second and third argument values are
  723. /// guaranteed to remain the same during a single call to `process`, and the closure
  724. /// may cache computations derived from the second and third argument (hence the
  725. /// `FnMut` type). To perform the _ToASCII_ operation, `|_, _, _| false` must be
  726. /// passed as the closure. To perform the _ToUnicode_ operation, `|_, _, _| true` must
  727. /// be passed as the closure. A more complex closure may be used to prepare a domain
  728. /// name for display in a user interface so that labels are converted to the Unicode
  729. /// form in general but potentially misleading labels are converted to the Punycode
  730. /// form.
  731. /// `sink` - The object that receives the output (in the non-passthrough case).
  732. /// `ascii_sink` - A second sink that receives the _ToASCII_ form only if there
  733. /// were no errors and `sink` received at least one character of non-ASCII output.
  734. /// The purpose of this argument is to enable a user interface display form of the
  735. /// domain and the _ToASCII_ form of the domain to be computed efficiently together.
  736. /// This argument is useless when `output_as_unicode` always returns `false`, in
  737. /// which case the _ToASCII_ form ends up in `sink` already. If `ascii_sink` receives
  738. /// no output and the return value is `Ok(ProcessingSuccess::WroteToSink)`, use the
  739. /// output received by `sink` also as the _ToASCII_ result.
  740. ///
  741. /// # Return value
  742. ///
  743. /// * `Ok(ProcessingSuccess::Passthrough)` - The caller must treat
  744. /// `unsafe { core::str::from_utf8_unchecked(domain_name) }` as the output. (This
  745. /// return value asserts that calling `core::str::from_utf8_unchecked(domain_name)`
  746. /// is safe.)
  747. /// * `Ok(ProcessingSuccess::WroteToSink)` - The caller must treat was was written
  748. /// to `sink` as the output. If another sink was passed as `ascii_sink` but it did
  749. /// not receive output, the caller must treat what was written to `sink` also as
  750. /// the _ToASCII_ output. Otherwise, if `ascii_sink` received output, the caller
  751. /// must treat what was written to `ascii_sink` as the _ToASCII_ output.
  752. /// * `Err(ProcessingError::ValidityError)` - The input was in error and must
  753. /// not be used for DNS lookup or otherwise in a network protocol. If `error_policy`
  754. /// was `ErrorPolicy::MarkErrors`, the output written to `sink` may be displayed
  755. /// to the user as an illustration of where the error was or the errors were.
  756. /// * `Err(ProcessingError::SinkError)` - Either `sink` or `ascii_sink` returned
  757. /// [`core::fmt::Error`]. The partial output written to `sink` `ascii_sink` must not
  758. /// be used. If `W` never returns [`core::fmt::Error`], this method never returns
  759. /// `Err(ProcessingError::SinkError)`.
  760. ///
  761. /// # Safety-usable invariant
  762. ///
  763. /// If the return value is `Ok(ProcessingSuccess::Passthrough)`, `domain_name` is
  764. /// ASCII and `core::str::from_utf8_unchecked(domain_name)` is safe. (Note:
  765. /// Other return values do _not_ imply that `domain_name` wasn't ASCII!)
  766. ///
  767. /// # Security considerations
  768. ///
  769. /// Showing labels whose Unicode form might mislead the user as Punycode instead is
  770. /// an imperfect security mechanism, because [the Punycode form itself may be resemble
  771. /// a user-recognizable name](https://www.unicode.org/reports/tr36/#TablePunycodeSpoofing).
  772. /// However, since this mechanism is common practice, this API provides support for the
  773. /// the mechanism.
  774. ///
  775. /// Punycode processing is quadratic, so to avoid denial of service, this method imposes
  776. /// length limits on Punycode treating especially long inputs as being in error. These
  777. /// limits are well higher than the DNS length limits and are not more restrictive than
  778. /// the limits imposed by ICU4C.
  779. #[allow(clippy::too_many_arguments)]
  780. pub fn process<W: Write + ?Sized, OutputUnicode: FnMut(&[char], &[char], bool) -> bool>(
  781. &self,
  782. domain_name: &[u8],
  783. ascii_deny_list: AsciiDenyList,
  784. hyphens: Hyphens,
  785. error_policy: ErrorPolicy,
  786. mut output_as_unicode: OutputUnicode,
  787. sink: &mut W,
  788. ascii_sink: Option<&mut W>,
  789. ) -> Result<ProcessingSuccess, ProcessingError> {
  790. let fail_fast = error_policy == ErrorPolicy::FailFast;
  791. let mut domain_buffer = SmallVec::<[char; 253]>::new();
  792. let mut already_punycode = SmallVec::<[AlreadyAsciiLabel; 8]>::new();
  793. // `process_inner` could be pasted inline here, but it's out of line in order
  794. // to avoid duplicating that code when monomorphizing over `W` and `OutputUnicode`.
  795. let (passthrough_up_to, is_bidi, had_errors) = self.process_inner(
  796. domain_name,
  797. ascii_deny_list,
  798. hyphens,
  799. fail_fast,
  800. &mut domain_buffer,
  801. &mut already_punycode,
  802. );
  803. if passthrough_up_to == domain_name.len() {
  804. debug_assert!(!had_errors);
  805. return Ok(ProcessingSuccess::Passthrough);
  806. }
  807. // Checked only after passthrough as a micro optimization.
  808. if fail_fast && had_errors {
  809. return Err(ProcessingError::ValidityError);
  810. }
  811. debug_assert_eq!(had_errors, domain_buffer.contains(&'\u{FFFD}'));
  812. let without_dot = if let Some(without_dot) = domain_buffer.strip_suffix(&['.']) {
  813. without_dot
  814. } else {
  815. &domain_buffer[..]
  816. };
  817. // unwrap is OK, because we always have at least one label
  818. let tld = without_dot.rsplit(|c| *c == '.').next().unwrap();
  819. let mut had_unicode_output = false;
  820. let mut seen_label = false;
  821. let mut already_punycode_iter = already_punycode.iter();
  822. let mut passthrough_up_to_extended = passthrough_up_to;
  823. let mut flushed_prefix = false;
  824. for label in domain_buffer.split(|c| *c == '.') {
  825. // Unwrap is OK, because there are supposed to be as many items in
  826. // `already_punycode` as there are labels.
  827. let input_punycode = *already_punycode_iter.next().unwrap();
  828. if seen_label {
  829. if flushed_prefix {
  830. sink.write_char('.')?;
  831. } else {
  832. debug_assert_eq!(domain_name[passthrough_up_to_extended], b'.');
  833. passthrough_up_to_extended += 1;
  834. if passthrough_up_to_extended == domain_name.len() {
  835. debug_assert!(!had_errors);
  836. return Ok(ProcessingSuccess::Passthrough);
  837. }
  838. }
  839. }
  840. seen_label = true;
  841. if let AlreadyAsciiLabel::MixedCaseAscii(mixed_case) = input_punycode {
  842. if let Some(first_upper_case) =
  843. mixed_case.iter().position(|c| c.is_ascii_uppercase())
  844. {
  845. let (head, tail) = mixed_case.split_at(first_upper_case);
  846. let slice_to_write = if flushed_prefix {
  847. head
  848. } else {
  849. flushed_prefix = true;
  850. passthrough_up_to_extended += head.len();
  851. debug_assert_ne!(passthrough_up_to_extended, domain_name.len());
  852. &domain_name[..passthrough_up_to_extended]
  853. };
  854. // SAFETY: `mixed_case` and `domain_name` up to `passthrough_up_to_extended` are known to be ASCII.
  855. sink.write_str(unsafe { core::str::from_utf8_unchecked(slice_to_write) })?;
  856. for c in tail.iter() {
  857. sink.write_char(char::from(c.to_ascii_lowercase()))?;
  858. }
  859. } else if flushed_prefix {
  860. // SAFETY: `mixed_case` is known to be ASCII.
  861. sink.write_str(unsafe { core::str::from_utf8_unchecked(mixed_case) })?;
  862. } else {
  863. passthrough_up_to_extended += mixed_case.len();
  864. if passthrough_up_to_extended == domain_name.len() {
  865. debug_assert!(!had_errors);
  866. return Ok(ProcessingSuccess::Passthrough);
  867. }
  868. }
  869. continue;
  870. }
  871. let potentially_punycode = if fail_fast {
  872. debug_assert!(classify_for_punycode(label) != PunycodeClassification::Error);
  873. !is_ascii(label)
  874. } else {
  875. classify_for_punycode(label) == PunycodeClassification::Unicode
  876. };
  877. let passthrough = if potentially_punycode {
  878. let unicode = output_as_unicode(label, tld, is_bidi);
  879. had_unicode_output |= unicode;
  880. unicode
  881. } else {
  882. true
  883. };
  884. if passthrough {
  885. if !flushed_prefix {
  886. flushed_prefix = true;
  887. // SAFETY: `domain_name` up to `passthrough_up_to_extended` is known to be ASCII.
  888. sink.write_str(unsafe {
  889. core::str::from_utf8_unchecked(&domain_name[..passthrough_up_to_extended])
  890. })?;
  891. }
  892. for c in label.iter().copied() {
  893. sink.write_char(c)?;
  894. }
  895. } else if let AlreadyAsciiLabel::MixedCasePunycode(mixed_case) = input_punycode {
  896. if let Some(first_upper_case) =
  897. mixed_case.iter().position(|c| c.is_ascii_uppercase())
  898. {
  899. let (head, tail) = mixed_case.split_at(first_upper_case);
  900. let slice_to_write = if flushed_prefix {
  901. head
  902. } else {
  903. flushed_prefix = true;
  904. passthrough_up_to_extended += head.len();
  905. debug_assert_ne!(passthrough_up_to_extended, domain_name.len());
  906. &domain_name[..passthrough_up_to_extended]
  907. };
  908. // SAFETY: `mixed_case` and `domain_name` up to `passthrough_up_to_extended` are known to be ASCII.
  909. sink.write_str(unsafe { core::str::from_utf8_unchecked(slice_to_write) })?;
  910. for c in tail.iter() {
  911. sink.write_char(char::from(c.to_ascii_lowercase()))?;
  912. }
  913. } else if flushed_prefix {
  914. // SAFETY: `mixed_case` is known to be ASCII.
  915. sink.write_str(unsafe { core::str::from_utf8_unchecked(mixed_case) })?;
  916. } else {
  917. passthrough_up_to_extended += mixed_case.len();
  918. if passthrough_up_to_extended == domain_name.len() {
  919. debug_assert!(!had_errors);
  920. return Ok(ProcessingSuccess::Passthrough);
  921. }
  922. }
  923. } else {
  924. if !flushed_prefix {
  925. flushed_prefix = true;
  926. // SAFETY: `domain_name` up to `passthrough_up_to_extended` is known to be ASCII.
  927. sink.write_str(unsafe {
  928. core::str::from_utf8_unchecked(&domain_name[..passthrough_up_to_extended])
  929. })?;
  930. }
  931. write_punycode_label(label, sink)?;
  932. }
  933. }
  934. if had_errors {
  935. return Err(ProcessingError::ValidityError);
  936. }
  937. if had_unicode_output {
  938. if let Some(sink) = ascii_sink {
  939. let mut seen_label = false;
  940. let mut already_punycode_iter = already_punycode.iter();
  941. let mut passthrough_up_to_extended = passthrough_up_to;
  942. let mut flushed_prefix = false;
  943. for label in domain_buffer.split(|c| *c == '.') {
  944. // Unwrap is OK, because there are supposed to be as many items in
  945. // `already_punycode` as there are labels.
  946. let input_punycode = *already_punycode_iter.next().unwrap();
  947. if seen_label {
  948. if flushed_prefix {
  949. sink.write_char('.')?;
  950. } else {
  951. debug_assert_eq!(domain_name[passthrough_up_to_extended], b'.');
  952. passthrough_up_to_extended += 1;
  953. }
  954. }
  955. seen_label = true;
  956. if let AlreadyAsciiLabel::MixedCaseAscii(mixed_case) = input_punycode {
  957. if let Some(first_upper_case) =
  958. mixed_case.iter().position(|c| c.is_ascii_uppercase())
  959. {
  960. let (head, tail) = mixed_case.split_at(first_upper_case);
  961. let slice_to_write = if flushed_prefix {
  962. head
  963. } else {
  964. flushed_prefix = true;
  965. passthrough_up_to_extended += head.len();
  966. debug_assert_ne!(passthrough_up_to_extended, domain_name.len());
  967. &domain_name[..passthrough_up_to_extended]
  968. };
  969. // SAFETY: `mixed_case` and `domain_name` up to `passthrough_up_to_extended` are known to be ASCII.
  970. sink.write_str(unsafe {
  971. core::str::from_utf8_unchecked(slice_to_write)
  972. })?;
  973. for c in tail.iter() {
  974. sink.write_char(char::from(c.to_ascii_lowercase()))?;
  975. }
  976. } else if flushed_prefix {
  977. // SAFETY: `mixed_case` is known to be ASCII.
  978. sink.write_str(unsafe { core::str::from_utf8_unchecked(mixed_case) })?;
  979. } else {
  980. passthrough_up_to_extended += mixed_case.len();
  981. }
  982. continue;
  983. }
  984. if is_ascii(label) {
  985. if !flushed_prefix {
  986. flushed_prefix = true;
  987. // SAFETY: `domain_name` up to `passthrough_up_to_extended` is known to be ASCII.
  988. sink.write_str(unsafe {
  989. core::str::from_utf8_unchecked(
  990. &domain_name[..passthrough_up_to_extended],
  991. )
  992. })?;
  993. }
  994. for c in label.iter().copied() {
  995. sink.write_char(c)?;
  996. }
  997. } else if let AlreadyAsciiLabel::MixedCasePunycode(mixed_case) = input_punycode
  998. {
  999. if let Some(first_upper_case) =
  1000. mixed_case.iter().position(|c| c.is_ascii_uppercase())
  1001. {
  1002. let (head, tail) = mixed_case.split_at(first_upper_case);
  1003. let slice_to_write = if flushed_prefix {
  1004. head
  1005. } else {
  1006. flushed_prefix = true;
  1007. passthrough_up_to_extended += head.len();
  1008. debug_assert_ne!(passthrough_up_to_extended, domain_name.len());
  1009. &domain_name[..passthrough_up_to_extended]
  1010. };
  1011. // SAFETY: `mixed_case` and `domain_name` up to `passthrough_up_to_extended` are known to be ASCII.
  1012. sink.write_str(unsafe {
  1013. core::str::from_utf8_unchecked(slice_to_write)
  1014. })?;
  1015. for c in tail.iter() {
  1016. sink.write_char(char::from(c.to_ascii_lowercase()))?;
  1017. }
  1018. } else if flushed_prefix {
  1019. // SAFETY: `mixed_case` is known to be ASCII.
  1020. sink.write_str(unsafe { core::str::from_utf8_unchecked(mixed_case) })?;
  1021. } else {
  1022. passthrough_up_to_extended += mixed_case.len();
  1023. }
  1024. } else {
  1025. if !flushed_prefix {
  1026. flushed_prefix = true;
  1027. // SAFETY: `domain_name` up to `passthrough_up_to_extended` is known to be ASCII.
  1028. sink.write_str(unsafe {
  1029. core::str::from_utf8_unchecked(
  1030. &domain_name[..passthrough_up_to_extended],
  1031. )
  1032. })?;
  1033. }
  1034. write_punycode_label(label, sink)?;
  1035. }
  1036. }
  1037. if !flushed_prefix {
  1038. // SAFETY: `domain_name` up to `passthrough_up_to_extended` is known to be ASCII.
  1039. sink.write_str(unsafe {
  1040. core::str::from_utf8_unchecked(&domain_name[..passthrough_up_to_extended])
  1041. })?;
  1042. }
  1043. }
  1044. }
  1045. Ok(ProcessingSuccess::WroteToSink)
  1046. }
  1047. /// The part of `process` that doesn't need to be generic over the sink and
  1048. /// can avoid monomorphizing in the interest of code size.
  1049. #[inline(never)]
  1050. fn process_inner<'a>(
  1051. &self,
  1052. domain_name: &'a [u8],
  1053. ascii_deny_list: AsciiDenyList,
  1054. hyphens: Hyphens,
  1055. fail_fast: bool,
  1056. domain_buffer: &mut SmallVec<[char; 253]>,
  1057. already_punycode: &mut SmallVec<[AlreadyAsciiLabel<'a>; 8]>,
  1058. ) -> (usize, bool, bool) {
  1059. // Sadly, this even faster-path ASCII tier is needed to avoid regressing
  1060. // performance.
  1061. let mut iter = domain_name.iter();
  1062. let mut most_recent_label_start = iter.clone();
  1063. let tail = loop {
  1064. if let Some(&b) = iter.next() {
  1065. if in_inclusive_range8(b, b'a', b'z') {
  1066. continue;
  1067. }
  1068. if b == b'.' {
  1069. most_recent_label_start = iter.clone();
  1070. continue;
  1071. }
  1072. break most_recent_label_start.as_slice();
  1073. } else {
  1074. // Success! The whole input passes through on the fastest path!
  1075. return (domain_name.len(), false, false);
  1076. }
  1077. };
  1078. let deny_list = ascii_deny_list.bits;
  1079. let deny_list_deny_dot = deny_list | DOT_MASK;
  1080. let mut had_errors = false;
  1081. let mut passthrough_up_to = domain_name.len() - tail.len(); // Index into `domain_name`
  1082. // 253 ASCII characters is the max length for a valid domain name
  1083. // (excluding the root dot).
  1084. let mut current_label_start; // Index into `domain_buffer`
  1085. let mut seen_label = false;
  1086. let mut in_prefix = true;
  1087. for label in tail.split(|b| *b == b'.') {
  1088. // We check for passthrough only for the prefix. That is, if we
  1089. // haven't moved on and started filling `domain_buffer`. Keeping
  1090. // this stuff in one loop where the first items keep being skipped
  1091. // once they have been skipped at least once instead of working
  1092. // this into a fancier loop structure in order to make sure that
  1093. // no item from the iterator is lost or processed twice.
  1094. // Furthermore, after the passthrough fails, restarting the
  1095. // normalization process after each pre-existing ASCII dot also
  1096. // provides an opportunity for the processing to get back onto
  1097. // an ASCII fast path that bypasses the normalizer for ASCII
  1098. // after a pre-existing ASCII dot (pre-existing in the sense
  1099. // of not coming from e.g. normalizing an ideographic dot).
  1100. if in_prefix && is_passthrough_ascii_label(label) {
  1101. if seen_label {
  1102. debug_assert_eq!(domain_name[passthrough_up_to], b'.');
  1103. passthrough_up_to += 1;
  1104. }
  1105. seen_label = true;
  1106. passthrough_up_to += label.len();
  1107. continue;
  1108. }
  1109. if seen_label {
  1110. if in_prefix {
  1111. debug_assert_eq!(domain_name[passthrough_up_to], b'.');
  1112. passthrough_up_to += 1;
  1113. } else {
  1114. domain_buffer.push('.');
  1115. }
  1116. }
  1117. seen_label = true;
  1118. in_prefix = false;
  1119. current_label_start = domain_buffer.len();
  1120. if !label.is_empty() {
  1121. let (ascii, non_ascii) = split_ascii_fast_path_prefix(label);
  1122. let non_punycode_ascii_label = if non_ascii.is_empty() {
  1123. if has_punycode_prefix(ascii) {
  1124. if (ascii.last() != Some(&b'-'))
  1125. && (ascii.len() - 4 <= PUNYCODE_DECODE_MAX_INPUT_LENGTH)
  1126. {
  1127. if let Ok(decode) =
  1128. Decoder::default().decode::<u8, InternalCaller>(&ascii[4..])
  1129. {
  1130. // 63 ASCII characters is the max length for a valid DNS label and xn-- takes 4
  1131. // characters.
  1132. let mut label_buffer = SmallVec::<[char; 59]>::new();
  1133. label_buffer.extend(decode);
  1134. if self.after_punycode_decode(
  1135. domain_buffer,
  1136. current_label_start,
  1137. &label_buffer,
  1138. deny_list_deny_dot,
  1139. fail_fast,
  1140. &mut had_errors,
  1141. ) {
  1142. return (0, false, true);
  1143. }
  1144. if self.check_label(
  1145. hyphens,
  1146. &mut domain_buffer[current_label_start..],
  1147. fail_fast,
  1148. &mut had_errors,
  1149. true,
  1150. true,
  1151. ) {
  1152. return (0, false, true);
  1153. }
  1154. } else {
  1155. // Punycode failed
  1156. if fail_fast {
  1157. return (0, false, true);
  1158. }
  1159. had_errors = true;
  1160. domain_buffer.push('\u{FFFD}');
  1161. let mut iter = ascii.iter();
  1162. // Discard the first character that we replaced.
  1163. let _ = iter.next();
  1164. domain_buffer.extend(iter.map(|c| {
  1165. // Can't have dot here, so `deny_list` vs `deny_list_deny_dot` does
  1166. // not matter.
  1167. apply_ascii_deny_list_to_potentially_upper_case_ascii(
  1168. *c, deny_list,
  1169. )
  1170. }));
  1171. };
  1172. // If there were errors, we won't be trying to use this
  1173. // anyway later, so it's fine to put it here unconditionally.
  1174. already_punycode.push(AlreadyAsciiLabel::MixedCasePunycode(label));
  1175. continue;
  1176. } else if fail_fast {
  1177. return (0, false, true);
  1178. }
  1179. // Else fall through to the complex path and rediscover error
  1180. // there.
  1181. false
  1182. } else {
  1183. true
  1184. }
  1185. } else {
  1186. false
  1187. };
  1188. for c in ascii.iter().map(|c| {
  1189. // Can't have dot here, so `deny_list` vs `deny_list_deny_dot` does
  1190. // not matter.
  1191. apply_ascii_deny_list_to_potentially_upper_case_ascii(*c, deny_list)
  1192. }) {
  1193. if c == '\u{FFFD}' {
  1194. if fail_fast {
  1195. return (0, false, true);
  1196. }
  1197. had_errors = true;
  1198. }
  1199. domain_buffer.push(c);
  1200. }
  1201. if non_punycode_ascii_label {
  1202. if hyphens != Hyphens::Allow
  1203. && check_hyphens(
  1204. &mut domain_buffer[current_label_start..],
  1205. hyphens == Hyphens::CheckFirstLast,
  1206. fail_fast,
  1207. &mut had_errors,
  1208. )
  1209. {
  1210. return (0, false, true);
  1211. }
  1212. already_punycode.push(if had_errors {
  1213. AlreadyAsciiLabel::Other
  1214. } else {
  1215. AlreadyAsciiLabel::MixedCaseAscii(label)
  1216. });
  1217. continue;
  1218. }
  1219. already_punycode.push(AlreadyAsciiLabel::Other);
  1220. let mut first_needs_combining_mark_check = ascii.is_empty();
  1221. let mut needs_contextj_check = !non_ascii.is_empty();
  1222. let mut mapping = self
  1223. .mapper
  1224. .map_normalize(non_ascii.chars())
  1225. .map(|c| apply_ascii_deny_list_to_lower_cased_unicode(c, deny_list));
  1226. loop {
  1227. let n = mapping.next();
  1228. match n {
  1229. None | Some('.') => {
  1230. if domain_buffer[current_label_start..]
  1231. .starts_with(&['x', 'n', '-', '-'])
  1232. {
  1233. let mut punycode_precondition_failed = false;
  1234. for c in domain_buffer[current_label_start + 4..].iter_mut() {
  1235. if !c.is_ascii() {
  1236. if fail_fast {
  1237. return (0, false, true);
  1238. }
  1239. had_errors = true;
  1240. *c = '\u{FFFD}';
  1241. punycode_precondition_failed = true;
  1242. }
  1243. }
  1244. if let Some(last) = domain_buffer.last_mut() {
  1245. if *last == '-' {
  1246. // Either there's nothing after the "xn--" prefix
  1247. // and we got the last hyphen of "xn--", or there
  1248. // are no Punycode digits after the last delimiter
  1249. // which would result in Punycode decode outputting
  1250. // ASCII only.
  1251. if fail_fast {
  1252. return (0, false, true);
  1253. }
  1254. had_errors = true;
  1255. *last = '\u{FFFD}';
  1256. punycode_precondition_failed = true;
  1257. }
  1258. } else {
  1259. unreachable!();
  1260. }
  1261. // Reject excessively long input
  1262. // https://github.com/whatwg/url/issues/824
  1263. // https://unicode-org.atlassian.net/browse/ICU-13727
  1264. if domain_buffer.len() - current_label_start - 4
  1265. > PUNYCODE_DECODE_MAX_INPUT_LENGTH
  1266. {
  1267. if fail_fast {
  1268. return (0, false, true);
  1269. }
  1270. had_errors = true;
  1271. domain_buffer[current_label_start
  1272. + 4
  1273. + PUNYCODE_DECODE_MAX_INPUT_LENGTH] = '\u{FFFD}';
  1274. punycode_precondition_failed = true;
  1275. }
  1276. if !punycode_precondition_failed {
  1277. if let Ok(decode) = Decoder::default()
  1278. .decode::<char, InternalCaller>(
  1279. &domain_buffer[current_label_start + 4..],
  1280. )
  1281. {
  1282. first_needs_combining_mark_check = true;
  1283. needs_contextj_check = true;
  1284. // 63 ASCII characters is the max length for a valid DNS label and xn-- takes 4
  1285. // characters.
  1286. let mut label_buffer = SmallVec::<[char; 59]>::new();
  1287. label_buffer.extend(decode);
  1288. domain_buffer.truncate(current_label_start);
  1289. if self.after_punycode_decode(
  1290. domain_buffer,
  1291. current_label_start,
  1292. &label_buffer,
  1293. deny_list_deny_dot,
  1294. fail_fast,
  1295. &mut had_errors,
  1296. ) {
  1297. return (0, false, true);
  1298. }
  1299. } else {
  1300. // Punycode failed
  1301. if fail_fast {
  1302. return (0, false, true);
  1303. }
  1304. had_errors = true;
  1305. domain_buffer[current_label_start] = '\u{FFFD}';
  1306. needs_contextj_check = false; // ASCII label
  1307. first_needs_combining_mark_check = false;
  1308. };
  1309. } else {
  1310. first_needs_combining_mark_check = false;
  1311. needs_contextj_check = false; // Non-ASCII already turned to U+FFFD.
  1312. }
  1313. }
  1314. if self.check_label(
  1315. hyphens,
  1316. &mut domain_buffer[current_label_start..],
  1317. fail_fast,
  1318. &mut had_errors,
  1319. first_needs_combining_mark_check,
  1320. needs_contextj_check,
  1321. ) {
  1322. return (0, false, true);
  1323. }
  1324. if n.is_none() {
  1325. break;
  1326. }
  1327. domain_buffer.push('.');
  1328. current_label_start = domain_buffer.len();
  1329. first_needs_combining_mark_check = true;
  1330. needs_contextj_check = true;
  1331. already_punycode.push(AlreadyAsciiLabel::Other);
  1332. }
  1333. Some(c) => {
  1334. if c == '\u{FFFD}' {
  1335. if fail_fast {
  1336. return (0, false, true);
  1337. }
  1338. had_errors = true;
  1339. }
  1340. domain_buffer.push(c);
  1341. }
  1342. }
  1343. }
  1344. } else {
  1345. // Empty label
  1346. already_punycode.push(AlreadyAsciiLabel::MixedCaseAscii(label));
  1347. }
  1348. }
  1349. let is_bidi = self.is_bidi(domain_buffer);
  1350. if is_bidi {
  1351. for label in domain_buffer.split_mut(|c| *c == '.') {
  1352. if let Some((first, tail)) = label.split_first_mut() {
  1353. let first_bc = self.bidi_class.get(*first);
  1354. if (FIRST_BC_MASK & bidi_class_to_mask(first_bc)) == 0 {
  1355. // Neither RTL label nor LTR label
  1356. if fail_fast {
  1357. return (0, false, true);
  1358. }
  1359. had_errors = true;
  1360. *first = '\u{FFFD}';
  1361. continue;
  1362. }
  1363. let is_ltr = first_bc == BidiClass::LeftToRight;
  1364. // Trim NSM
  1365. let mut middle = tail;
  1366. #[allow(clippy::while_let_loop)]
  1367. loop {
  1368. if let Some((last, prior)) = middle.split_last_mut() {
  1369. let last_bc = self.bidi_class.get(*last);
  1370. if last_bc == BidiClass::NonspacingMark {
  1371. middle = prior;
  1372. continue;
  1373. }
  1374. let last_mask = if is_ltr { LAST_LTR_MASK } else { LAST_RTL_MASK };
  1375. if (bidi_class_to_mask(last_bc) & last_mask) == 0 {
  1376. if fail_fast {
  1377. return (0, false, true);
  1378. }
  1379. had_errors = true;
  1380. *last = '\u{FFFD}';
  1381. }
  1382. if is_ltr {
  1383. for c in prior.iter_mut() {
  1384. let bc = self.bidi_class.get(*c);
  1385. if (bidi_class_to_mask(bc) & MIDDLE_LTR_MASK) == 0 {
  1386. if fail_fast {
  1387. return (0, false, true);
  1388. }
  1389. had_errors = true;
  1390. *c = '\u{FFFD}';
  1391. }
  1392. }
  1393. } else {
  1394. let mut numeral_state = RtlNumeralState::Undecided;
  1395. for c in prior.iter_mut() {
  1396. let bc = self.bidi_class.get(*c);
  1397. if (bidi_class_to_mask(bc) & MIDDLE_RTL_MASK) == 0 {
  1398. if fail_fast {
  1399. return (0, false, true);
  1400. }
  1401. had_errors = true;
  1402. *c = '\u{FFFD}';
  1403. } else {
  1404. match numeral_state {
  1405. RtlNumeralState::Undecided => {
  1406. if bc == BidiClass::EuropeanNumber {
  1407. numeral_state = RtlNumeralState::European;
  1408. } else if bc == BidiClass::ArabicNumber {
  1409. numeral_state = RtlNumeralState::Arabic;
  1410. }
  1411. }
  1412. RtlNumeralState::European => {
  1413. if bc == BidiClass::ArabicNumber {
  1414. if fail_fast {
  1415. return (0, false, true);
  1416. }
  1417. had_errors = true;
  1418. *c = '\u{FFFD}';
  1419. }
  1420. }
  1421. RtlNumeralState::Arabic => {
  1422. if bc == BidiClass::EuropeanNumber {
  1423. if fail_fast {
  1424. return (0, false, true);
  1425. }
  1426. had_errors = true;
  1427. *c = '\u{FFFD}';
  1428. }
  1429. }
  1430. }
  1431. }
  1432. }
  1433. if (numeral_state == RtlNumeralState::European
  1434. && last_bc == BidiClass::ArabicNumber)
  1435. || (numeral_state == RtlNumeralState::Arabic
  1436. && last_bc == BidiClass::EuropeanNumber)
  1437. {
  1438. if fail_fast {
  1439. return (0, false, true);
  1440. }
  1441. had_errors = true;
  1442. *last = '\u{FFFD}';
  1443. }
  1444. }
  1445. break;
  1446. } else {
  1447. // One-character label or label where
  1448. // everything after the first character
  1449. // is just non-spacing marks.
  1450. break;
  1451. }
  1452. }
  1453. }
  1454. }
  1455. }
  1456. (passthrough_up_to, is_bidi, had_errors)
  1457. }
  1458. #[inline(never)]
  1459. fn after_punycode_decode(
  1460. &self,
  1461. domain_buffer: &mut SmallVec<[char; 253]>,
  1462. current_label_start: usize,
  1463. label_buffer: &[char],
  1464. deny_list_deny_dot: u128,
  1465. fail_fast: bool,
  1466. had_errors: &mut bool,
  1467. ) -> bool {
  1468. for c in self
  1469. .mapper
  1470. .normalize_validate(label_buffer.iter().copied())
  1471. .map(|c| apply_ascii_deny_list_to_lower_cased_unicode(c, deny_list_deny_dot))
  1472. {
  1473. if c == '\u{FFFD}' {
  1474. if fail_fast {
  1475. return true;
  1476. }
  1477. *had_errors = true;
  1478. }
  1479. domain_buffer.push(c);
  1480. }
  1481. let normalized = &mut domain_buffer[current_label_start..];
  1482. if let Err(()) =
  1483. normalized
  1484. .iter_mut()
  1485. .zip(label_buffer.iter())
  1486. .try_for_each(|(norm_c, decoded_c)| {
  1487. if *norm_c == *decoded_c {
  1488. Ok(())
  1489. } else {
  1490. // Mark the first difference
  1491. *norm_c = '\u{FFFD}';
  1492. Err(())
  1493. }
  1494. })
  1495. {
  1496. if fail_fast {
  1497. return true;
  1498. }
  1499. *had_errors = true;
  1500. }
  1501. false
  1502. }
  1503. #[inline(never)]
  1504. fn check_label(
  1505. &self,
  1506. hyphens: Hyphens,
  1507. mut_label: &mut [char],
  1508. fail_fast: bool,
  1509. had_errors: &mut bool,
  1510. first_needs_combining_mark_check: bool,
  1511. needs_contextj_check: bool,
  1512. ) -> bool {
  1513. if hyphens != Hyphens::Allow
  1514. && check_hyphens(
  1515. mut_label,
  1516. hyphens == Hyphens::CheckFirstLast,
  1517. fail_fast,
  1518. had_errors,
  1519. )
  1520. {
  1521. return true;
  1522. }
  1523. if first_needs_combining_mark_check {
  1524. if let Some(first) = mut_label.first_mut() {
  1525. if (general_category_to_mask(self.general_category.get(*first)) & MARK_MASK) != 0 {
  1526. if fail_fast {
  1527. return true;
  1528. }
  1529. *had_errors = true;
  1530. *first = '\u{FFFD}';
  1531. }
  1532. }
  1533. }
  1534. if needs_contextj_check {
  1535. // ContextJ
  1536. for i in 0..mut_label.len() {
  1537. let c = mut_label[i];
  1538. if !in_inclusive_range_char(c, '\u{200C}', '\u{200D}') {
  1539. continue;
  1540. }
  1541. let (head, joiner_and_tail) = mut_label.split_at_mut(i);
  1542. if let Some((joiner, tail)) = joiner_and_tail.split_first_mut() {
  1543. if let Some(previous) = head.last() {
  1544. if self.canonical_combining_class.get(*previous)
  1545. == CanonicalCombiningClass::Virama
  1546. {
  1547. continue;
  1548. }
  1549. } else {
  1550. // No preceding character
  1551. if fail_fast {
  1552. return true;
  1553. }
  1554. *had_errors = true;
  1555. *joiner = '\u{FFFD}';
  1556. continue;
  1557. }
  1558. if c == '\u{200D}' {
  1559. // ZWJ only has the virama rule
  1560. if fail_fast {
  1561. return true;
  1562. }
  1563. *had_errors = true;
  1564. *joiner = '\u{FFFD}';
  1565. continue;
  1566. }
  1567. debug_assert_eq!(c, '\u{200C}');
  1568. if !self.has_appropriately_joining_char(
  1569. head.iter().rev().copied(),
  1570. LEFT_OR_DUAL_JOINING_MASK,
  1571. ) || !self.has_appropriately_joining_char(
  1572. tail.iter().copied(),
  1573. RIGHT_OR_DUAL_JOINING_MASK,
  1574. ) {
  1575. if fail_fast {
  1576. return true;
  1577. }
  1578. *had_errors = true;
  1579. *joiner = '\u{FFFD}';
  1580. }
  1581. } else {
  1582. debug_assert!(false);
  1583. }
  1584. }
  1585. }
  1586. if !is_ascii(mut_label) && mut_label.len() > PUNYCODE_ENCODE_MAX_INPUT_LENGTH {
  1587. // Limit quadratic behavior
  1588. // https://github.com/whatwg/url/issues/824
  1589. // https://unicode-org.atlassian.net/browse/ICU-13727
  1590. if fail_fast {
  1591. return true;
  1592. }
  1593. *had_errors = true;
  1594. mut_label[PUNYCODE_ENCODE_MAX_INPUT_LENGTH] = '\u{FFFD}';
  1595. }
  1596. false
  1597. }
  1598. #[inline(always)]
  1599. fn has_appropriately_joining_char<I: Iterator<Item = char>>(
  1600. &self,
  1601. iter: I,
  1602. required_mask: u32,
  1603. ) -> bool {
  1604. for c in iter {
  1605. let jt = self.joining_type.get(c);
  1606. if (joining_type_to_mask(jt) & required_mask) != 0 {
  1607. return true;
  1608. }
  1609. if jt == JoiningType::Transparent {
  1610. continue;
  1611. }
  1612. return false;
  1613. }
  1614. false
  1615. }
  1616. #[inline(always)]
  1617. fn is_bidi(&self, buffer: &[char]) -> bool {
  1618. for &c in buffer {
  1619. if c < '\u{0590}' {
  1620. // Below Hebrew
  1621. continue;
  1622. }
  1623. if in_inclusive_range_char(c, '\u{0900}', '\u{FB1C}') {
  1624. debug_assert_ne!(c, '\u{200F}'); // disallowed
  1625. continue;
  1626. }
  1627. if in_inclusive_range_char(c, '\u{1F000}', '\u{3FFFF}') {
  1628. continue;
  1629. }
  1630. if in_inclusive_range_char(c, '\u{FF00}', '\u{107FF}') {
  1631. continue;
  1632. }
  1633. if in_inclusive_range_char(c, '\u{11000}', '\u{1E7FF}') {
  1634. continue;
  1635. }
  1636. if (RTL_MASK & bidi_class_to_mask(self.bidi_class.get(c))) != 0 {
  1637. return true;
  1638. }
  1639. }
  1640. false
  1641. }
  1642. }
  1643. fn check_hyphens(
  1644. mut_label: &mut [char],
  1645. allow_third_fourth: bool,
  1646. fail_fast: bool,
  1647. had_errors: &mut bool,
  1648. ) -> bool {
  1649. if let Some(first) = mut_label.first_mut() {
  1650. if *first == '-' {
  1651. if fail_fast {
  1652. return true;
  1653. }
  1654. *had_errors = true;
  1655. *first = '\u{FFFD}';
  1656. }
  1657. }
  1658. if let Some(last) = mut_label.last_mut() {
  1659. if *last == '-' {
  1660. if fail_fast {
  1661. return true;
  1662. }
  1663. *had_errors = true;
  1664. *last = '\u{FFFD}';
  1665. }
  1666. }
  1667. if allow_third_fourth {
  1668. return false;
  1669. }
  1670. if mut_label.len() >= 4 && mut_label[2] == '-' && mut_label[3] == '-' {
  1671. if fail_fast {
  1672. return true;
  1673. }
  1674. *had_errors = true;
  1675. mut_label[2] = '\u{FFFD}';
  1676. mut_label[3] = '\u{FFFD}';
  1677. }
  1678. false
  1679. }