Files
Emil 05f92b6422 libCes CP932 fix for Windows/macOS, fix SaveDataDialog lockups (#878)
* libs: fix CP932 iconv conversion on Windows and macOS

SDL3's SDL_SYSTEM_ICONV build option defaults off on Windows and macOS,
so SDL falls back to its own bundled iconv there, which only implements
ASCII/Latin-1/UTF/UCS. SDL_iconv_open("UTF-8", "CP932") always failed as
a result, crashing games that convert Shift-JIS strings via
sceCesMbcsStrToUtf8Str (e.g. save-data dialogs).

Add a small CesIconv shim selected at compile time: Windows converts one
character at a time via the Win32 codepage-932 APIs, replicating
SDL_iconv's chunked semantics and error codes; macOS calls real POSIX
iconv directly, since libiconv is always present there and does support
CP932, unlike SDL's fallback. Linux is untouched, since SDL already
links real system iconv by default there.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* libs: fix SaveDataDialog infinite loop on save

SaveDataDialogOpen() jumped straight from SAVE_STATUS_INITIALIZED (1) to
SAVE_STATUS_FINISHED (3), skipping SAVE_STATUS_RUNNING (2) entirely. At
least one title's save-write dialog stage only accepts a FINISHED status
after observing a RUNNING poll first; without it, the title assumed the
Open() request never took effect and re-issued it forever, hanging the
save flow (this only became visible once the CP932 crash above stopped
masking it).

Open() now starts the dialog as RUNNING. UpdateStatus() advances the
state machine, reporting RUNNING once before flipping to FINISHED for
subsequent polls; GetStatus() is untouched, staying a pure peek with no
side effects, matching the real SDK's pump-vs-peek split between the two
calls.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Add iconv linking to macOS

* Fix missing macOS iconv link for tests

* Fix CP932 edge cases and save dialog status consistency

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: nmzik <Nmzik@mail.ru>
2026-09-28 18:42:22 +04:00

128 lines
4.8 KiB
C++

#include "libs/libCes.cpp"
#include <string>
#include <vector>
namespace Loader {
void SymbolDatabase::Add(const SymbolResolve&, uint64_t, const std::string&) {}
namespace Timer {
double GetTimeMs() {
return 0.0;
}
} // namespace Timer
} // namespace Loader
namespace {
using namespace Libs::LibCes;
int failures = 0;
#define CHECK(condition) \
do { \
if (!(condition)) { \
std::fprintf(stderr, "CesTests:%d: %s\n", __LINE__, #condition); \
++failures; \
} \
} while (false)
void CheckConversion(CesContext* context, const std::vector<uint8_t>& source,
const std::string& expected) {
std::vector<uint8_t> output(expected.size() + 2, 0xcc);
uint32_t consumed = 0;
uint32_t produced = 0;
CHECK(CesMbcsStrToUtf8Str(context, source.data(), 0, &consumed, output.data(),
static_cast<uint32_t>(expected.size() + 1), &produced) == 0);
CHECK(consumed == source.size() - 1);
CHECK(produced == expected.size());
CHECK(std::memcmp(output.data(), expected.data(), expected.size()) == 0);
CHECK(output[expected.size()] == 0);
CHECK(output.back() == 0xcc);
CHECK(CesMbcsStrGetUtf8Len(context, source.data(), 0, &consumed, &produced) == 0);
CHECK(consumed == source.size() - 1);
CHECK(produced == expected.size());
}
void TestMappingsAndMeasurement(CesContext* context) {
CheckConversion(context, {0}, "");
CheckConversion(context, {'S', 'a', 'v', 'e', ' ', 0x93, 0xfa, 0x96, 0x7b, 0xb6, 0},
"Save \xe6\x97\xa5\xe6\x9c\xac\xef\xbd\xb6");
// CP932 maps this character to U+FF5E, unlike strict Shift-JIS (U+301C).
CheckConversion(context, {0x81, 0x60, 0}, "\xef\xbd\x9e");
std::vector<uint8_t> source;
std::string expected;
for (int i = 0; i < 200; ++i) {
source.insert(source.end(), {0x82, 0xa0});
expected += "\xe3\x81\x82";
}
source.push_back(0);
CheckConversion(context, source, expected);
}
void TestPartialOutput(CesContext* context) {
const uint8_t source[] = {'A', 0x82, 0xa0, 'B', 0};
for (uint32_t capacity = 1; capacity <= 7; ++capacity) {
std::array<uint8_t, 8> output;
output.fill(0xcc);
uint32_t consumed = 99;
uint32_t produced = 99;
const int status =
CesMbcsStrToUtf8Str(context, source, 0, &consumed, output.data(), capacity, &produced);
const uint32_t expected_consumed = capacity == 1 ? 0
: capacity < 5 ? 1
: capacity == 5 ? 3
: 4;
const uint32_t expected_produced = capacity == 1 ? 0
: capacity < 5 ? 1
: capacity == 5 ? 4
: 5;
CHECK(status == (capacity < 6 ? CES_ERROR_DST_BUFFER_END : 0));
CHECK(consumed == expected_consumed);
CHECK(produced == expected_produced);
CHECK(std::memcmp(output.data(),
"A\xe3\x81\x82"
"B",
expected_produced) == 0);
CHECK(output[expected_produced] == 0);
CHECK(output[capacity] == 0xcc);
}
}
void TestErrors(CesContext* context) {
struct Case {
std::vector<uint8_t> source;
uint32_t limit;
int error;
};
const Case cases[] = {
{{'A', 0x80, 0}, 0, CES_ERROR_INVALID_ENCODE},
{{'A', 0x82, 0}, 0, CES_ERROR_INVALID_ENCODE},
{{'A', 0x82, 0xa0, 0}, 2, CES_ERROR_SRC_BUFFER_END},
{{'A', 0x82, 0x20, 0}, 0, CES_ERROR_INVALID_ENCODE},
{{'A', 0x81, 0xad, 0}, 0, CES_ERROR_UNASSIGNED_CODE},
};
for (const auto& test: cases) {
std::array<uint8_t, 16> output {};
uint32_t consumed = 99;
uint32_t produced = 99;
CHECK(CesMbcsStrToUtf8Str(context, test.source.data(), test.limit, &consumed, output.data(),
output.size(), &produced) == test.error);
CHECK(consumed == 1 && produced == 1);
CHECK(output[0] == 'A' && output[1] == 0);
CHECK(CesMbcsStrGetUtf8Len(context, test.source.data(), test.limit, &consumed, &produced) ==
test.error);
CHECK(consumed == 1 && produced == 1);
}
}
} // namespace
int main() {
CesProfile profile {};
CesContext context {};
CHECK(CesUcsProfileInitSJis1997Cp932(&profile) == &profile);
CHECK(CesMbcsUcsContextInit(&context, &profile) == 0);
TestMappingsAndMeasurement(&context);
TestPartialOutput(&context);
TestErrors(&context);
return failures == 0 ? 0 : 1;
}