summaryrefslogtreecommitdiffstats
path: root/netwerk/dns/nsIDNService.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
commit43a97878ce14b72f0981164f87f2e35e14151312 (patch)
tree620249daf56c0258faa40cbdcf9cfba06de2a846 /netwerk/dns/nsIDNService.cpp
parentInitial commit. (diff)
downloadfirefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz
firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--netwerk/dns/nsIDNService.cpp848
1 files changed, 848 insertions, 0 deletions
diff --git a/netwerk/dns/nsIDNService.cpp b/netwerk/dns/nsIDNService.cpp
new file mode 100644
index 0000000000..5455cff38a
--- /dev/null
+++ b/netwerk/dns/nsIDNService.cpp
@@ -0,0 +1,848 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MainThreadUtils.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Preferences.h"
+#include "nsIDNService.h"
+#include "nsReadableUtils.h"
+#include "nsCRT.h"
+#include "nsServiceManagerUtils.h"
+#include "nsUnicharUtils.h"
+#include "nsUnicodeProperties.h"
+#include "harfbuzz/hb.h"
+#include "punycode.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/Casting.h"
+#include "mozilla/StaticPrefs_network.h"
+#include "mozilla/TextUtils.h"
+#include "mozilla/Utf8.h"
+#include "mozilla/intl/FormatBuffer.h"
+#include "mozilla/intl/UnicodeProperties.h"
+#include "mozilla/intl/UnicodeScriptCodes.h"
+
+#include "ICUUtils.h"
+
+using namespace mozilla;
+using namespace mozilla::intl;
+using namespace mozilla::unicode;
+using namespace mozilla::net;
+using mozilla::Preferences;
+
+// Currently we use the non-transitional processing option -- see
+// http://unicode.org/reports/tr46/
+// To switch to transitional processing, change the value of this flag
+// and kTransitionalProcessing in netwerk/test/unit/test_idna2008.js to true
+// (revert bug 1218179).
+const intl::IDNA::ProcessingType kIDNA2008_DefaultProcessingType =
+ intl::IDNA::ProcessingType::NonTransitional;
+
+//-----------------------------------------------------------------------------
+// According to RFC 1034 - 3.1. Name space specifications and terminology
+// the maximum label size would be 63. However, this is enforced at the DNS
+// level and none of the other browsers seem to not enforce the VerifyDnsLength
+// check in https://unicode.org/reports/tr46/#ToASCII
+// Instead, we choose a rather arbitrary but larger size.
+static const uint32_t kMaxULabelSize = 256;
+// RFC 3490 - 5. ACE prefix
+static const char kACEPrefix[] = "xn--";
+
+//-----------------------------------------------------------------------------
+
+#define NS_NET_PREF_EXTRAALLOWED "network.IDN.extra_allowed_chars"
+#define NS_NET_PREF_EXTRABLOCKED "network.IDN.extra_blocked_chars"
+#define NS_NET_PREF_IDNRESTRICTION "network.IDN.restriction_profile"
+
+static inline bool isOnlySafeChars(const nsString& in,
+ const nsTArray<BlocklistRange>& aBlocklist) {
+ if (aBlocklist.IsEmpty()) {
+ return true;
+ }
+ const char16_t* cur = in.BeginReading();
+ const char16_t* end = in.EndReading();
+
+ for (; cur < end; ++cur) {
+ if (CharInBlocklist(*cur, aBlocklist)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+// nsIDNService
+//-----------------------------------------------------------------------------
+
+/* Implementation file */
+NS_IMPL_ISUPPORTS(nsIDNService, nsIIDNService)
+
+static const char* gCallbackPrefs[] = {
+ NS_NET_PREF_EXTRAALLOWED,
+ NS_NET_PREF_EXTRABLOCKED,
+ NS_NET_PREF_IDNRESTRICTION,
+ nullptr,
+};
+
+nsresult nsIDNService::Init() {
+ MOZ_ASSERT(NS_IsMainThread());
+ // Take a strong reference for our listener with the preferences service,
+ // which we will release on shutdown.
+ // It's OK if we remove the observer a bit early, as it just means we won't
+ // respond to `network.IDN.extra_{allowed,blocked}_chars` and
+ // `network.IDN.restriction_profile` pref changes during shutdown.
+ Preferences::RegisterPrefixCallbacks(PrefChanged, gCallbackPrefs, this);
+ RunOnShutdown(
+ [self = RefPtr{this}]() mutable {
+ Preferences::UnregisterPrefixCallbacks(PrefChanged, gCallbackPrefs,
+ self.get());
+ self = nullptr;
+ },
+ ShutdownPhase::XPCOMWillShutdown);
+ prefsChanged(nullptr);
+
+ return NS_OK;
+}
+
+void nsIDNService::prefsChanged(const char* pref) {
+ MOZ_ASSERT(NS_IsMainThread());
+ AutoWriteLock lock(mLock);
+
+ if (!pref || nsLiteralCString(NS_NET_PREF_EXTRAALLOWED).Equals(pref) ||
+ nsLiteralCString(NS_NET_PREF_EXTRABLOCKED).Equals(pref)) {
+ InitializeBlocklist(mIDNBlocklist);
+ }
+ if (!pref || nsLiteralCString(NS_NET_PREF_IDNRESTRICTION).Equals(pref)) {
+ nsAutoCString profile;
+ if (NS_FAILED(
+ Preferences::GetCString(NS_NET_PREF_IDNRESTRICTION, profile))) {
+ profile.Truncate();
+ }
+ if (profile.EqualsLiteral("moderate")) {
+ mRestrictionProfile = eModeratelyRestrictiveProfile;
+ } else if (profile.EqualsLiteral("high")) {
+ mRestrictionProfile = eHighlyRestrictiveProfile;
+ } else {
+ mRestrictionProfile = eASCIIOnlyProfile;
+ }
+ }
+}
+
+nsIDNService::nsIDNService() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ auto createResult =
+ mozilla::intl::IDNA::TryCreate(kIDNA2008_DefaultProcessingType);
+ MOZ_ASSERT(createResult.isOk());
+ mIDNA = createResult.unwrap();
+}
+
+nsIDNService::~nsIDNService() = default;
+
+nsresult nsIDNService::IDNA2008ToUnicode(const nsACString& input,
+ nsAString& output) {
+ NS_ConvertUTF8toUTF16 inputStr(input);
+
+ Span<const char16_t> inputSpan{inputStr};
+ intl::nsTStringToBufferAdapter buffer(output);
+ auto result = mIDNA->LabelToUnicode(inputSpan, buffer);
+
+ nsresult rv = NS_OK;
+ if (result.isErr()) {
+ rv = ICUUtils::ICUErrorToNsResult(result.unwrapErr());
+ if (rv == NS_ERROR_FAILURE) {
+ rv = NS_ERROR_MALFORMED_URI;
+ }
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ intl::IDNA::Info info = result.unwrap();
+ if (info.HasErrors()) {
+ rv = NS_ERROR_MALFORMED_URI;
+ }
+
+ return rv;
+}
+
+nsresult nsIDNService::IDNA2008StringPrep(const nsAString& input,
+ nsAString& output,
+ stringPrepFlag flag) {
+ Span<const char16_t> inputSpan{input};
+ intl::nsTStringToBufferAdapter buffer(output);
+ auto result = mIDNA->LabelToUnicode(inputSpan, buffer);
+
+ nsresult rv = NS_OK;
+ if (result.isErr()) {
+ rv = ICUUtils::ICUErrorToNsResult(result.unwrapErr());
+ if (rv == NS_ERROR_FAILURE) {
+ rv = NS_ERROR_MALFORMED_URI;
+ }
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ intl::IDNA::Info info = result.unwrap();
+
+ // Output the result of nameToUnicode even if there were errors.
+ // But in the case of invalid punycode, the uidna_labelToUnicode result
+ // appears to get an appended U+FFFD REPLACEMENT CHARACTER, which will
+ // confuse our subsequent processing, so we drop that.
+ // (https://bugzilla.mozilla.org/show_bug.cgi?id=1399540#c9)
+ if ((info.HasInvalidPunycode() || info.HasInvalidAceLabel()) &&
+ !output.IsEmpty() && output.Last() == 0xfffd) {
+ output.Truncate(output.Length() - 1);
+ }
+
+ if (flag == eStringPrepIgnoreErrors) {
+ return NS_OK;
+ }
+
+ bool hasError = flag == eStringPrepForDNS
+ ? info.HasErrors() && !info.HasInvalidHyphen()
+ : info.HasErrors();
+
+ if (hasError) {
+ if (flag == eStringPrepForDNS) {
+ output.Truncate();
+ }
+ rv = NS_ERROR_MALFORMED_URI;
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP nsIDNService::ConvertUTF8toACE(const nsACString& input,
+ nsACString& ace) {
+ return UTF8toACE(input, ace, eStringPrepForDNS);
+}
+
+nsresult nsIDNService::UTF8toACE(const nsACString& input, nsACString& ace,
+ stringPrepFlag flag) {
+ nsresult rv;
+ NS_ConvertUTF8toUTF16 ustr(input);
+
+ // map ideographic period to ASCII period etc.
+ normalizeFullStops(ustr);
+
+ uint32_t len, offset;
+ len = 0;
+ offset = 0;
+ nsAutoCString encodedBuf;
+
+ nsAString::const_iterator start, end;
+ ustr.BeginReading(start);
+ ustr.EndReading(end);
+ ace.Truncate();
+
+ // encode nodes if non ASCII
+ while (start != end) {
+ len++;
+ if (*start++ == (char16_t)'.') {
+ rv = stringPrepAndACE(Substring(ustr, offset, len - 1), encodedBuf, flag);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ ace.Append(encodedBuf);
+ ace.Append('.');
+ offset += len;
+ len = 0;
+ }
+ }
+
+ // encode the last node if non ASCII
+ if (len) {
+ rv = stringPrepAndACE(Substring(ustr, offset, len), encodedBuf, flag);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ ace.Append(encodedBuf);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsIDNService::ConvertACEtoUTF8(const nsACString& input,
+ nsACString& _retval) {
+ return ACEtoUTF8(input, _retval, eStringPrepForDNS);
+}
+
+nsresult nsIDNService::ACEtoUTF8(const nsACString& input, nsACString& _retval,
+ stringPrepFlag flag) {
+ // RFC 3490 - 4.2 ToUnicode
+ // ToUnicode never fails. If any step fails, then the original input
+ // sequence is returned immediately in that step.
+ //
+ // Note that this refers to the decoding of a single label.
+ // ACEtoUTF8 may be called with a sequence of labels separated by dots;
+ // this test applies individually to each label.
+
+ uint32_t len = 0, offset = 0;
+ nsAutoCString decodedBuf;
+
+ nsACString::const_iterator start, end;
+ input.BeginReading(start);
+ input.EndReading(end);
+ _retval.Truncate();
+
+ // loop and decode nodes
+ while (start != end) {
+ len++;
+ if (*start++ == '.') {
+ nsDependentCSubstring origLabel(input, offset, len - 1);
+ if (NS_FAILED(decodeACE(origLabel, decodedBuf, flag))) {
+ // If decoding failed, use the original input sequence
+ // for this label.
+ _retval.Append(origLabel);
+ } else {
+ _retval.Append(decodedBuf);
+ }
+
+ _retval.Append('.');
+ offset += len;
+ len = 0;
+ }
+ }
+ // decode the last node
+ if (len) {
+ nsDependentCSubstring origLabel(input, offset, len);
+ if (NS_FAILED(decodeACE(origLabel, decodedBuf, flag))) {
+ _retval.Append(origLabel);
+ } else {
+ _retval.Append(decodedBuf);
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsIDNService::IsACE(const nsACString& input, bool* _retval) {
+ // look for the ACE prefix in the input string. it may occur
+ // at the beginning of any segment in the domain name. for
+ // example: "www.xn--ENCODED.com"
+
+ if (!IsAscii(input)) {
+ *_retval = false;
+ return NS_OK;
+ }
+
+ auto stringContains = [](const nsACString& haystack,
+ const nsACString& needle) {
+ return std::search(haystack.BeginReading(), haystack.EndReading(),
+ needle.BeginReading(),
+ needle.EndReading()) != haystack.EndReading();
+ };
+
+ *_retval = StringBeginsWith(input, "xn--"_ns) ||
+ (!input.IsEmpty() && input[0] != '.' &&
+ stringContains(input, ".xn--"_ns));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsIDNService::Normalize(const nsACString& input,
+ nsACString& output) {
+ // protect against bogus input
+ NS_ENSURE_TRUE(IsUtf8(input), NS_ERROR_UNEXPECTED);
+
+ NS_ConvertUTF8toUTF16 inUTF16(input);
+ normalizeFullStops(inUTF16);
+
+ // pass the domain name to stringprep label by label
+ nsAutoString outUTF16, outLabel;
+
+ uint32_t len = 0, offset = 0;
+ nsresult rv;
+ nsAString::const_iterator start, end;
+ inUTF16.BeginReading(start);
+ inUTF16.EndReading(end);
+
+ while (start != end) {
+ len++;
+ if (*start++ == char16_t('.')) {
+ rv = stringPrep(Substring(inUTF16, offset, len - 1), outLabel,
+ eStringPrepIgnoreErrors);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ outUTF16.Append(outLabel);
+ outUTF16.Append(char16_t('.'));
+ offset += len;
+ len = 0;
+ }
+ }
+ if (len) {
+ rv = stringPrep(Substring(inUTF16, offset, len), outLabel,
+ eStringPrepIgnoreErrors);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ outUTF16.Append(outLabel);
+ }
+
+ CopyUTF16toUTF8(outUTF16, output);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsIDNService::ConvertToDisplayIDN(const nsACString& input,
+ bool* _isASCII,
+ nsACString& _retval) {
+ // If host is ACE, then convert to UTF-8 if the host is in the IDN whitelist.
+ // Else, if host is already UTF-8, then make sure it is normalized per IDN.
+
+ nsresult rv = NS_OK;
+
+ // Even if the hostname is not ASCII, individual labels may still be ACE, so
+ // test IsACE before testing IsASCII
+ bool isACE;
+ IsACE(input, &isACE);
+
+ if (IsAscii(input)) {
+ // first, canonicalize the host to lowercase, for whitelist lookup
+ _retval = input;
+ ToLowerCase(_retval);
+
+ if (isACE && !StaticPrefs::network_IDN_show_punycode()) {
+ // ACEtoUTF8() can't fail, but might return the original ACE string
+ nsAutoCString temp(_retval);
+ // Convert from ACE to UTF8 only those labels which are considered safe
+ // for display
+ ACEtoUTF8(temp, _retval, eStringPrepForUI);
+ *_isASCII = IsAscii(_retval);
+ } else {
+ *_isASCII = true;
+ }
+ } else {
+ // We have to normalize the hostname before testing against the domain
+ // whitelist (see bug 315411), and to ensure the entire string gets
+ // normalized.
+ //
+ // Normalization and the tests for safe display below, assume that the
+ // input is Unicode, so first convert any ACE labels to UTF8
+ if (isACE) {
+ nsAutoCString temp;
+ ACEtoUTF8(input, temp, eStringPrepIgnoreErrors);
+ rv = Normalize(temp, _retval);
+ } else {
+ rv = Normalize(input, _retval);
+ }
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ if (StaticPrefs::network_IDN_show_punycode() &&
+ NS_SUCCEEDED(UTF8toACE(_retval, _retval, eStringPrepIgnoreErrors))) {
+ *_isASCII = true;
+ return NS_OK;
+ }
+
+ // normalization could result in an ASCII-only hostname. alternatively, if
+ // the host is converted to ACE by the normalizer, then the host may contain
+ // unsafe characters, so leave it ACE encoded. see bug 283016, bug 301694,
+ // and bug 309311.
+ *_isASCII = IsAscii(_retval);
+ if (!*_isASCII) {
+ // UTF8toACE with eStringPrepForUI may return a domain name where
+ // some labels are in UTF-8 and some are in ACE, depending on
+ // whether they are considered safe for display
+ rv = UTF8toACE(_retval, _retval, eStringPrepForUI);
+ *_isASCII = IsAscii(_retval);
+ return rv;
+ }
+ }
+
+ return NS_OK;
+} // Will generate a mutex still-held warning
+
+//-----------------------------------------------------------------------------
+
+static nsresult utf16ToUcs4(const nsAString& in, uint32_t* out,
+ uint32_t outBufLen, uint32_t* outLen) {
+ uint32_t i = 0;
+ nsAString::const_iterator start, end;
+ in.BeginReading(start);
+ in.EndReading(end);
+
+ while (start != end) {
+ char16_t curChar;
+
+ curChar = *start++;
+
+ if (start != end && NS_IS_SURROGATE_PAIR(curChar, *start)) {
+ out[i] = SURROGATE_TO_UCS4(curChar, *start);
+ ++start;
+ } else {
+ out[i] = curChar;
+ }
+
+ i++;
+ if (i >= outBufLen) {
+ return NS_ERROR_MALFORMED_URI;
+ }
+ }
+ out[i] = (uint32_t)'\0';
+ *outLen = i;
+ return NS_OK;
+}
+
+static nsresult punycode(const nsAString& in, nsACString& out) {
+ uint32_t ucs4Buf[kMaxULabelSize + 1];
+ uint32_t ucs4Len = 0u;
+ nsresult rv = utf16ToUcs4(in, ucs4Buf, kMaxULabelSize, &ucs4Len);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // need maximum 20 bits to encode 16 bit Unicode character
+ // (include null terminator)
+ const uint32_t kEncodedBufSize = kMaxULabelSize * 20 / 8 + 1 + 1;
+ char encodedBuf[kEncodedBufSize];
+ punycode_uint encodedLength = kEncodedBufSize;
+
+ enum punycode_status status =
+ punycode_encode(ucs4Len, ucs4Buf, nullptr, &encodedLength, encodedBuf);
+
+ if (punycode_success != status || encodedLength >= kEncodedBufSize) {
+ return NS_ERROR_MALFORMED_URI;
+ }
+
+ encodedBuf[encodedLength] = '\0';
+ out.Assign(nsDependentCString(kACEPrefix) + nsDependentCString(encodedBuf));
+
+ return rv;
+}
+
+// RFC 3454
+//
+// 1) Map -- For each character in the input, check if it has a mapping
+// and, if so, replace it with its mapping. This is described in section 3.
+//
+// 2) Normalize -- Possibly normalize the result of step 1 using Unicode
+// normalization. This is described in section 4.
+//
+// 3) Prohibit -- Check for any characters that are not allowed in the
+// output. If any are found, return an error. This is described in section
+// 5.
+//
+// 4) Check bidi -- Possibly check for right-to-left characters, and if any
+// are found, make sure that the whole string satisfies the requirements
+// for bidirectional strings. If the string does not satisfy the requirements
+// for bidirectional strings, return an error. This is described in section 6.
+//
+// 5) Check unassigned code points -- If allowUnassigned is false, check for
+// any unassigned Unicode points and if any are found return an error.
+// This is described in section 7.
+//
+nsresult nsIDNService::stringPrep(const nsAString& in, nsAString& out,
+ stringPrepFlag flag) {
+ return IDNA2008StringPrep(in, out, flag);
+}
+
+nsresult nsIDNService::stringPrepAndACE(const nsAString& in, nsACString& out,
+ stringPrepFlag flag) {
+ nsresult rv = NS_OK;
+
+ out.Truncate();
+
+ if (IsAscii(in)) {
+ LossyCopyUTF16toASCII(in, out);
+ // If label begins with xn-- we still want to check its validity
+ if (!StringBeginsWith(in, u"xn--"_ns)) {
+ return NS_OK;
+ }
+ }
+
+ nsAutoString strPrep;
+ rv = stringPrep(in, strPrep, flag);
+ if (flag == eStringPrepForDNS) {
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (IsAscii(strPrep)) {
+ LossyCopyUTF16toASCII(strPrep, out);
+ return NS_OK;
+ }
+
+ if (flag == eStringPrepForUI && NS_SUCCEEDED(rv) && isLabelSafe(in)) {
+ CopyUTF16toUTF8(strPrep, out);
+ return NS_OK;
+ }
+
+ return punycode(strPrep, out);
+}
+
+// RFC 3490
+// 1) Whenever dots are used as label separators, the following characters
+// MUST be recognized as dots: U+002E (full stop), U+3002 (ideographic full
+// stop), U+FF0E (fullwidth full stop), U+FF61 (halfwidth ideographic full
+// stop).
+
+void nsIDNService::normalizeFullStops(nsAString& s) {
+ nsAString::const_iterator start, end;
+ s.BeginReading(start);
+ s.EndReading(end);
+ int32_t index = 0;
+
+ while (start != end) {
+ switch (*start) {
+ case 0x3002:
+ case 0xFF0E:
+ case 0xFF61:
+ s.ReplaceLiteral(index, 1, u".");
+ break;
+ default:
+ break;
+ }
+ start++;
+ index++;
+ }
+}
+
+nsresult nsIDNService::decodeACE(const nsACString& in, nsACString& out,
+ stringPrepFlag flag) {
+ bool isAce;
+ IsACE(in, &isAce);
+ if (!isAce) {
+ out.Assign(in);
+ return NS_OK;
+ }
+
+ nsAutoString utf16;
+ nsresult result = IDNA2008ToUnicode(in, utf16);
+ NS_ENSURE_SUCCESS(result, result);
+
+ if (flag != eStringPrepForUI || isLabelSafe(utf16)) {
+ CopyUTF16toUTF8(utf16, out);
+ } else {
+ out.Assign(in);
+ return NS_OK;
+ }
+
+ // Validation: encode back to ACE and compare the strings
+ nsAutoCString ace;
+ nsresult rv = UTF8toACE(out, ace, flag);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (flag == eStringPrepForDNS &&
+ !ace.Equals(in, nsCaseInsensitiveCStringComparator)) {
+ return NS_ERROR_MALFORMED_URI;
+ }
+
+ return NS_OK;
+}
+
+namespace mozilla::net {
+
+enum ScriptCombo : int32_t {
+ UNSET = -1,
+ BOPO = 0,
+ CYRL = 1,
+ GREK = 2,
+ HANG = 3,
+ HANI = 4,
+ HIRA = 5,
+ KATA = 6,
+ LATN = 7,
+ OTHR = 8,
+ JPAN = 9, // Latin + Han + Hiragana + Katakana
+ CHNA = 10, // Latin + Han + Bopomofo
+ KORE = 11, // Latin + Han + Hangul
+ HNLT = 12, // Latin + Han (could be any of the above combinations)
+ FAIL = 13,
+};
+
+} // namespace mozilla::net
+
+bool nsIDNService::isLabelSafe(const nsAString& label) {
+ AutoReadLock lock(mLock);
+
+ if (!isOnlySafeChars(PromiseFlatString(label), mIDNBlocklist)) {
+ return false;
+ }
+
+ // We should never get here if the label is ASCII
+ NS_ASSERTION(!IsAscii(label), "ASCII label in IDN checking");
+ if (mRestrictionProfile == eASCIIOnlyProfile) {
+ return false;
+ }
+
+ nsAString::const_iterator current, end;
+ label.BeginReading(current);
+ label.EndReading(end);
+
+ Script lastScript = Script::INVALID;
+ uint32_t previousChar = 0;
+ uint32_t baseChar = 0; // last non-diacritic seen (base char for marks)
+ uint32_t savedNumberingSystem = 0;
+// Simplified/Traditional Chinese check temporarily disabled -- bug 857481
+#if 0
+ HanVariantType savedHanVariant = HVT_NotHan;
+#endif
+
+ ScriptCombo savedScript = ScriptCombo::UNSET;
+
+ while (current != end) {
+ uint32_t ch = *current++;
+
+ if (current != end && NS_IS_SURROGATE_PAIR(ch, *current)) {
+ ch = SURROGATE_TO_UCS4(ch, *current++);
+ }
+
+ IdentifierType idType = GetIdentifierType(ch);
+ if (idType == IDTYPE_RESTRICTED) {
+ return false;
+ }
+ MOZ_ASSERT(idType == IDTYPE_ALLOWED);
+
+ // Check for mixed script
+ Script script = UnicodeProperties::GetScriptCode(ch);
+ if (script != Script::COMMON && script != Script::INHERITED &&
+ script != lastScript) {
+ if (illegalScriptCombo(script, savedScript)) {
+ return false;
+ }
+ }
+
+ // U+30FC should be preceded by a Hiragana/Katakana.
+ if (ch == 0x30fc && lastScript != Script::HIRAGANA &&
+ lastScript != Script::KATAKANA) {
+ return false;
+ }
+
+ if (ch == 0x307 &&
+ (previousChar == 'i' || previousChar == 'j' || previousChar == 'l')) {
+ return false;
+ }
+
+ // Check for mixed numbering systems
+ auto genCat = GetGeneralCategory(ch);
+ if (genCat == HB_UNICODE_GENERAL_CATEGORY_DECIMAL_NUMBER) {
+ uint32_t zeroCharacter =
+ ch - mozilla::intl::UnicodeProperties::GetNumericValue(ch);
+ if (savedNumberingSystem == 0) {
+ // If we encounter a decimal number, save the zero character from that
+ // numbering system.
+ savedNumberingSystem = zeroCharacter;
+ } else if (zeroCharacter != savedNumberingSystem) {
+ return false;
+ }
+ }
+
+ if (genCat == HB_UNICODE_GENERAL_CATEGORY_NON_SPACING_MARK) {
+ // Check for consecutive non-spacing marks.
+ if (previousChar != 0 && previousChar == ch) {
+ return false;
+ }
+ // Check for marks whose expected script doesn't match the base script.
+ if (lastScript != Script::INVALID) {
+ UnicodeProperties::ScriptExtensionVector scripts;
+ auto extResult = UnicodeProperties::GetExtensions(ch, scripts);
+ MOZ_ASSERT(extResult.isOk());
+ if (extResult.isErr()) {
+ return false;
+ }
+
+ int nScripts = AssertedCast<int>(scripts.length());
+
+ // nScripts will always be >= 1, because even for undefined characters
+ // it will return Script::INVALID.
+ // If the mark just has script=COMMON or INHERITED, we can't check any
+ // more carefully, but if it has specific scriptExtension codes, then
+ // assume those are the only valid scripts to use it with.
+ if (nScripts > 1 || (Script(scripts[0]) != Script::COMMON &&
+ Script(scripts[0]) != Script::INHERITED)) {
+ while (--nScripts >= 0) {
+ if (Script(scripts[nScripts]) == lastScript) {
+ break;
+ }
+ }
+ if (nScripts == -1) {
+ return false;
+ }
+ }
+ }
+ // Check for diacritics on dotless-i, which would be indistinguishable
+ // from normal accented letter i.
+ if (baseChar == 0x0131 &&
+ ((ch >= 0x0300 && ch <= 0x0314) || ch == 0x031a)) {
+ return false;
+ }
+ } else {
+ baseChar = ch;
+ }
+
+ if (script != Script::COMMON && script != Script::INHERITED) {
+ lastScript = script;
+ }
+
+ // Simplified/Traditional Chinese check temporarily disabled -- bug 857481
+#if 0
+
+ // Check for both simplified-only and traditional-only Chinese characters
+ HanVariantType hanVariant = GetHanVariant(ch);
+ if (hanVariant == HVT_SimplifiedOnly || hanVariant == HVT_TraditionalOnly) {
+ if (savedHanVariant == HVT_NotHan) {
+ savedHanVariant = hanVariant;
+ } else if (hanVariant != savedHanVariant) {
+ return false;
+ }
+ }
+#endif
+
+ previousChar = ch;
+ }
+ return true;
+}
+
+// Scripts that we care about in illegalScriptCombo
+static inline ScriptCombo findScriptIndex(Script aScript) {
+ switch (aScript) {
+ case Script::BOPOMOFO:
+ return ScriptCombo::BOPO;
+ case Script::CYRILLIC:
+ return ScriptCombo::CYRL;
+ case Script::GREEK:
+ return ScriptCombo::GREK;
+ case Script::HANGUL:
+ return ScriptCombo::HANG;
+ case Script::HAN:
+ return ScriptCombo::HANI;
+ case Script::HIRAGANA:
+ return ScriptCombo::HIRA;
+ case Script::KATAKANA:
+ return ScriptCombo::KATA;
+ case Script::LATIN:
+ return ScriptCombo::LATN;
+ default:
+ return ScriptCombo::OTHR;
+ }
+}
+
+static const ScriptCombo scriptComboTable[13][9] = {
+ /* thisScript: BOPO CYRL GREK HANG HANI HIRA KATA LATN OTHR
+ * savedScript */
+ /* BOPO */ {BOPO, FAIL, FAIL, FAIL, CHNA, FAIL, FAIL, CHNA, FAIL},
+ /* CYRL */ {FAIL, CYRL, FAIL, FAIL, FAIL, FAIL, FAIL, FAIL, FAIL},
+ /* GREK */ {FAIL, FAIL, GREK, FAIL, FAIL, FAIL, FAIL, FAIL, FAIL},
+ /* HANG */ {FAIL, FAIL, FAIL, HANG, KORE, FAIL, FAIL, KORE, FAIL},
+ /* HANI */ {CHNA, FAIL, FAIL, KORE, HANI, JPAN, JPAN, HNLT, FAIL},
+ /* HIRA */ {FAIL, FAIL, FAIL, FAIL, JPAN, HIRA, JPAN, JPAN, FAIL},
+ /* KATA */ {FAIL, FAIL, FAIL, FAIL, JPAN, JPAN, KATA, JPAN, FAIL},
+ /* LATN */ {CHNA, FAIL, FAIL, KORE, HNLT, JPAN, JPAN, LATN, OTHR},
+ /* OTHR */ {FAIL, FAIL, FAIL, FAIL, FAIL, FAIL, FAIL, OTHR, FAIL},
+ /* JPAN */ {FAIL, FAIL, FAIL, FAIL, JPAN, JPAN, JPAN, JPAN, FAIL},
+ /* CHNA */ {CHNA, FAIL, FAIL, FAIL, CHNA, FAIL, FAIL, CHNA, FAIL},
+ /* KORE */ {FAIL, FAIL, FAIL, KORE, KORE, FAIL, FAIL, KORE, FAIL},
+ /* HNLT */ {CHNA, FAIL, FAIL, KORE, HNLT, JPAN, JPAN, HNLT, FAIL}};
+
+bool nsIDNService::illegalScriptCombo(Script script, ScriptCombo& savedScript) {
+ if (savedScript == ScriptCombo::UNSET) {
+ savedScript = findScriptIndex(script);
+ return false;
+ }
+
+ savedScript = scriptComboTable[savedScript][findScriptIndex(script)];
+ /*
+ * Special case combinations that depend on which profile is in use
+ * In the Highly Restrictive profile Latin is not allowed with any
+ * other script
+ *
+ * In the Moderately Restrictive profile Latin mixed with any other
+ * single script is allowed.
+ */
+ return ((savedScript == OTHR &&
+ mRestrictionProfile == eHighlyRestrictiveProfile) ||
+ savedScript == FAIL);
+}