diff --git a/ground_suite/windows/runner/test_utf8_from_utf16.py b/ground_suite/windows/runner/test_utf8_from_utf16.py new file mode 100644 index 0000000..9cd9879 --- /dev/null +++ b/ground_suite/windows/runner/test_utf8_from_utf16.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""Run the production conversion function with a controlled Windows API stub. + +This host check compiles only Utf8FromUtf16 from utils.cpp. It checks API arguments, +counts, and failure handling using 16-bit wchar_t and known UTF-16/UTF-8 data. It +does not build the Windows runner or validate Windows' Unicode implementation. +""" + +import argparse +from pathlib import Path +import subprocess + + +PREFIX = r""" +#include +#include +#include + +static_assert(sizeof(wchar_t) == 2, "test requires UTF-16 code units"); +constexpr unsigned CP_UTF8 = 65001; +constexpr unsigned WC_ERR_INVALID_CHARS = 0x80; +static const wchar_t* expected_source; +static std::string expected_utf8; +static int query_result; +static int conversion_result; +static int calls; + +int WideCharToMultiByte(unsigned code_page, unsigned flags, const wchar_t* source, + int input_length, char* target, int capacity, + const char* default_char, int* used_default_char) { + ++calls; + assert(code_page == CP_UTF8 && flags == WC_ERR_INVALID_CHARS); + assert(source == expected_source && input_length == -1); + assert(default_char == nullptr && used_default_char == nullptr); + if (target == nullptr) { + assert(calls == 1 && capacity == 0); + return query_result; + } + assert(calls == 2 && capacity == query_result); + assert(capacity == static_cast(expected_utf8.size()) + 1); + std::memcpy(target, expected_utf8.c_str(), expected_utf8.size() + 1); + return conversion_result; +} +""" + +SUFFIX = r""" +void configure(const wchar_t* source, const std::string& expected, int query, + int converted) { + expected_source = source; + expected_utf8 = expected; + query_result = query; + conversion_result = converted; + calls = 0; +} + +int main() { + assert(Utf8FromUtf16(nullptr).empty()); + assert(calls == 0); + + const wchar_t empty[] = {0}; + configure(empty, "", 1, 1); + assert(Utf8FromUtf16(empty).empty()); + assert(calls == 2); + + // Independent known encoding: A, U+00E9, U+1F642 (a surrogate pair). + const wchar_t text[] = {0x0041, 0x00e9, 0xd83d, 0xde42, 0}; + const std::string utf8 = "A\xc3\xa9\xf0\x9f\x99\x82"; + configure(text, utf8, 8, 8); + assert(Utf8FromUtf16(text) == utf8); + assert(calls == 2); + + // WC_ERR_INVALID_CHARS reports an isolated surrogate as a zero query result. + const wchar_t invalid[] = {0xd800, 0}; + for (int failed : {0, -1}) { + configure(invalid, "", failed, 0); + assert(Utf8FromUtf16(invalid).empty()); + assert(calls == 1); + } + for (int failed_or_mismatched : {0, -1, 7, 9}) { + configure(text, utf8, 8, failed_or_mismatched); + assert(Utf8FromUtf16(text).empty()); + assert(calls == 2); + } +} +""" + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--work-dir", type=Path, required=True) + parser.add_argument("--cxx", default="c++") + args = parser.parse_args() + work = args.work_dir.resolve() + work.mkdir(parents=True, exist_ok=True) + source = Path(__file__).with_name("utils.cpp").read_text() + start = source.index("std::string Utf8FromUtf16(") + end = source.index("\n}", start) + 2 + fixture = work / "utf8_from_utf16.cpp" + fixture.write_text(PREFIX + source[start:end] + SUFFIX) + binary = work / "utf8_from_utf16_test" + subprocess.run( + [ + args.cxx, + "-std=c++17", + "-fshort-wchar", + "-Wall", + "-Wextra", + "-Werror", + "-pedantic", + str(fixture), + "-o", + str(binary), + ], + check=True, + ) + subprocess.run([str(binary)], check=True) + print("Utf8FromUtf16 host stub checks passed (9 cases, 16-bit wchar_t)") + + +if __name__ == "__main__": + main() diff --git a/ground_suite/windows/runner/utils.cpp b/ground_suite/windows/runner/utils.cpp index 3a0b465..cb5a55a 100644 --- a/ground_suite/windows/runner/utils.cpp +++ b/ground_suite/windows/runner/utils.cpp @@ -45,21 +45,24 @@ std::string Utf8FromUtf16(const wchar_t* utf16_string) { if (utf16_string == nullptr) { return std::string(); } - unsigned int target_length = ::WideCharToMultiByte( + const int target_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, - -1, nullptr, 0, nullptr, nullptr) - -1; // remove the trailing null character - int input_length = (int)wcslen(utf16_string); + -1, nullptr, 0, nullptr, nullptr); + if (target_length <= 0) { + return std::string(); + } std::string utf8_string; - if (target_length == 0 || target_length > utf8_string.max_size()) { + if (static_cast(target_length) > utf8_string.max_size()) { return utf8_string; } + // With a terminated input, both calls include the trailing null character. utf8_string.resize(target_length); int converted_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, - input_length, utf8_string.data(), target_length, nullptr, nullptr); - if (converted_length == 0) { + -1, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length != target_length) { return std::string(); } + utf8_string.resize(static_cast(converted_length) - 1); return utf8_string; }