Bug 840706 - Part 2: Make ToDouble and ToFloat more spec-compliant, r=smaug,xpcom-reviewers,barret

The main use of this method appears to be when parsing attributes with floating
point values in HTML. By changing from PR_strtod, this change does have some
semantic differences around edge-cases, though I've tried to keep it compliant
with the wording in the standard.

Here are some of the edge-cases I investigated:

1. We appear to build nspr with INFNAN_CHECK undefined, meaning that we did not
   parse strings like "inf", "infinity" and "nan" both before and after this
   change.
2. PR_strtod already ignored locale (built with USE_LOCALE undefined), so
   locale behaviour shouldn't have changed.
3. Neither PR_strtod nor the spec support hex floats, so I didn't enable them
   in double-conversion.
4. Leading whitespace is skipped by both PR_strtod and in the spec, so is
   skipped after this change.
5. Numbers too large to fit in a double (e.g. 2E308) were previously returned
   as inf or -inf by PR_strtod. The spec specifies that these values should return
   an error instead, so they have been changed to produce an error with this
   change.
   This is a web-visible change (<progress value=2E308 max=10> is indeterminate
   in Chromium and complete in Firefox before this change, it will be
   indeterminate for both after the change).
6. Parsing for floats & doubles are now handled seperately due to
   differences in the maximum and minimum representable values.

Differential Revision: https://phabricator.services.mozilla.com/D148305
This commit is contained in:
Nika Layzell 2022-12-06 20:27:49 +00:00
Родитель 97bb85687e
Коммит adfb1a6344
5 изменённых файлов: 214 добавлений и 138 удалений

Просмотреть файл

