blob: 6e85552b71b807f51b8091ae287242e11dacf3ab [file] [log] [blame]
/*
* Copyright (C) 2015 Andy VanWagoner (andy@vanwagoner.family)
* Copyright (C) 2015 Sukolsak Sakshuwong (sukolsak@gmail.com)
* Copyright (C) 2016-2021 Apple Inc. All Rights Reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "IntlCollator.h"
#include "IntlObjectInlines.h"
#include "JSBoundFunction.h"
#include "JSCInlines.h"
#include "ObjectConstructor.h"
#include <wtf/HexNumber.h>
namespace JSC {
const ClassInfo IntlCollator::s_info = { "Object", &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(IntlCollator) };
namespace IntlCollatorInternal {
constexpr bool verbose = false;
}
IntlCollator* IntlCollator::create(VM& vm, Structure* structure)
{
IntlCollator* format = new (NotNull, allocateCell<IntlCollator>(vm.heap)) IntlCollator(vm, structure);
format->finishCreation(vm);
return format;
}
Structure* IntlCollator::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype)
{
return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info());
}
IntlCollator::IntlCollator(VM& vm, Structure* structure)
: Base(vm, structure)
{
}
void IntlCollator::finishCreation(VM& vm)
{
Base::finishCreation(vm);
ASSERT(inherits(vm, info()));
}
template<typename Visitor>
void IntlCollator::visitChildrenImpl(JSCell* cell, Visitor& visitor)
{
IntlCollator* thisObject = jsCast<IntlCollator*>(cell);
ASSERT_GC_OBJECT_INHERITS(thisObject, info());
Base::visitChildren(thisObject, visitor);
visitor.append(thisObject->m_boundCompare);
}
DEFINE_VISIT_CHILDREN(IntlCollator);
Vector<String> IntlCollator::sortLocaleData(const String& locale, RelevantExtensionKey key)
{
// 9.1 Internal slots of Service Constructors & 10.2.3 Internal slots (ECMA-402 2.0)
Vector<String> keyLocaleData;
switch (key) {
case RelevantExtensionKey::Co: {
// 10.2.3 "The first element of [[sortLocaleData]][locale].co and [[searchLocaleData]][locale].co must be null for all locale values."
keyLocaleData.append({ });
UErrorCode status = U_ZERO_ERROR;
auto enumeration = std::unique_ptr<UEnumeration, ICUDeleter<uenum_close>>(ucol_getKeywordValuesForLocale("collation", locale.utf8().data(), false, &status));
if (U_SUCCESS(status)) {
const char* pointer;
int32_t length = 0;
while ((pointer = uenum_next(enumeration.get(), &length, &status)) && U_SUCCESS(status)) {
// 10.2.3 "The values "standard" and "search" must not be used as elements in any [[sortLocaleData]][locale].co and [[searchLocaleData]][locale].co array."
String collation(pointer, length);
if (collation == "standard"_s || collation == "search"_s)
continue;
if (auto mapped = mapICUCollationKeywordToBCP47(collation))
keyLocaleData.append(WTFMove(mapped.value()));
else
keyLocaleData.append(WTFMove(collation));
}
}
break;
}
case RelevantExtensionKey::Kf:
keyLocaleData.reserveInitialCapacity(3);
keyLocaleData.uncheckedAppend("false"_s);
keyLocaleData.uncheckedAppend("lower"_s);
keyLocaleData.uncheckedAppend("upper"_s);
break;
case RelevantExtensionKey::Kn:
keyLocaleData.reserveInitialCapacity(2);
keyLocaleData.uncheckedAppend("false"_s);
keyLocaleData.uncheckedAppend("true"_s);
break;
default:
ASSERT_NOT_REACHED();
}
return keyLocaleData;
}
Vector<String> IntlCollator::searchLocaleData(const String&, RelevantExtensionKey key)
{
// 9.1 Internal slots of Service Constructors & 10.2.3 Internal slots (ECMA-402 2.0)
Vector<String> keyLocaleData;
switch (key) {
case RelevantExtensionKey::Co:
// 10.2.3 "The first element of [[sortLocaleData]][locale].co and [[searchLocaleData]][locale].co must be null for all locale values."
keyLocaleData.reserveInitialCapacity(1);
keyLocaleData.append({ });
break;
case RelevantExtensionKey::Kf:
keyLocaleData.reserveInitialCapacity(3);
keyLocaleData.uncheckedAppend("false"_s);
keyLocaleData.uncheckedAppend("lower"_s);
keyLocaleData.uncheckedAppend("upper"_s);
break;
case RelevantExtensionKey::Kn:
keyLocaleData.reserveInitialCapacity(2);
keyLocaleData.uncheckedAppend("false"_s);
keyLocaleData.uncheckedAppend("true"_s);
break;
default:
ASSERT_NOT_REACHED();
}
return keyLocaleData;
}
// https://tc39.github.io/ecma402/#sec-initializecollator
void IntlCollator::initializeCollator(JSGlobalObject* globalObject, JSValue locales, JSValue optionsValue)
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto requestedLocales = canonicalizeLocaleList(globalObject, locales);
RETURN_IF_EXCEPTION(scope, void());
JSObject* options = intlCoerceOptionsToObject(globalObject, optionsValue);
RETURN_IF_EXCEPTION(scope, void());
m_usage = intlOption<Usage>(globalObject, options, vm.propertyNames->usage, { { "sort"_s, Usage::Sort }, { "search"_s, Usage::Search } }, "usage must be either \"sort\" or \"search\""_s, Usage::Sort);
RETURN_IF_EXCEPTION(scope, void());
auto localeData = (m_usage == Usage::Sort) ? sortLocaleData : searchLocaleData;
ResolveLocaleOptions localeOptions;
LocaleMatcher localeMatcher = intlOption<LocaleMatcher>(globalObject, options, vm.propertyNames->localeMatcher, { { "lookup"_s, LocaleMatcher::Lookup }, { "best fit"_s, LocaleMatcher::BestFit } }, "localeMatcher must be either \"lookup\" or \"best fit\""_s, LocaleMatcher::BestFit);
RETURN_IF_EXCEPTION(scope, void());
{
String collation = intlStringOption(globalObject, options, vm.propertyNames->collation, { }, nullptr, nullptr);
RETURN_IF_EXCEPTION(scope, void());
if (!collation.isNull()) {
if (!isUnicodeLocaleIdentifierType(collation)) {
throwRangeError(globalObject, scope, "collation is not a well-formed collation value"_s);
return;
}
localeOptions[static_cast<unsigned>(RelevantExtensionKey::Co)] = WTFMove(collation);
}
}
TriState numeric = intlBooleanOption(globalObject, options, vm.propertyNames->numeric);
RETURN_IF_EXCEPTION(scope, void());
if (numeric != TriState::Indeterminate)
localeOptions[static_cast<unsigned>(RelevantExtensionKey::Kn)] = String(numeric == TriState::True ? "true"_s : "false"_s);
String caseFirstOption = intlStringOption(globalObject, options, vm.propertyNames->caseFirst, { "upper", "lower", "false" }, "caseFirst must be either \"upper\", \"lower\", or \"false\"", nullptr);
RETURN_IF_EXCEPTION(scope, void());
if (!caseFirstOption.isNull())
localeOptions[static_cast<unsigned>(RelevantExtensionKey::Kf)] = caseFirstOption;
const auto& availableLocales = intlCollatorAvailableLocales();
auto resolved = resolveLocale(globalObject, availableLocales, requestedLocales, localeMatcher, localeOptions, { RelevantExtensionKey::Co, RelevantExtensionKey::Kf, RelevantExtensionKey::Kn }, localeData);
m_locale = resolved.locale;
if (m_locale.isEmpty()) {
throwTypeError(globalObject, scope, "failed to initialize Collator due to invalid locale"_s);
return;
}
const String& collation = resolved.extensions[static_cast<unsigned>(RelevantExtensionKey::Co)];
m_collation = collation.isNull() ? "default"_s : collation;
m_numeric = resolved.extensions[static_cast<unsigned>(RelevantExtensionKey::Kn)] == "true"_s;
const String& caseFirstString = resolved.extensions[static_cast<unsigned>(RelevantExtensionKey::Kf)];
if (caseFirstString == "lower")
m_caseFirst = CaseFirst::Lower;
else if (caseFirstString == "upper")
m_caseFirst = CaseFirst::Upper;
else
m_caseFirst = CaseFirst::False;
m_sensitivity = intlOption<Sensitivity>(globalObject, options, vm.propertyNames->sensitivity, { { "base"_s, Sensitivity::Base }, { "accent"_s, Sensitivity::Accent }, { "case"_s, Sensitivity::Case }, { "variant"_s, Sensitivity::Variant } }, "sensitivity must be either \"base\", \"accent\", \"case\", or \"variant\""_s, Sensitivity::Variant);
RETURN_IF_EXCEPTION(scope, void());
TriState ignorePunctuation = intlBooleanOption(globalObject, options, vm.propertyNames->ignorePunctuation);
RETURN_IF_EXCEPTION(scope, void());
m_ignorePunctuation = (ignorePunctuation == TriState::True);
// UCollator does not offer an option to configure "usage" via ucol_setAttribute. So we need to pass this option via locale.
CString dataLocaleWithExtensions;
switch (m_usage) {
case Usage::Sort:
if (collation.isNull())
dataLocaleWithExtensions = resolved.dataLocale.utf8();
else
dataLocaleWithExtensions = makeString(resolved.dataLocale, "-u-co-", m_collation).utf8();
break;
case Usage::Search:
// searchLocaleData filters out "co" unicode extension. However, we need to pass "co" to ICU when Usage::Search is specified.
// So we need to pass "co" unicode extension through locale. Since the other relevant extensions are handled via ucol_setAttribute,
// we can just use dataLocale
// Since searchLocaleData filters out "co" unicode extension, "collation" option is just ignored.
dataLocaleWithExtensions = makeString(resolved.dataLocale, "-u-co-search").utf8();
break;
}
dataLogLnIf(IntlCollatorInternal::verbose, "locale:(", resolved.locale, "),dataLocaleWithExtensions:(", dataLocaleWithExtensions, ")");
UErrorCode status = U_ZERO_ERROR;
m_collator = std::unique_ptr<UCollator, UCollatorDeleter>(ucol_open(dataLocaleWithExtensions.data(), &status));
if (U_FAILURE(status)) {
throwTypeError(globalObject, scope, "failed to initialize Collator"_s);
return;
}
UColAttributeValue strength = UCOL_PRIMARY;
UColAttributeValue caseLevel = UCOL_OFF;
UColAttributeValue caseFirst = UCOL_OFF;
switch (m_sensitivity) {
case Sensitivity::Base:
break;
case Sensitivity::Accent:
strength = UCOL_SECONDARY;
break;
case Sensitivity::Case:
caseLevel = UCOL_ON;
break;
case Sensitivity::Variant:
strength = UCOL_TERTIARY;
break;
}
switch (m_caseFirst) {
case CaseFirst::False:
break;
case CaseFirst::Lower:
caseFirst = UCOL_LOWER_FIRST;
break;
case CaseFirst::Upper:
caseFirst = UCOL_UPPER_FIRST;
break;
}
// Keep in sync with canDoASCIIUCADUCETComparisonSlow about used attributes.
ucol_setAttribute(m_collator.get(), UCOL_STRENGTH, strength, &status);
ucol_setAttribute(m_collator.get(), UCOL_CASE_LEVEL, caseLevel, &status);
ucol_setAttribute(m_collator.get(), UCOL_CASE_FIRST, caseFirst, &status);
ucol_setAttribute(m_collator.get(), UCOL_NUMERIC_COLLATION, m_numeric ? UCOL_ON : UCOL_OFF, &status);
// FIXME: Setting UCOL_ALTERNATE_HANDLING to UCOL_SHIFTED causes punctuation and whitespace to be
// ignored. There is currently no way to ignore only punctuation.
ucol_setAttribute(m_collator.get(), UCOL_ALTERNATE_HANDLING, m_ignorePunctuation ? UCOL_SHIFTED : UCOL_DEFAULT, &status);
// "The method is required to return 0 when comparing Strings that are considered canonically
// equivalent by the Unicode standard."
ucol_setAttribute(m_collator.get(), UCOL_NORMALIZATION_MODE, UCOL_ON, &status);
ASSERT(U_SUCCESS(status));
}
// https://tc39.es/ecma402/#sec-collator-comparestrings
JSValue IntlCollator::compareStrings(JSGlobalObject* globalObject, StringView x, StringView y) const
{
ASSERT(m_collator);
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
UErrorCode status = U_ZERO_ERROR;
UCollationResult result = ([&]() -> UCollationResult {
if (x.isAllSpecialCharacters<canUseASCIIUCADUCETComparison>() && y.isAllSpecialCharacters<canUseASCIIUCADUCETComparison>()) {
if (canDoASCIIUCADUCETComparison()) {
if (x.is8Bit() && y.is8Bit())
return compareASCIIWithUCADUCET(x.characters8(), x.length(), y.characters8(), y.length());
if (x.is8Bit())
return compareASCIIWithUCADUCET(x.characters8(), x.length(), y.characters16(), y.length());
if (y.is8Bit())
return compareASCIIWithUCADUCET(x.characters16(), x.length(), y.characters8(), y.length());
return compareASCIIWithUCADUCET(x.characters16(), x.length(), y.characters16(), y.length());
}
if (x.is8Bit() && y.is8Bit())
return ucol_strcollUTF8(m_collator.get(), bitwise_cast<const char*>(x.characters8()), x.length(), bitwise_cast<const char*>(y.characters8()), y.length(), &status);
}
return ucol_strcoll(m_collator.get(), x.upconvertedCharacters(), x.length(), y.upconvertedCharacters(), y.length());
}());
if (U_FAILURE(status))
return throwException(globalObject, scope, createError(globalObject, "Failed to compare strings."_s));
return jsNumber(result);
}
ASCIILiteral IntlCollator::usageString(Usage usage)
{
switch (usage) {
case Usage::Sort:
return "sort"_s;
case Usage::Search:
return "search"_s;
}
ASSERT_NOT_REACHED();
return ASCIILiteral::null();
}
ASCIILiteral IntlCollator::sensitivityString(Sensitivity sensitivity)
{
switch (sensitivity) {
case Sensitivity::Base:
return "base"_s;
case Sensitivity::Accent:
return "accent"_s;
case Sensitivity::Case:
return "case"_s;
case Sensitivity::Variant:
return "variant"_s;
}
ASSERT_NOT_REACHED();
return ASCIILiteral::null();
}
ASCIILiteral IntlCollator::caseFirstString(CaseFirst caseFirst)
{
switch (caseFirst) {
case CaseFirst::False:
return "false"_s;
case CaseFirst::Lower:
return "lower"_s;
case CaseFirst::Upper:
return "upper"_s;
}
ASSERT_NOT_REACHED();
return ASCIILiteral::null();
}
// https://tc39.es/ecma402/#sec-intl.collator.prototype.resolvedoptions
JSObject* IntlCollator::resolvedOptions(JSGlobalObject* globalObject) const
{
VM& vm = globalObject->vm();
JSObject* options = constructEmptyObject(globalObject);
options->putDirect(vm, vm.propertyNames->locale, jsString(vm, m_locale));
options->putDirect(vm, vm.propertyNames->usage, jsNontrivialString(vm, usageString(m_usage)));
options->putDirect(vm, vm.propertyNames->sensitivity, jsNontrivialString(vm, sensitivityString(m_sensitivity)));
options->putDirect(vm, vm.propertyNames->ignorePunctuation, jsBoolean(m_ignorePunctuation));
options->putDirect(vm, vm.propertyNames->collation, jsString(vm, m_collation));
options->putDirect(vm, vm.propertyNames->numeric, jsBoolean(m_numeric));
options->putDirect(vm, vm.propertyNames->caseFirst, jsNontrivialString(vm, caseFirstString(m_caseFirst)));
return options;
}
void IntlCollator::setBoundCompare(VM& vm, JSBoundFunction* format)
{
m_boundCompare.set(vm, this, format);
}
static bool canDoASCIIUCADUCETComparisonWithUCollator(UCollator& collator)
{
// Attributes are default ones unless we set. So, non-configured attributes are default ones.
static constexpr std::pair<UColAttribute, UColAttributeValue> attributes[] = {
{ UCOL_FRENCH_COLLATION, UCOL_OFF },
{ UCOL_ALTERNATE_HANDLING, UCOL_NON_IGNORABLE },
{ UCOL_STRENGTH, UCOL_TERTIARY },
{ UCOL_CASE_LEVEL, UCOL_OFF },
{ UCOL_CASE_FIRST, UCOL_OFF },
{ UCOL_NUMERIC_COLLATION, UCOL_OFF },
// We do not check UCOL_NORMALIZATION_MODE status since FCD normalization does nothing for ASCII strings.
};
for (auto& pair : attributes) {
UErrorCode status = U_ZERO_ERROR;
auto result = ucol_getAttribute(&collator, pair.first, &status);
ASSERT(U_SUCCESS(status));
if (result != pair.second)
return false;
}
// Check existence of tailoring rules. If they do not exist, collation algorithm is UCA DUCET.
int32_t length = 0;
ucol_getRules(&collator, &length);
return !length;
}
bool IntlCollator::updateCanDoASCIIUCADUCETComparison() const
{
// ICU uses the CLDR root collation order as a default starting point for ordering. (The CLDR root collation is based on the UCA DUCET.)
// And customizes this root collation via rules.
// The root collation is UCA DUCET and it is code-point comparison if the characters are all ASCII.
// http://www.unicode.org/reports/tr10/
ASSERT(m_collator);
auto checkASCIIUCADUCETComparisonCompatibility = [&] {
if (m_usage != Usage::Sort)
return false;
if (m_collation != "default"_s)
return false;
if (m_sensitivity != Sensitivity::Variant)
return false;
if (m_caseFirst != CaseFirst::False)
return false;
if (m_numeric)
return false;
if (m_ignorePunctuation)
return false;
return canDoASCIIUCADUCETComparisonWithUCollator(*m_collator);
};
bool result = checkASCIIUCADUCETComparisonCompatibility();
m_canDoASCIIUCADUCETComparison = triState(result);
return result;
}
#if ASSERT_ENABLED
void IntlCollator::checkICULocaleInvariants(const LocaleSet& locales)
{
for (auto& locale : locales) {
auto checkASCIIOrderingWithDUCET = [](const String& locale, UCollator& collator) {
bool allAreGood = true;
for (unsigned x = 0; x < 128; ++x) {
for (unsigned y = 0; y < 128; ++y) {
if (canUseASCIIUCADUCETComparison(x) && canUseASCIIUCADUCETComparison(y)) {
UErrorCode status = U_ZERO_ERROR;
UChar xstring[] = { static_cast<UChar>(x), 0 };
UChar ystring[] = { static_cast<UChar>(y), 0 };
auto resultICU = ucol_strcoll(&collator, xstring, 1, ystring, 1);
ASSERT(U_SUCCESS(status));
auto resultJSC = compareASCIIWithUCADUCET(xstring, 1, ystring, 1);
if (resultICU != resultJSC) {
dataLogLn("BAD ", locale, " ", makeString(hex(x)), "(", StringView(xstring, 1), ") <=> ", makeString(hex(y)), "(", StringView(ystring, 1), ") ICU:(", static_cast<int32_t>(resultICU), "),JSC:(", static_cast<int32_t>(resultJSC), ")");
allAreGood = false;
}
}
}
}
return allAreGood;
};
UErrorCode status = U_ZERO_ERROR;
auto collator = std::unique_ptr<UCollator, ICUDeleter<ucol_close>>(ucol_open(locale.ascii().data(), &status));
ASSERT(U_SUCCESS(status));
ucol_setAttribute(collator.get(), UCOL_STRENGTH, UCOL_TERTIARY, &status);
ASSERT(U_SUCCESS(status));
ucol_setAttribute(collator.get(), UCOL_CASE_LEVEL, UCOL_OFF, &status);
ASSERT(U_SUCCESS(status));
ucol_setAttribute(collator.get(), UCOL_CASE_FIRST, UCOL_OFF, &status);
ASSERT(U_SUCCESS(status));
ucol_setAttribute(collator.get(), UCOL_NUMERIC_COLLATION, UCOL_OFF, &status);
ASSERT(U_SUCCESS(status));
ucol_setAttribute(collator.get(), UCOL_ALTERNATE_HANDLING, UCOL_DEFAULT, &status);
ASSERT(U_SUCCESS(status));
ucol_setAttribute(collator.get(), UCOL_NORMALIZATION_MODE, UCOL_ON, &status);
ASSERT(U_SUCCESS(status));
if (!canDoASCIIUCADUCETComparisonWithUCollator(*collator))
continue;
// This should not have reorder.
int32_t length = ucol_getReorderCodes(collator.get(), nullptr, 0, &status);
ASSERT(U_SUCCESS(status));
ASSERT(!length);
// Contractions and Expansions are defined as a rule. If there is no tailoring rule, then they should be UCA DUCET's default.
auto ensureNotIncludingASCII = [&](USet& set) {
Vector<UChar, 32> buffer;
for (int32_t index = 0, count = uset_getItemCount(&set); index < count; ++index) {
// start and end are inclusive.
UChar32 start = 0;
UChar32 end = 0;
auto status = callBufferProducingFunction(uset_getItem, &set, index, &start, &end, buffer);
ASSERT(U_SUCCESS(status));
if (buffer.isEmpty()) {
if (isASCII(start)) {
dataLogLn("BAD ", locale, " including ASCII tailored characters");
CRASH();
}
} else {
if (StringView(buffer.data(), buffer.size()).isAllASCII()) {
dataLogLn("BAD ", locale, " ", String(buffer.data(), buffer.size()), " including ASCII tailored characters");
CRASH();
}
}
}
};
auto contractions = std::unique_ptr<USet, ICUDeleter<uset_close>>(uset_openEmpty());
auto expansions = std::unique_ptr<USet, ICUDeleter<uset_close>>(uset_openEmpty());
ucol_getContractionsAndExpansions(collator.get(), contractions.get(), expansions.get(), true, &status);
ASSERT(U_SUCCESS(status));
ensureNotIncludingASCII(*contractions);
ensureNotIncludingASCII(*expansions);
// This locale should not have tailoring.
auto tailored = std::unique_ptr<USet, ICUDeleter<uset_close>>(ucol_getTailoredSet(collator.get(), &status));
ensureNotIncludingASCII(*tailored);
dataLogLnIf(IntlCollatorInternal::verbose, "LOCALE ", locale);
ASSERT(checkASCIIOrderingWithDUCET(locale, *collator));
}
}
#endif
} // namespace JSC