Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion core/libs/prx/libSceJson2/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ if(BUILD_TESTING)
target_link_libraries(guest_json_tests PRIVATE libSceJson2 libc)
configure_windows_unwind(guest_json_tests)
add_test(NAME guest_json COMMAND guest_json_tests)
add_test(NAME guest_json_numeric_locale COMMAND guest_json_tests --numeric-locale)
set_tests_properties(guest_json_numeric_locale PROPERTIES SKIP_RETURN_CODE 77)
if(WIN32)
set_tests_properties(guest_json PROPERTIES ENVIRONMENT_MODIFICATION
set_tests_properties(guest_json guest_json_numeric_locale PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceJson2>")
endif()
endif()
20 changes: 16 additions & 4 deletions core/libs/prx/libSceJson2/Export.cpp
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
#include <algorithm>
#include <cerrno>
#include <charconv>
#include <cmath>
#include <cstdint>
#include <cstddef>
#include <cstdio>
#include <cstring>
#include <list>
#include <locale>
#include <mutex>
#include <sstream>
#include <stdexcept>
#include <string>
#include "SceTypes.hpp"
Expand Down Expand Up @@ -218,8 +221,9 @@ void AppendEscaped(std::string& out, const std::string& text) {
std::string RealText(double value) {
if (!std::isfinite(value)) throw std::runtime_error("sce::Json: cannot serialize a non-finite number");
char buffer[32];
std::snprintf(buffer, sizeof(buffer), "%.17g", value);
return buffer;
const auto result = std::to_chars(buffer, buffer + sizeof(buffer), value, std::chars_format::general, 17);
if (result.ec != std::errc{}) throw std::runtime_error("sce::Json: real number formatting failed");
return std::string(buffer, result.ptr);
}

void Serialize(std::string& out, const Node& n) {
Expand Down Expand Up @@ -372,8 +376,16 @@ class Parser {
}
}
}
const double real = std::strtod(text.c_str(), nullptr);
if (!std::isfinite(real)) throw std::runtime_error("sce::Json::Parser::parse: number " + text + " overflows a double; the console's behaviour is unverified");
double real = 0;
const auto parsed = std::from_chars(text.data(), text.data() + text.size(), real);
bool converted = parsed.ec == std::errc{} && parsed.ptr == text.data() + text.size();
if (parsed.ec == std::errc::result_out_of_range) {
std::istringstream stream(text);
stream.imbue(std::locale::classic());
stream >> real;
converted = !stream.fail() && stream.eof();
}
if (!converted || !std::isfinite(real)) throw std::runtime_error("sce::Json::Parser::parse: number " + text + " overflows a double; the console's behaviour is unverified");
out.type = TypeReal;
out.real = real;
return true;
Expand Down
75 changes: 74 additions & 1 deletion core/libs/tests/GuestJson.cpp
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include <cstddef>
#include <clocale>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <limits>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>

struct Value { void* node; };
Expand Down Expand Up @@ -762,7 +765,77 @@ static void StaticNullAccess() {
_ZN3sce4Json5ValueD1Ev(&fallback);
}

int main() {
static int NumericLocale() {
const std::string previous = std::setlocale(LC_NUMERIC, nullptr);
const char* selected = nullptr;
for (const char* name : {"de_DE.UTF-8", "de_DE.utf8", "de-DE", "German_Germany.1252", "fr_FR.UTF-8"}) {
if (std::setlocale(LC_NUMERIC, name) && std::strcmp(std::localeconv()->decimal_point, ",") == 0) {
selected = name;
break;
}
}
if (!selected) {
std::setlocale(LC_NUMERIC, previous.c_str());
std::fprintf(stderr, "No decimal-comma numeric locale is installed\n");
return 77;
}
std::printf("JSON numeric locale: %s\n", selected);
int failures = 0;
const auto check = [&](bool success, const char* operation, const char* actual) {
if (!success) {
std::fprintf(stderr, "%s failed: %s\n", operation, actual);
++failures;
}
};
for (const auto number : {1.5, -0.125, 1.25e30, std::numeric_limits<double>::max(),
std::numeric_limits<double>::min(), std::numeric_limits<double>::denorm_min()}) {
Value value{};
_ZN3sce4Json5ValueC1Ed(&value, number);
const auto text = Serialize(value);
check(text.find(',') == std::string::npos, "Decimal-point serialization", text.c_str());
Value parsed{};
_ZN3sce4Json5ValueC1Ev(&parsed);
const int result = _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&parsed, text.c_str(), text.size());
check(result == 0 && *_ZNK3sce4Json5Value7getRealEv(&parsed) == number, "Numeric round trip", text.c_str());
_ZN3sce4Json5ValueD1Ev(&parsed);
_ZN3sce4Json5ValueD1Ev(&value);
}
for (const auto [text, number] : {std::pair{"1.5", 1.5}, {"-1.25e2", -125.0},
{"4.9406564584124654e-324", std::numeric_limits<double>::denorm_min()},
{"2.4703282292062327e-324", 0.0},
{"2.4703282292062328e-324", std::numeric_limits<double>::denorm_min()},
{"18446744073709551616", 18446744073709551616.0},
{"1e-999", 0.0}, {"-1e-999", -0.0}}) {
Value value{};
_ZN3sce4Json5ValueC1Ev(&value);
const int result = _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&value, text, std::strlen(text));
const auto parsed = *_ZNK3sce4Json5Value7getRealEv(&value);
check(result == 0 && parsed == number && std::signbit(parsed) == std::signbit(number), "Decimal-point parsing", text);
_ZN3sce4Json5ValueD1Ev(&value);
}
Value root{};
_ZN3sce4Json5ValueC1Ev(&root);
constexpr char document[] = "{\"value\":1.5,\"list\":[2.5,-0.125]}";
check(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, document, sizeof(document) - 1) == 0,
"Nested numeric parsing", document);
const auto serialized = Serialize(root);
check(serialized == document, "Nested numeric serialization", serialized.c_str());
check(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, "1,5", 3) != 0, "Reject decimal comma", "1,5");
bool overflow = false;
try {
_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, "1e309", 5);
} catch (const std::runtime_error&) {
overflow = true;
}
check(overflow, "Retain overflow rejection", "1e309");
_ZN3sce4Json5ValueD1Ev(&root);
check(std::strcmp(std::localeconv()->decimal_point, ",") == 0, "Preserve host locale", selected);
std::setlocale(LC_NUMERIC, previous.c_str());
return failures ? 1 : 0;
}