@ -26,40 +26,6 @@ bool nsTString<T>::SetCharAt(char16_t aChar, index_type aIndex) {
return true;
}
/**
* nsTString::ToDouble
*/
template <>
double nsTString<char>::ToDouble(bool aAllowTrailingChars,
nsresult* aErrorCode) const {
double res = 0.0;
if (this->Length() > 0) {
char* conv_stopped;
const char* str = this->get();
// Use PR_strtod, not strtod, since we don't want locale involved.
res = PR_strtod(str, &conv_stopped);
if (aAllowTrailingChars && conv_stopped != str) {
*aErrorCode = NS_OK;
} else if (!aAllowTrailingChars && conv_stopped == str + this->Length()) {
*aErrorCode = NS_OK;
} else {
*aErrorCode = NS_ERROR_ILLEGAL_VALUE;
}
} else {
// The string was too short (0 characters)
*aErrorCode = NS_ERROR_ILLEGAL_VALUE;
}
return res;
}
template <>
double nsTString<char16_t>::ToDouble(bool aAllowTrailingChars,
nsresult* aErrorCode) const {
NS_LossyConvertUTF16toASCII cString(BeginReading(), Length());
return aAllowTrailingChars ? cString.ToDoubleAllowTrailingChars(aErrorCode)
: cString.ToDouble(aErrorCode);
}
template <typename T>
void nsTString<T>::Rebind(const char_type* data, size_type length) {
// If we currently own a buffer, release it.

Просмотреть файл

@ -180,37 +180,6 @@ class nsTString : public nsTSubstring<T> {
char_type operator[](index_type aIndex) const { return CharAt(aIndex); }
/**
* Perform string to double-precision float conversion.
*
* @param aErrorCode will contain error if one occurs
* @return double-precision float rep of string value
*/
double ToDouble(nsresult* aErrorCode) const {
return ToDouble(/* aAllowTrailingChars = */ false, aErrorCode);
}
/**
* Perform string to single-precision float conversion.
*
* @param aErrorCode will contain error if one occurs
* @return single-precision float rep of string value
*/
float ToFloat(nsresult* aErrorCode) const {
return float(ToDouble(aErrorCode));
}
/**
* Similar to above ToDouble and ToFloat but allows trailing characters that
* are not converted.
*/
double ToDoubleAllowTrailingChars(nsresult* aErrorCode) const {
return ToDouble(/* aAllowTrailingChars = */ true, aErrorCode);
}
float ToFloatAllowTrailingChars(nsresult* aErrorCode) const {
return float(ToDoubleAllowTrailingChars(aErrorCode));
}
/**
* Set a char inside this string at given index
*
@ -248,8 +217,6 @@ class nsTString : public nsTSubstring<T> {
friend const nsTString<char>& VoidCString();
friend const nsTString<char16_t>& VoidString();
double ToDouble(bool aAllowTrailingChars, nsresult* aErrorCode) const;
// Used by Null[C]String.
explicit nsTString(DataFlags aDataFlags)
: substring_type(char_traits::sEmptyBuffer, 0,
@ -257,13 +224,6 @@ class nsTString : public nsTSubstring<T> {
ClassFlags::NULL_TERMINATED) {}
};
template <>
double nsTString<char>::ToDouble(bool aAllowTrailingChars,
nsresult* aErrorCode) const;
template <>
double nsTString<char16_t>::ToDouble(bool aAllowTrailingChars,
nsresult* aErrorCode) const;
extern template class nsTString<char>;
extern template class nsTString<char16_t>;

Просмотреть файл

@ -6,6 +6,9 @@
#include "nsTStringRepr.h"
#include "double-conversion/string-to-double.h"
#include "mozilla/FloatingPoint.h"
#include "nsError.h"
#include "nsString.h"
namespace mozilla::detail {
@ -181,6 +184,89 @@ bool nsTStringRepr<T>::EqualsIgnoreCase(const std::string_view& aString) const {
});
}
// We can't use the method `StringToDoubleConverter::ToDouble` due to linking
// issues on Windows as it's in mozglue. Instead, implement the selection logic
// using an overload set.
//
// StringToFloat is used instead of StringToDouble for floats due to differences
// in rounding behaviour.
static void StringToFP(
const double_conversion::StringToDoubleConverter& aConverter,
const char* aData, size_t aLength, int* aProcessed, float* aResult) {
*aResult = aConverter.StringToFloat(aData, aLength, aProcessed);
}
static void StringToFP(
const double_conversion::StringToDoubleConverter& aConverter,
const char* aData, size_t aLength, int* aProcessed, double* aResult) {
*aResult = aConverter.StringToDouble(aData, aLength, aProcessed);
}
static void StringToFP(
const double_conversion::StringToDoubleConverter& aConverter,
const char16_t* aData, size_t aLength, int* aProcessed, float* aResult) {
*aResult = aConverter.StringToFloat(reinterpret_cast<const uc16*>(aData),
aLength, aProcessed);
}
static void StringToFP(
const double_conversion::StringToDoubleConverter& aConverter,
const char16_t* aData, size_t aLength, int* aProcessed, double* aResult) {
*aResult = aConverter.StringToDouble(reinterpret_cast<const uc16*>(aData),
aLength, aProcessed);
}
template <typename FloatT, typename CharT>
static FloatT ParseFloatingPoint(const nsTStringRepr<CharT>& aString,
bool aAllowTrailingChars,
nsresult* aErrorCode) {
// Performs conversion to double following the "rules for parsing
// floating-point number values" from the HTML standard.
// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#rules-for-parsing-floating-point-number-values
//
// This behaviour allows for leading spaces, and will not generate infinity or
// NaN values except in error conditions.
int flags = double_conversion::StringToDoubleConverter::ALLOW_LEADING_SPACES;
if (aAllowTrailingChars) {
flags |= double_conversion::StringToDoubleConverter::ALLOW_TRAILING_JUNK;
}
double_conversion::StringToDoubleConverter converter(
flags, mozilla::UnspecifiedNaN<double>(),
mozilla::UnspecifiedNaN<double>(), nullptr, nullptr);
FloatT result;
int processed;
StringToFP(converter, aString.Data(), aString.Length(), &processed, &result);
*aErrorCode = mozilla::IsFinite(result) ? NS_OK : NS_ERROR_ILLEGAL_VALUE;
return result;
}
template <typename T>
double nsTStringRepr<T>::ToDouble(nsresult* aErrorCode) const {
return ParseFloatingPoint<double, T>(*this, /* aAllowTrailingChars */ false,
aErrorCode);
}
template <typename T>
double nsTStringRepr<T>::ToDoubleAllowTrailingChars(
nsresult* aErrorCode) const {
return ParseFloatingPoint<double, T>(*this, /* aAllowTrailingChars */ true,
aErrorCode);
}
template <typename T>
float nsTStringRepr<T>::ToFloat(nsresult* aErrorCode) const {
return ParseFloatingPoint<float, T>(*this, /* aAllowTrailingChars */ false,
aErrorCode);
}
template <typename T>
float nsTStringRepr<T>::ToFloatAllowTrailingChars(nsresult* aErrorCode) const {
return ParseFloatingPoint<float, T>(*this, /* aAllowTrailingChars */ true,
aErrorCode);
}
} // namespace mozilla::detail
template class mozilla::detail::nsTStringRepr<char>;

