diff --git a/test/files/fuzz_issues/rtrip_fail38 b/test/files/fuzz_issues/rtrip_fail38 new file mode 100644 index 00000000..bfeefc0b --- /dev/null +++ b/test/files/fuzz_issues/rtrip_fail38 @@ -0,0 +1 @@ +ZSh^-3999h$/991(•úÿ) \ No newline at end of file diff --git a/test/fuzz_issue_tests.cpp b/test/fuzz_issue_tests.cpp index cfe68363..3689ed78 100644 --- a/test/fuzz_issue_tests.cpp +++ b/test/fuzz_issue_tests.cpp @@ -10,6 +10,7 @@ SPDX-License-Identifier: BSD-3-Clause #include #include +#include #include #include #include @@ -232,9 +233,42 @@ TEST_P(rtripProblems, rtripFiles) { auto cdata = loadFailureFile("rtrip_fail", GetParam()); auto u1 = unit_from_string(cdata); + if (GetParam() == 38) { + std::cerr << "rtrip parametrized input length: " << cdata.size() + << "\n"; + std::cerr << "rtrip parametrized input (hex): "; + for (const auto byte : cdata) { + std::cerr + << std::hex << std::setw(2) << std::setfill('0') + << static_cast(static_cast(byte)); + } + std::cerr << std::dec << "\n"; + std::cerr << "rtrip parametrized u1 error: " << std::boolalpha + << is_error(u1) << ", multiplier: " << u1.multiplier() + << "\n"; + } if (!is_error(u1)) { auto str = to_string(u1); auto u2 = unit_from_string(str); + if (GetParam() == 38) { + std::cerr << "rtrip parametrized serialized: [" << str << "]\n"; + std::cerr << "rtrip parametrized u2 error: " << is_error(u2) + << ", multiplier: " << u2.multiplier() + << ", e flag: " << u2.base_units().has_e_flag() + << ", equation: " << u2.base_units().is_equation() + << "\n"; + std::cerr << "rtrip parametrized u1 flags: e=" + << u1.base_units().has_e_flag() + << ", equation=" << u1.base_units().is_equation() << "\n"; + std::cerr << "rtrip parametrized root1 error: " + << is_error(root(u1, 2)) + << ", e flag: " << root(u1, 2).base_units().has_e_flag() + << "\n"; + std::cerr << "rtrip parametrized root2 error: " + << is_error(root(u2, 2)) + << ", e flag: " << root(u2, 2).base_units().has_e_flag() + << "\n"; + } EXPECT_FALSE(is_error(u2)); if (u2 == u1) { EXPECT_EQ(u2, u1); @@ -255,30 +289,121 @@ TEST_P(rtripProblems, rtripFiles) } } -INSTANTIATE_TEST_SUITE_P(rtripFiles, rtripProblems, ::testing::Range(1, 38)); +INSTANTIATE_TEST_SUITE_P(rtripFiles, rtripProblems, ::testing::Range(1, 39)); TEST(fuzzFailures, rtripSingleProblems) { - auto cdata = loadFailureFile("rtrip_fail", 37); + auto cdata = loadFailureFile("rtrip_fail", 38); + ASSERT_FALSE(cdata.empty()); auto u1 = unit_from_string(cdata); - if (!is_error(u1)) { + std::cerr << "rtrip input (hex): "; + for (const auto byte : cdata) { + std::cerr + << std::hex << std::setw(2) << std::setfill('0') + << static_cast(static_cast(byte)); + } + std::cerr << std::dec << "\n"; + std::cerr << "input length: " << cdata.size() << "\n"; + std::cerr << "u1 error: " << std::boolalpha << is_error(u1) + << ", multiplier: " << u1.multiplier() << "\n"; + if (is_error(u1)) { + std::cerr << "u1 was rejected; no round-trip values available\n"; + } else { auto str = to_string(u1); - std::cout << str << '\n'; auto u2 = unit_from_string(str); + auto preciseRoot1 = root(u1, 2); + auto preciseRoot2 = root(u2, 2); + auto root1 = root(unit_cast(u1), 2); + auto root2 = root(unit_cast(u2), 2); + const auto printUnitData = [](const char* name, + const precise_unit& unit) { + const auto base = unit.base_units(); + std::cerr << name << " base: m=" << base.meter() + << ", kg=" << base.kg() << ", s=" << base.second() + << ", A=" << base.ampere() << ", K=" << base.kelvin() + << ", mol=" << base.mole() << ", cd=" << base.candela() + << ", rad=" << base.radian() + << ", currency=" << base.currency() + << ", count=" << base.count() + << ", i=" << base.has_i_flag() + << ", e=" << base.has_e_flag() + << ", equation=" << base.is_equation() << "\n"; + }; + std::cerr << "serialized: [" << str << "]\n"; + std::cerr << "u2 error: " << is_error(u2) + << ", multiplier: " << u2.multiplier() + << ", e flag: " << u2.base_units().has_e_flag() + << ", equation: " << u2.base_units().is_equation() << "\n"; + std::cerr << "u1 flags: e=" << u1.base_units().has_e_flag() + << ", equation=" << u1.base_units().is_equation() << "\n"; + const auto equationUnit = unit_from_string("EQXUN[8]"); + const auto zeroEquationUnit = unit_from_string("0*EQXUN[8]"); + std::cerr << "EQXUN[8]: error=" << is_error(equationUnit) + << ", equation=" << equationUnit.base_units().is_equation() + << ", e=" << equationUnit.base_units().has_e_flag() << "\n"; + std::cerr + << "0*EQXUN[8]: error=" << is_error(zeroEquationUnit) + << ", equation=" << zeroEquationUnit.base_units().is_equation() + << ", e=" << zeroEquationUnit.base_units().has_e_flag() << "\n"; + const auto equationPos = str.rfind("EQXUN[8]"); + if (equationPos != std::string::npos) { + const auto serializedTail = str.substr(equationPos); + const auto tailUnit = unit_from_string(serializedTail); + std::cerr << "serialized tail: [" << serializedTail + << "], error=" << is_error(tailUnit) + << ", equation=" << tailUnit.base_units().is_equation() + << ", e=" << tailUnit.base_units().has_e_flag() << "\n"; + } + if (str.size() > 2 && str[0] == '0' && str[1] == '*') { + const auto zeroPrefixTail = str.substr(2); + const auto zeroPrefixTailUnit = unit_from_string(zeroPrefixTail); + const auto zeroPrefixUnit = unit_from_string("0*" + zeroPrefixTail); + std::cerr << "zero-prefix tail: [" << zeroPrefixTail + << "], error=" << is_error(zeroPrefixTailUnit) + << ", equation=" + << zeroPrefixTailUnit.base_units().is_equation() + << ", e=" << zeroPrefixTailUnit.base_units().has_e_flag() + << "\n"; + std::cerr + << "zero-prefix parse: error=" << is_error(zeroPrefixUnit) + << ", equation=" << zeroPrefixUnit.base_units().is_equation() + << ", e=" << zeroPrefixUnit.base_units().has_e_flag() << "\n"; + } + printUnitData("u1", u1); + printUnitData("u2", u2); + printUnitData("precise root1", preciseRoot1); + printUnitData("precise root2", preciseRoot2); + std::cerr << "precise root1 error: " << is_error(preciseRoot1) + << ", multiplier: " << preciseRoot1.multiplier() + << ", e flag: " << preciseRoot1.base_units().has_e_flag() + << "\n"; + std::cerr << "precise root2 error: " << is_error(preciseRoot2) + << ", multiplier: " << preciseRoot2.multiplier() + << ", e flag: " << preciseRoot2.base_units().has_e_flag() + << "\n"; + std::cerr << "root1 error: " << is_error(root1) + << ", multiplier: " << root1.multiplier() + << ", e flag: " << root1.base_units().has_e_flag() << "\n"; + std::cerr << "root2 error: " << is_error(root2) + << ", multiplier: " << root2.multiplier() + << ", e flag: " << root2.base_units().has_e_flag() << "\n"; EXPECT_FALSE(is_error(u2)); if (u2 == u1) { EXPECT_EQ(u2, u1); EXPECT_EQ(unit_cast(u2), unit_cast(u1)); EXPECT_FALSE(units::unit_cast(u2) != units::unit_cast(u1)); } else if (!is_error(root(u2, 2))) { + std::cerr << "selected branch: square root\n"; EXPECT_EQ(root(unit_cast(u2), 2), root(unit_cast(u1), 2)); EXPECT_FALSE( root(units::unit_cast(u2), 2) != root(units::unit_cast(u1), 2)); } else if (!is_error(root(u2, 3))) { + std::cerr << "selected branch: cube root\n"; EXPECT_EQ(root(unit_cast(u2), 3), root(unit_cast(u1), 3)); EXPECT_FALSE( root(units::unit_cast(u2), 3) != root(units::unit_cast(u1), 3)); } else { + std::cerr << "selected branch: direct unit comparison\n"; auto uc1 = unit_cast(u1); auto uc2 = unit_cast(u2); EXPECT_EQ(uc2, uc1); diff --git a/test/test_unit_ops.cpp b/test/test_unit_ops.cpp index 2bb1d769..08d835aa 100644 --- a/test/test_unit_ops.cpp +++ b/test/test_unit_ops.cpp @@ -831,6 +831,9 @@ TEST(specialOps, rootHertz) EXPECT_FALSE(is_error(precise::special::ASD)); auto rh = precise::special::rootHertz; + auto asd = precise::m / precise::s.pow(2) / rh; + + EXPECT_EQ(asd, precise::special::ASD); EXPECT_EQ(rh.pow(1), rh); EXPECT_EQ(rh.pow(0), precise::one); @@ -851,6 +854,19 @@ TEST(specialOps, rootHertz) // EXPECT_EQ(rh.pow(-6), s.pow(3)); } +TEST(specialOps, rootMeterAndRootHertzDistinct) +{ + auto rm = precise::special::rootMeter; + auto rh = precise::special::rootHertz; + + EXPECT_NE(rm, rh); + + EXPECT_EQ(rh.pow(2), precise::Hz); + EXPECT_EQ(rh.pow(-2), precise::s); + + EXPECT_NE(rh.pow(2), precise::m); +} + TEST(specialOps, degC) { auto res = precise::degC.pow(2); diff --git a/test/test_unit_strings.cpp b/test/test_unit_strings.cpp index bbf59936..27751c47 100644 --- a/test/test_unit_strings.cpp +++ b/test/test_unit_strings.cpp @@ -557,6 +557,88 @@ TEST(stringToUnits, morePower) EXPECT_EQ(precise::us::mile.pow(2), unit_from_string("mi(USA)^(2)")); } +TEST(stringToUnits, decimalPowerExponents) +{ + EXPECT_EQ(precise::special::rootMeter, unit_from_string("m^0.5")); + EXPECT_EQ(precise::special::rootMeter, unit_from_string("m**0.5")); + EXPECT_EQ(precise::special::rootMeter, unit_from_string("m^(0.5)")); + EXPECT_EQ(precise::special::rootMeter, unit_from_string("sqrt(m)")); + EXPECT_EQ(precise::special::rootMeter.inv(), unit_from_string("m^-0.5")); + EXPECT_EQ(precise::special::rootMeter.inv(), unit_from_string("m^(-0.5)")); + EXPECT_EQ(precise::m.pow(2), unit_from_string("m^2.0")); + EXPECT_EQ(precise::m.pow(2), unit_from_string("m^2.00")); + EXPECT_EQ(precise::kg / precise::m.pow(2), unit_from_string("kg/(m^2)")); + EXPECT_EQ( + precise::kg / precise::m.pow(2), + unit_from_string("kg/(m2)")); + EXPECT_EQ(precise::Hz, unit_from_string("s^-1.0")); + EXPECT_EQ(precise::m.pow(2), unit_from_string("m^2e0")); +#ifdef UNITS_CONSTEXPR_IF_SUPPORTED + if constexpr (detail::bitwidth::base_size == sizeof(std::uint64_t)) { +#else + if (detail::bitwidth::base_size == sizeof(std::uint64_t)) { +#endif + EXPECT_EQ(precise::m.pow(200), unit_from_string("m^2e2")); + } else { + EXPECT_TRUE(is_error(unit_from_string("m^2e2"))); + } + EXPECT_EQ(precise::special::rootHertz, unit_from_string("Hz^0.5")); + EXPECT_EQ(precise::special::rootHertz, unit_from_string("Hz^(0.5)")); + EXPECT_EQ(precise::special::rootHertz.inv(), unit_from_string("sqrt(s)")); + EXPECT_EQ(unit_from_string("[m/s2/Hz^(1/2)]"), precise::special::ASD); + EXPECT_EQ(unit_from_string("m/s^2/Hz^0.5"), precise::special::ASD); + + const std::vector invalidPowers{ + "m^0.25", + "m^(0.25)", + "m^1.5", + "m^2.25", + "m^2.5", + "m^-2.5", + "m^2e", + "[(0.5)]^34s"}; + + for (const auto& powerString : invalidPowers) { + EXPECT_TRUE(is_error(unit_from_string(powerString))) << powerString; + } +} + +TEST(stringToUnits, bracketParenthesizedExponents) +{ + // The modified code specifically handles the case where: + // - There's a ']' before '^' + // - The content between brackets is parenthesized + // - That content parses as a pure number + // This pattern is invalid and should be rejected + + // Test that these invalid patterns are properly rejected + // (This exercises the strtod-based validation we modified) + EXPECT_TRUE(is_error(unit_from_string("[(0.5)]^34s"))); + EXPECT_TRUE(is_error(unit_from_string("[(2)]^3m"))); + EXPECT_TRUE(is_error(unit_from_string("[(1)]^5kg"))); + EXPECT_TRUE(is_error(unit_from_string("[(2.0)]^2m"))); + EXPECT_TRUE(is_error(unit_from_string("[(2e0)]^2m"))); + EXPECT_TRUE(is_error(unit_from_string("[(2e+1)]^1m"))); + EXPECT_TRUE(is_error(unit_from_string("[(2e-1)]^1m"))); + EXPECT_TRUE(is_error(unit_from_string("[(+2)]^2m"))); + EXPECT_TRUE(is_error(unit_from_string("[(-1)]^2m"))); + EXPECT_TRUE(is_error(unit_from_string("[(-0.5)]^3m"))); + + // These should be valid because the content is not purely numeric + // (the strtod parsing doesn't consume the entire string) + EXPECT_NE(unit_from_string("[(m)]"), precise::error); + EXPECT_NE(unit_from_string("[(m/s)]"), precise::error); + EXPECT_NE(unit_from_string("[(kg*m)]"), precise::error); + + // Partially numeric content - parsing stops before end + EXPECT_NE(unit_from_string("[(2m)]"), precise::error); + EXPECT_NE(unit_from_string("[(1.5kg)]"), precise::error); + EXPECT_NE(unit_from_string("[(0.5x)]"), precise::error); + + // Valid complex bracket notation that was already tested + EXPECT_EQ(unit_from_string("[m/s2/Hz^(1/2)]"), precise::special::ASD); +} + TEST(stringToUnits, specialUnits) { EXPECT_EQ( @@ -1953,6 +2035,31 @@ TEST(stringCleanup, withCommodities) res = detail::testing::testCleanUpString("1/m^2", ~commodities::aluminum); EXPECT_EQ(res, "1/m{aluminum}^2"); + + res = detail::testing::testCleanUpString( + "$/item^(32)*EQXUN[8]", ~commodities::aluminum); + EXPECT_EQ(res, "$/item{aluminum}^(32)*EQXUN[8]"); +} + +TEST(stringToUnits, trailingCommodityAfterPower) +{ + if (sizeof(UNITS_BASE_TYPE) != 8) { + return; + } + + const auto canonical = unit_from_string("0*$/item{aluminum}^(32)*EQXUN[8]"); + const auto legacy = unit_from_string("0*$/item^(32){aluminum}*EQXUN[8]"); + ASSERT_FALSE(is_error(canonical)); + ASSERT_FALSE(is_error(legacy)); + EXPECT_EQ(legacy, canonical); + + const auto serialized = to_string(canonical); + EXPECT_EQ(serialized, "0*$/item{aluminum}^(32)*EQXUN[8]"); + const auto reparsed = unit_from_string(serialized); + ASSERT_FALSE(is_error(reparsed)); + EXPECT_EQ(reparsed, canonical); + + EXPECT_TRUE(is_error(unit_from_string("0*item^(not_an_exponent)"))); } TEST(stringGeneration, addPowerString) diff --git a/units/commodities.cpp b/units/commodities.cpp index c9701146..0ebd9dd8 100644 --- a/units/commodities.cpp +++ b/units/commodities.cpp @@ -199,7 +199,8 @@ static uint32_t stringHash(const std::string& str) std::uint32_t hash{hashcodes::firstH}; for (auto c : str) { hash = (hash * hashcodes::Ac) ^ - (static_cast(c) * hashcodes::Bc); + (static_cast(static_cast(c)) * + hashcodes::Bc); } return hash; // or return h % C; } @@ -243,7 +244,10 @@ static void removeEscapeSequences(std::string& str) uint32_t getCommodity(std::string comm) { removeEscapeSequences(comm); - std::transform(comm.begin(), comm.end(), comm.begin(), ::tolower); + std::transform( + comm.begin(), comm.end(), comm.begin(), [](unsigned char character) { + return static_cast(std::tolower(character)); + }); if (allowCustomCommodities.load(std::memory_order_acquire)) { if (!customCommodityCodes.empty()) { auto fnd2 = customCommodityCodes.find(comm); @@ -292,7 +296,15 @@ std::string getCommodityName(std::uint32_t commodity) if (!customCommodityNames.empty()) { auto fnd2 = customCommodityNames.find(commodity); if (fnd2 != customCommodityNames.end()) { - return fnd2->second; + const auto& name = fnd2->second; + const auto printable = std::all_of( + name.begin(), name.end(), [](unsigned char character) { + return character >= 0x20U && character <= 0x7EU; + }); + if (printable) { + return name; + } + return std::string("CXCOMM[") + std::to_string(commodity) + "]"; } } } @@ -320,7 +332,13 @@ std::string getCommodityName(std::uint32_t commodity) void addCustomCommodity(std::string comm, std::uint32_t code) { if (allowCustomCommodities.load()) { - std::transform(comm.begin(), comm.end(), comm.begin(), ::tolower); + std::transform( + comm.begin(), + comm.end(), + comm.begin(), + [](unsigned char character) { + return static_cast(std::tolower(character)); + }); customCommodityNames.emplace(code, comm); customCommodityCodes.emplace(comm, code); } diff --git a/units/units.cpp b/units/units.cpp index 980d1438..3aa07076 100644 --- a/units/units.cpp +++ b/units/units.cpp @@ -1141,6 +1141,14 @@ static std::string } } } else { // inverse commodity + const auto multloc = propUnitString.find('*', 2); + if (propUnitString.compare(0, 2, "1/") == 0 && + multloc != std::string::npos && + (propUnitString.compare(multloc + 1, 5, "CXUN[") == 0 || + propUnitString.compare(multloc + 1, 6, "CXCUN[") == 0)) { + return propUnitString.substr(multloc + 1) + "/" + + propUnitString.substr(2, multloc - 2) + cString; + } auto loc = propUnitString.find_last_of('/'); if (loc == std::string::npos) { auto rs = checkForCustomUnit(cString); @@ -1159,7 +1167,14 @@ static std::string propUnitString.push_back('/'); propUnitString.append(cString); } else { - auto locp = propUnitString.find_last_of("^*"); + // Stop at the next multiplication separator. In particular, + // an equation-unit suffix ("*EQXUN[...]") must not cause the + // commodity to be appended after the powered denominator. + auto end = propUnitString.find('*', loc + 1); + if (end != std::string::npos && end > 0) { + --end; + } + auto locp = propUnitString.find_last_of("^*", end); if (locp == std::string::npos || locp < loc) { propUnitString.append(cString); } else { @@ -2251,7 +2266,12 @@ static double readNumericalWords(const std::string& ustring, size_t& index) std::string lcstring{ustring}; // make the string lower case for consistency std::transform( - lcstring.begin(), lcstring.end(), lcstring.begin(), ::tolower); + lcstring.begin(), + lcstring.end(), + lcstring.begin(), + [](unsigned char character) { + return static_cast(std::tolower(character)); + }); for (const auto& wp : groupNumericalWords) { auto loc = lcstring.find(std::get<0>(wp)); if (loc != std::string::npos) { @@ -2850,10 +2870,13 @@ static precise_unit continue; } } - if (unit[1] > 0 && (std::isupper(unit[1]) != 0) && - (std::toupper(unit[0]) == irep.first[0]) && + if (static_cast(unit[1]) > 0 && + (std::isupper(static_cast(unit[1])) != 0) && + (std::toupper(static_cast(unit[0])) == + irep.first[0]) && (unit[1] == irep.first[1])) { - unit[0] = std::toupper(unit[0]); + unit[0] = static_cast( + std::toupper(static_cast(unit[0]))); } } auto fnd = unit.find(irep.first); @@ -3923,7 +3946,12 @@ static void ciConversion(std::string& unit_string) }; // transform to upper case so we have a common starting point std::transform( - unit_string.begin(), unit_string.end(), unit_string.begin(), ::toupper); + unit_string.begin(), + unit_string.end(), + unit_string.begin(), + [](unsigned char character) { + return static_cast(std::toupper(character)); + }); auto fnd = ciConversions.find(unit_string); if (fnd != ciConversions.end()) { unit_string = fnd->second; @@ -3958,42 +3986,100 @@ static bool checkExponentOperations(const std::string& unit_string) // check all power operations auto cx = unit_string.find_first_of('^'); while (cx != std::string::npos) { + if (cx == 0) { + return false; + } const bool ndigit = isDigitCharacter(unit_string[cx - 1]); - ++cx; - const char c = unit_string[cx]; - if (!isDigitCharacter(c)) { - if (c == '-') { - if (!isDigitCharacter(unit_string[cx + 1])) { + if (unit_string[cx - 1] == ']') { + int index = static_cast(cx) - 2; + if (segmentcheckReverse(unit_string, '[', index)) { + const auto openBracket = static_cast(index) + 1; + auto contentStart = openBracket + 1; + auto contentLength = cx - contentStart - 1; + bool parenthesizedContent = false; + if (contentLength >= 2 && unit_string[contentStart] == '(' && + unit_string[contentStart + contentLength - 1] == ')') { + parenthesizedContent = true; + ++contentStart; + contentLength -= 2; + } + char* eptr{nullptr}; + const auto content = + unit_string.substr(contentStart, contentLength); + const auto parsedValue = std::strtod(content.c_str(), &eptr); + (void)parsedValue; + if (parenthesizedContent && !content.empty() && + eptr == content.c_str() + content.size()) { return false; } + } + } + ++cx; + if (cx >= unit_string.size()) { + return false; + } + const bool parenthesized = (unit_string[cx] == '('); + if (parenthesized) { + ++cx; + if (cx >= unit_string.size()) { + return false; + } + } + if (unit_string[cx] == '-' || unit_string[cx] == '+') { + ++cx; + } + const auto startDigit = cx; + bool dpoint_encountered = false; + while (cx < unit_string.size() && isDigitCharacter(unit_string[cx])) { + ++cx; + } + const auto wholeDigitCount = cx - startDigit; + if (cx < unit_string.size() && unit_string[cx] == '.') { + dpoint_encountered = true; + ++cx; + while (cx < unit_string.size() && + isDigitCharacter(unit_string[cx])) { ++cx; - } else if (c == '(') { + } + } + if (wholeDigitCount == 0 && !dpoint_encountered) { + return false; + } + if (cx < unit_string.size() && + (unit_string[cx] == 'e' || unit_string[cx] == 'E')) { + ++cx; + if (cx < unit_string.size() && + (unit_string[cx] == '-' || unit_string[cx] == '+')) { ++cx; - if (unit_string[cx] == '-') { - ++cx; - } - bool dpoint_encountered = false; - while (unit_string[cx] != ')') { - if (!isDigitCharacter(unit_string[cx])) { - if (unit_string[cx] == '.' && !dpoint_encountered) { - dpoint_encountered = true; - } else { - return false; - } - } - ++cx; - } - } else { + } + const auto exponentDigitStart = cx; + while (cx < unit_string.size() && + isDigitCharacter(unit_string[cx])) { + ++cx; + } + if (cx == exponentDigitStart) { return false; } } + if (parenthesized) { + if (cx >= unit_string.size() || unit_string[cx] != ')') { + return false; + } + } else if ( + cx < unit_string.size() && unit_string[cx] != '*' && + unit_string[cx] != '/' && unit_string[cx] != '^' && + unit_string[cx] != ')' && unit_string[cx] != ']' && + unit_string[cx] != '}') { + return false; + } else { + --cx; + } #ifdef UNITS_CONSTEXPR_IF_SUPPORTED if constexpr (detail::bitwidth::base_size == sizeof(std::uint32_t)) { #else if (detail::bitwidth::base_size == sizeof(std::uint32_t)) { #endif - if (unit_string.size() > cx + 1 && - isDigitCharacter(unit_string[cx + 1]) && !ndigit) { + if (wholeDigitCount > 1 && !ndigit) { // non representable unit power return false; } @@ -4085,9 +4171,10 @@ static bool checkValidUnitString( break; } } - if (!checkExponentOperations(unit_string)) { - return false; - } + } + + if (!checkExponentOperations(unit_string)) { + return false; } return true; @@ -4774,7 +4861,7 @@ static bool cleanUnitString(std::string& unit_string, std::uint64_t match_flags) if (seq > 1) { auto c2 = unit_string[fnd + seq]; if (c2 != '\0' && c2 != '*' && c2 != '/' && c2 != '^' && - c2 != 'e' && c2 != 'E') { + c2 != 'e' && c2 != 'E' && c2 != '.') { unit_string.insert(fnd + seq, 1, '*'); } } @@ -4796,7 +4883,9 @@ static bool cleanUnitString(std::string& unit_string, std::uint64_t match_flags) unit_string.begin() + bloc + 1, unit_string.begin() + ind - 1, unit_string.begin() + bloc + 1, - ::tolower); + [](unsigned char character) { + return static_cast(std::tolower(character)); + }); bloc = unit_string.find_first_of('{', ind); } else { bloc = std::string::npos; @@ -4999,7 +5088,9 @@ static precise_unit checkNamedUnitCode( codeString.begin(), codeString.end(), codeString.begin(), - ::toupper); + [](unsigned char character) { + return static_cast(std::toupper(character)); + }); } if (codeString.compare(0, 4, "X12:") == 0) { return x12_unit(codeString.substr(4)); @@ -5196,7 +5287,13 @@ static precise_unit checkForCustomUnit(const std::string& unit_string) return {1.0, precise::generate_custom_count_unit(0), hcode}; } - std::transform(csub.begin(), csub.end(), csub.begin(), ::tolower); + std::transform( + csub.begin(), + csub.end(), + csub.begin(), + [](unsigned char character) { + return static_cast(std::tolower(character)); + }); auto custcode = std::hash{}(csub); return precise::generate_custom_unit(custcode & 0x3FU); } @@ -5297,6 +5394,86 @@ static precise_unit unit_to_the_power_of( return precise::defunit; } +static bool + string_power_to_twice_power(const std::string& powerString, int& twicePower) +{ + char* eptr{nullptr}; + const auto power = std::strtod(powerString.c_str(), &eptr); + if (eptr != powerString.c_str() + powerString.size() || + !std::isfinite(power)) { + return false; + } + const auto scaledPower = 2.0 * power; + const auto roundedPower = std::round(scaledPower); + if (std::abs(scaledPower - roundedPower) > 1e-12 || + roundedPower > static_cast(std::numeric_limits::max()) || + roundedPower < static_cast(std::numeric_limits::min())) { + return false; + } + twicePower = static_cast(roundedPower); + return true; +} + +static precise_unit root_with_special_units(const precise_unit& un) +{ + auto retunit = root(un, 2); + if (!is_error(retunit)) { + return retunit; + } + + const auto meterPower = un.base_units().meter(); + if (meterPower % 2 != 0) { + const auto adjusted = + (meterPower > 0) ? un / precise::m : un * precise::m; + retunit = root(adjusted, 2); + if (!is_error(retunit)) { + return (meterPower > 0) ? retunit * precise::special::rootMeter : + retunit / precise::special::rootMeter; + } + } + + const auto secondPower = un.base_units().second(); + if (secondPower % 2 != 0) { + const auto adjusted = + (secondPower < 0) ? un / precise::Hz : un * precise::Hz; + retunit = root(adjusted, 2); + if (!is_error(retunit)) { + return (secondPower < 0) ? retunit * precise::special::rootHertz : + retunit / precise::special::rootHertz; + } + } + + return precise::invalid; +} + +static precise_unit unit_to_the_half_power_of( + const std::string& unit_string, + int twicePower, + std::uint64_t match_flags) +{ + const auto wholePower = twicePower / 2; + auto retunit = (wholePower == 0) ? + precise::one : + unit_to_the_power_of(unit_string, wholePower, match_flags); + if (is_error(retunit)) { + return precise::invalid; + } + + auto rootUnit = unit_to_the_power_of( + unit_string, (twicePower > 0) ? 1 : -1, match_flags); + if (is_error(rootUnit)) { + return precise::invalid; + } + rootUnit = root_with_special_units(rootUnit); + if (is_error(rootUnit)) { + return precise::invalid; + } + if (wholePower != 0 && (rootUnit.has_i_flag() || rootUnit.has_e_flag())) { + return precise::invalid; + } + return retunit * rootUnit; +} + static precise_unit checkSIprefix(const std::string& unit_string, std::uint64_t match_flags) { @@ -5565,6 +5742,13 @@ static precise_unit unit_from_string_internal( if (is_valid(retunit)) { return front_unit * retunit; } + // A failed expression with unit operators is not an + // unbraced commodity. In particular, do not discard its + // dimensions when a zero multiplier precedes it. + if (unit_string.substr(index).find_first_of("*/^{}") != + std::string::npos) { + return precise::invalid; + } auto commodity = getCommodity(unit_string.substr(index)); front_unit.commodity(commodity); return front_unit; @@ -5629,71 +5813,56 @@ static precise_unit unit_from_string_internal( const bool containsPer = (findWordOperatorSep(unit_string, "per") != std::string::npos); + // A commodity following a power is accepted for backwards compatibility + // with strings emitted before commodities were placed before exponents. + // Handle it before parsing '^', otherwise the commodity text is mistaken + // for part of the exponent. + if ((match_flags & no_commodities) == 0 && unit_string.back() == '}' && + unit_string.find('{') != std::string::npos) { + return commoditizedUnit(unit_string, match_flags); + } + sep = findOperatorSep(unit_string, "^"); if (sep != std::string::npos) { auto pchar = sep - 1; - if (unit_string[sep + 1] == '(') { - ++sep; - } - const char c1 = unit_string[sep + 1]; - int power{+1}; - if (c1 == '-' || c1 == '+') { - ++sep; - if (unit_string.length() < sep + 2) { - // this should have been caught as an invalid sequence - // earlier - return precise::invalid; // LCOV_EXCL_LINE - } - // the - ',' is a +/- sign - power = -(c1 - ','); - } - if (isDigitCharacter(unit_string[sep + 1])) { + const bool parenthesizedPower = (unit_string[sep + 1] == '('); + const auto powerStart = sep + (parenthesizedPower ? 2 : 1); + auto powerLength = unit_string.size() - powerStart; + if (parenthesizedPower) { + if (unit_string.back() != ')' || powerLength < 2) { + return precise::invalid; + } + --powerLength; + } + int twicePower{0}; + if (!string_power_to_twice_power( + unit_string.substr(powerStart, powerLength), twicePower)) { + return precise::invalid; + } #ifdef UNITS_CONSTEXPR_IF_SUPPORTED - if constexpr (sizeof(UNITS_BASE_TYPE) == 8) { + if constexpr (sizeof(UNITS_BASE_TYPE) != 8) { #else - if (sizeof(UNITS_BASE_TYPE) == 8) { + if (sizeof(UNITS_BASE_TYPE) != 8) { #endif - size_t end = sep + 2; - for (; end < unit_string.size() && - isDigitCharacter(unit_string[end]); - ++end) { - } - auto powerStringLength = end - sep - 1; - if (powerStringLength > 1) { - auto pstring = - unit_string.substr(sep + 1, powerStringLength); - char* eptr{nullptr}; - auto mpower = strtoul(pstring.c_str(), &eptr, 10); - if (eptr - pstring.c_str() == - static_cast(powerStringLength)) { - power *= mpower; - } else { - return precise::invalid; // LCOV_EXCL_LINE - } - } else { - power *= (unit_string[sep + 1] - '0'); - } - - } else { - power *= (unit_string[sep + 1] - '0'); + if (!isDigitCharacter(unit_string[pchar]) && + (twicePower > 18 || twicePower < -18)) { + return precise::invalid; } + } + + const auto powerUnitString = + unit_string.substr(0, (pchar > 0) ? pchar + 1 : 1); + if (twicePower % 2 == 0) { + retunit = unit_to_the_power_of( + powerUnitString, twicePower / 2, match_flags); } else { - // the check functions should catch this but it would be - // problematic if not caught - return precise::invalid; // LCOV_EXCL_LINE - } - retunit = unit_to_the_power_of( - unit_string.substr(0, (pchar > 0) ? pchar + 1 : 1), - power, - match_flags); + retunit = unit_to_the_half_power_of( + powerUnitString, twicePower, match_flags); + } if (retunit != precise::defunit) { return retunit; } } - if ((match_flags & no_commodities) == 0 && unit_string.back() == '}' && - unit_string.find('{') != std::string::npos) { - return commoditizedUnit(unit_string, match_flags); - } retunit = checkSIprefix(unit_string, match_flags); if (is_valid(retunit)) { return retunit; @@ -5759,7 +5928,12 @@ static precise_unit unit_from_string_internal( return retunit; } std::transform( - ustring.begin(), ustring.end(), ustring.begin(), ::toupper); + ustring.begin(), + ustring.end(), + ustring.begin(), + [](unsigned char character) { + return static_cast(std::toupper(character)); + }); retunit = get_unit(ustring, match_flags); if (is_valid(retunit)) { @@ -5945,7 +6119,12 @@ static precise_unit unit_from_string_internal( { ustring = unit_string; std::transform( - ustring.begin(), ustring.end(), ustring.begin(), ::tolower); + ustring.begin(), + ustring.end(), + ustring.begin(), + [](unsigned char character) { + return static_cast(std::tolower(character)); + }); if (ustring != unit_string) { retunit = unit_quick_match(ustring, match_flags); if (!is_error(retunit)) { @@ -6178,7 +6357,12 @@ precise_unit default_unit(std::string unit_type) } } std::transform( - unit_type.begin(), unit_type.end(), unit_type.begin(), ::tolower); + unit_type.begin(), + unit_type.end(), + unit_type.begin(), + [](unsigned char character) { + return static_cast(std::tolower(character)); + }); unit_type.erase( std::remove(unit_type.begin(), unit_type.end(), ' '), unit_type.end()); auto fnd = measurement_types.find(unit_type); diff --git a/units/units_conversion_maps.hpp b/units/units_conversion_maps.hpp index beaea07d..5948f17f 100644 --- a/units/units_conversion_maps.hpp +++ b/units/units_conversion_maps.hpp @@ -13,7 +13,7 @@ SPDX-License-Identifier: BSD-3-Clause namespace UNITS_NAMESPACE { -UNITS_CPP14_CONSTEXPR_OBJECT std::array, 113> +UNITS_CPP14_CONSTEXPR_OBJECT std::array, 114> defined_unit_names_si{ {{m, "m"}, {m * m, "m^2"}, @@ -81,6 +81,7 @@ UNITS_CPP14_CONSTEXPR_OBJECT std::array, 113> {percent, "%"}, {unit_cast(precise::special::ASD), "ASD"}, {unit_cast(precise::special::rootHertz), "rootHertz"}, + {unit_cast(precise::special::rootMeter), "rootMeter"}, {currency, "$"}, {count, "count"}, {ratio, ""}, @@ -204,7 +205,7 @@ UNITS_CPP14_CONSTEXPR_OBJECT std::array, 55> /// definitions for the default units for specific types of measurmeents UNITS_CPP14_CONSTEXPR_OBJECT std::array< std::pair, - 1216> + 1219> defined_unit_strings_si{ {{"", precise::defunit}, {"[]", precise::defunit}, @@ -1409,6 +1410,8 @@ UNITS_CPP14_CONSTEXPR_OBJECT std::array< {"ASD", precise::special::ASD}, {"[m/s2/Hz^(1/2)]", precise::special::ASD}, {"[M/s2/HZ^(1/2)]", precise::special::ASD}, + {"sqrt(m)", precise::special::rootMeter}, + {"sqrt(s)", precise::special::rootHertz.inv()}, {"Hz^(1/2)", precise::special::rootHertz}, {"HZ^(1/2)", precise::special::rootHertz}, {"squarerootofhertz", precise::special::rootHertz}, @@ -1416,6 +1419,7 @@ UNITS_CPP14_CONSTEXPR_OBJECT std::array< {"roothertz", precise::special::rootHertz}, // capitalized version is needed since this is also a generated unit {"rootHertz", precise::special::rootHertz}, + {"rootmeter", precise::special::rootMeter}, {"B", precise::data::byte}, {"bel", precise::log::bel}, {"dB", precise::log::dB}, diff --git a/units/units_decl.hpp b/units/units_decl.hpp index c0167e3f..a41bf1ea 100644 --- a/units/units_decl.hpp +++ b/units/units_decl.hpp @@ -460,7 +460,6 @@ namespace detail { 0 : (power / 2) * ((second_ < 0) || (power < 0) ? 9 : -9); } - // needs to be defined for the full 32 bits(or 64 bits) signed int meter_ : bitwidth::meter; signed int second_ : bitwidth::second; // 8