int main(int argc, char** argv) {
if (argc == 2 && std::strcmp(argv[1], "--numeric-locale") == 0) return NumericLocale();
ParseAndRoundTrip();
NestingDepth();
ObjectsAndArrays();
Expand Down
2 changes: 2 additions & 0 deletions docs/dev/debt/libSceJson2.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

### Unknown function info

- [JSON real-number conversion](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) uses locale-independent `to_chars` with 17 significant digits and `from_chars`, following JSON's decimal-point grammar in RFC 8259 section 6. Out-of-range input falls back to a classic-locale stream so underflow retains the prior signed-zero behavior and overflow throws. Console rounding and formatting are unverified.

- [sce::Json::Parser::parse with a read callback](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the callback's contract is taken from PPSA04540's: it returns 0 with the next character, and at the end of its data writes `'\0'` and returns -1. Characters are read until the callback returns non-zero or gives `'\0'`, and the text read is then parsed as by the `(text, size)` overload, so trailing characters after the value are rejected as there. Whether the console stops reading at the end of the top-level value, and what it returns when the callback fails before the end, are unknown
- [sce::Json::Parser::parse](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - a number that overflows a double (e.g. `1e309`) throws: whether the console rejects the document or stores an infinite real is unknown
- [sce::Json::InitParameter2, InitParameterRtti and InitParameterRtti2](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - layouts inferred, not known. `InitParameter2` is taken as the 40-byte object of [SharpEmu](https://github.com/sharpemu/sharpemu/blob/1811cef94435db7655209fb39f65ddf258b93fd6/src/SharpEmu.Libs/Json/JsonExports.cs) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/eda6ba37ece127ab0396378404d99182e3e8a530/src/libs/libJson2.cpp): the allocator, its user data, the file buffer size, the 32-bit special float format type at 0x18 and 12 bytes both zero, and both constructors zero all of it. `InitParameterRtti2::setAllocatorRtti` writes the allocation parameters and user data where `setAllocator` writes the allocator, as both references do; whether the console also marks the object as using RTTI allocation is unknown. `InitParameterRtti`'s constructor writes the allocation parameters, user data and file buffer size to the first 24 bytes, an order inferred from its mangled arguments. The values of `SpecialFloatFormatType` and what they select are unknown, so `setSpecialFloatFormatType` stores any value and nothing reads it: `Initializer::initialize(InitParameter2)` ignores every field as `initialize(InitParameter)` does, serializing a non-finite real throws whatever the type, and whether the type lets the parser accept NaN or infinity is unknown. The setters return nothing; whether the console's return `this` is unknown
Expand Down
Loading