Просмотреть файл

@ -420,6 +420,35 @@ class nsTStringRepr {
int32_t RFindCharInSet(const string_view& aSet, int32_t aOffset = -1) const;
/**
* Perform locale-independent string to double-precision float conversion.
*
* Leading spaces in the string will be ignored. The returned value will be
* finite unless aErrorCode is set to a failed status.
*
* @param aErrorCode will contain error if one occurs
* @return double-precision float rep of string value
*/
double ToDouble(nsresult* aErrorCode) const;
/**
* Perform locale-independent string to single-precision float conversion.
*
* Leading spaces in the string will be ignored. The returned value will be
* finite unless aErrorCode is set to a failed status.
*
* @param aErrorCode will contain error if one occurs
* @return single-precision float rep of string value
*/
float ToFloat(nsresult* aErrorCode) const;
/**
* Similar to above ToDouble and ToFloat but allows trailing characters that
* are not converted.
*/
double ToDoubleAllowTrailingChars(nsresult* aErrorCode) const;
float ToFloatAllowTrailingChars(nsresult* aErrorCode) const;
protected:
nsTStringRepr() = delete; // Never instantiate directly

Просмотреть файл

@ -46,6 +46,9 @@ using mozilla::BlackBox;
using mozilla::fallible;
using mozilla::IsAscii;
using mozilla::IsUtf8;
using mozilla::Maybe;
using mozilla::Nothing;
using mozilla::Some;
using mozilla::Span;
#define TestExample1 \
@ -1539,102 +1542,134 @@ TEST_F(Strings, huge_capacity) {
}
}
static void test_tofloat_helper(const nsString& aStr, float aExpected,
bool aSuccess) {
static void test_tofloat_helper(const nsString& aStr,
mozilla::Maybe<float> aExpected) {
nsresult result;
EXPECT_EQ(aStr.ToFloat(&result), aExpected);
if (aSuccess) {
EXPECT_EQ(result, NS_OK);
float value = aStr.ToFloat(&result);
if (aExpected) {
EXPECT_TRUE(NS_SUCCEEDED(result));
EXPECT_EQ(value, *aExpected);
} else {
EXPECT_NE(result, NS_OK);
EXPECT_TRUE(NS_FAILED(result));
}
}
TEST_F(Strings, tofloat) {
test_tofloat_helper(u"42"_ns, 42.f, true);
test_tofloat_helper(u"42.0"_ns, 42.f, true);
test_tofloat_helper(u"-42"_ns, -42.f, true);
test_tofloat_helper(u"+42"_ns, 42, true);
test_tofloat_helper(u"13.37"_ns, 13.37f, true);
test_tofloat_helper(u"1.23456789"_ns, 1.23456789f, true);
test_tofloat_helper(u"1.98765432123456"_ns, 1.98765432123456f, true);
test_tofloat_helper(u"0"_ns, 0.f, true);
test_tofloat_helper(u"1.e5"_ns, 100000, true);
test_tofloat_helper(u""_ns, 0.f, false);
test_tofloat_helper(u"42foo"_ns, 42.f, false);
test_tofloat_helper(u"foo"_ns, 0.f, false);
test_tofloat_helper(u"1.5e-"_ns, 1.5f, false);
test_tofloat_helper(u"42"_ns, Some(42.f));
test_tofloat_helper(u"42.0"_ns, Some(42.f));
test_tofloat_helper(u"-42"_ns, Some(-42.f));
test_tofloat_helper(u"+42"_ns, Some(42));
test_tofloat_helper(u"13.37"_ns, Some(13.37f));
test_tofloat_helper(u"1.23456789"_ns, Some(1.23456789f));
test_tofloat_helper(u"1.98765432123456"_ns, Some(1.98765432123456f));
test_tofloat_helper(u"0"_ns, Some(0.f));
test_tofloat_helper(u"1.e5"_ns, Some(100000));
test_tofloat_helper(u""_ns, Nothing());
test_tofloat_helper(u"42foo"_ns, Nothing());
test_tofloat_helper(u"foo"_ns, Nothing());
test_tofloat_helper(u"1.5e-"_ns, Nothing());
// Leading spaces are ignored
test_tofloat_helper(u" \t5"_ns, Some(5.f));
// Values which are too large generate an error
test_tofloat_helper(u"3.402823e38"_ns, Some(3.402823e+38));
test_tofloat_helper(u"1e39"_ns, Nothing());
test_tofloat_helper(u"-3.402823e38"_ns, Some(-3.402823e+38));
test_tofloat_helper(u"-1e39"_ns, Nothing());
// Values which are too small round to zero
test_tofloat_helper(u"1.4013e-45"_ns, Some(1.4013e-45f));
test_tofloat_helper(u"1e-46"_ns, Some(0.f));
test_tofloat_helper(u"-1.4013e-45"_ns, Some(-1.4013e-45f));
test_tofloat_helper(u"-1e-46"_ns, Some(-0.f));
}
static void test_tofloat_allow_trailing_chars_helper(const nsString& aStr,
float aExpected,
bool aSuccess) {
Maybe<float> aExpected) {
nsresult result;
EXPECT_EQ(aStr.ToFloatAllowTrailingChars(&result), aExpected);
if (aSuccess) {
EXPECT_EQ(result, NS_OK);
float value = aStr.ToFloatAllowTrailingChars(&result);
if (aExpected) {
EXPECT_TRUE(NS_SUCCEEDED(result));
EXPECT_EQ(value, *aExpected);
} else {
EXPECT_NE(result, NS_OK);
EXPECT_TRUE(NS_FAILED(result));
}
}
TEST_F(Strings, ToFloatAllowTrailingChars) {
test_tofloat_allow_trailing_chars_helper(u""_ns, 0.f, false);
test_tofloat_allow_trailing_chars_helper(u"foo"_ns, 0.f, false);
test_tofloat_allow_trailing_chars_helper(u"42foo"_ns, 42.f, true);
test_tofloat_allow_trailing_chars_helper(u"42-5"_ns, 42.f, true);
test_tofloat_allow_trailing_chars_helper(u"13.37.8"_ns, 13.37f, true);
test_tofloat_allow_trailing_chars_helper(u"1.5e-"_ns, 1.5f, true);
test_tofloat_allow_trailing_chars_helper(u""_ns, Nothing());
test_tofloat_allow_trailing_chars_helper(u"foo"_ns, Nothing());
test_tofloat_allow_trailing_chars_helper(u"42foo"_ns, Some(42.f));
test_tofloat_allow_trailing_chars_helper(u"42-5"_ns, Some(42.f));
test_tofloat_allow_trailing_chars_helper(u"13.37.8"_ns, Some(13.37f));
test_tofloat_allow_trailing_chars_helper(u"1.5e-"_ns, Some(1.5f));
}
static void test_todouble_helper(const nsString& aStr, double aExpected,
bool aSuccess) {
static void test_todouble_helper(const nsString& aStr,
Maybe<double> aExpected) {
nsresult result;
EXPECT_EQ(aStr.ToDouble(&result), aExpected);
if (aSuccess) {
EXPECT_EQ(result, NS_OK);
double value = aStr.ToDouble(&result);
if (aExpected) {
EXPECT_TRUE(NS_SUCCEEDED(result));
EXPECT_EQ(value, *aExpected);
} else {
EXPECT_NE(result, NS_OK);
EXPECT_TRUE(NS_FAILED(result));
}
}
TEST_F(Strings, todouble) {
test_todouble_helper(u"42"_ns, 42, true);
test_todouble_helper(u"42.0"_ns, 42, true);
test_todouble_helper(u"-42"_ns, -42, true);
test_todouble_helper(u"+42"_ns, 42, true);
test_todouble_helper(u"13.37"_ns, 13.37, true);
test_todouble_helper(u"1.23456789"_ns, 1.23456789, true);
test_todouble_helper(u"1.98765432123456"_ns, 1.98765432123456, true);
test_todouble_helper(u"123456789.98765432123456"_ns, 123456789.98765432123456,
true);
test_todouble_helper(u"0"_ns, 0, true);
test_todouble_helper(u"1.e5"_ns, 100000, true);
test_todouble_helper(u""_ns, 0, false);
test_todouble_helper(u"42foo"_ns, 42, false);
test_todouble_helper(u"foo"_ns, 0, false);
test_todouble_helper(u"1.5e-"_ns, 1.5, false);
test_todouble_helper(u"42"_ns, Some(42));
test_todouble_helper(u"42.0"_ns, Some(42));
test_todouble_helper(u"-42"_ns, Some(-42));
test_todouble_helper(u"+42"_ns, Some(42));
test_todouble_helper(u"13.37"_ns, Some(13.37));
test_todouble_helper(u"1.23456789"_ns, Some(1.23456789));
test_todouble_helper(u"1.98765432123456"_ns, Some(1.98765432123456));
test_todouble_helper(u"123456789.98765432123456"_ns,
Some(123456789.98765432123456));
test_todouble_helper(u"0"_ns, Some(0));
test_todouble_helper(u"1.e5"_ns, Some(100000));
test_todouble_helper(u""_ns, Nothing());
test_todouble_helper(u"42foo"_ns, Nothing());
test_todouble_helper(u"foo"_ns, Nothing());
test_todouble_helper(u"1.5e-"_ns, Nothing());
// Leading spaces are ignored
test_todouble_helper(u" \t5"_ns, Some(5.));
// Values which are too large generate an error
test_todouble_helper(u"1.797693e+308"_ns, Some(1.797693e+308));
test_todouble_helper(u"1e309"_ns, Nothing());
test_todouble_helper(u"-1.797693e+308"_ns, Some(-1.797693e+308));
test_todouble_helper(u"-1e309"_ns, Nothing());
// Values which are too small round to zero
test_todouble_helper(u"4.940656e-324"_ns, Some(4.940656e-324));
test_todouble_helper(u"1e-325"_ns, Some(0.));
test_todouble_helper(u"-4.940656e-324"_ns, Some(-4.940656e-324));
test_todouble_helper(u"-1e-325"_ns, Some(-0.));
}
static void test_todouble_allow_trailing_chars_helper(const nsString& aStr,
double aExpected,
bool aSuccess) {
Maybe<double> aExpected) {
nsresult result;
EXPECT_EQ(aStr.ToDoubleAllowTrailingChars(&result), aExpected);
if (aSuccess) {
EXPECT_EQ(result, NS_OK);
double value = aStr.ToDoubleAllowTrailingChars(&result);
if (aExpected) {
EXPECT_TRUE(NS_SUCCEEDED(result));
EXPECT_EQ(value, *aExpected);
} else {
EXPECT_NE(result, NS_OK);
EXPECT_TRUE(NS_FAILED(result));
}
}
TEST_F(Strings, ToDoubleAllowTrailingChars) {
test_todouble_allow_trailing_chars_helper(u""_ns, 0, false);
test_todouble_allow_trailing_chars_helper(u"foo"_ns, 0, false);
test_todouble_allow_trailing_chars_helper(u"42foo"_ns, 42, true);
test_todouble_allow_trailing_chars_helper(u"42-5"_ns, 42, true);
test_todouble_allow_trailing_chars_helper(u"13.37.8"_ns, 13.37, true);
test_todouble_allow_trailing_chars_helper(u"1.5e-"_ns, 1.5, true);
test_todouble_allow_trailing_chars_helper(u""_ns, Nothing());
test_todouble_allow_trailing_chars_helper(u"foo"_ns, Nothing());
test_todouble_allow_trailing_chars_helper(u"42foo"_ns, Some(42));
test_todouble_allow_trailing_chars_helper(u"42-5"_ns, Some(42));
test_todouble_allow_trailing_chars_helper(u"13.37.8"_ns, Some(13.37));
test_todouble_allow_trailing_chars_helper(u"1.5e-"_ns, Some(1.5));
}
TEST_F(Strings, Split) {