uts46.rs 73 KB

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