fix(ground-suite): handle failed UTF-16 conversion sizes

Reject failed UTF-16 sizing queries before allocating the UTF-8 result
and preserve terminating-NUL accounting.

Validation: nine host-stub cases with 16-bit wchar_t; native Windows
execution remains unverified.
This commit is contained in:
Onur Özkan committed 2026-11-07 18:12:11 +03:00
1 parent a78754d909
commit 4953f0a974
2 files changed
+131 -7

No files matched your search

@@ -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 <cassert>
#include <cstring>
#include <string>
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<int>(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()
+10 -7
View File
@@ -45,21 +45,24 @@ std::string Utf8FromUtf16(const wchar_t* utf16_string) {
if (utf16_string == nullptr) { if (utf16_string == nullptr) {
return std::string(); return std::string();
} }
unsigned int target_length = ::WideCharToMultiByte( const int target_length = ::WideCharToMultiByte(
CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
-1, nullptr, 0, nullptr, nullptr) -1, nullptr, 0, nullptr, nullptr);
-1; // remove the trailing null character if (target_length <= 0) {
int input_length = (int)wcslen(utf16_string); return std::string();
}
std::string utf8_string; std::string utf8_string;
if (target_length == 0 || target_length > utf8_string.max_size()) { if (static_cast<size_t>(target_length) > utf8_string.max_size()) {
return utf8_string; return utf8_string;
} }
// With a terminated input, both calls include the trailing null character.
utf8_string.resize(target_length); utf8_string.resize(target_length);
int converted_length = ::WideCharToMultiByte( int converted_length = ::WideCharToMultiByte(
CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
input_length, utf8_string.data(), target_length, nullptr, nullptr); -1, utf8_string.data(), target_length, nullptr, nullptr);
if (converted_length == 0) { if (converted_length != target_length) {
return std::string(); return std::string();
} }
utf8_string.resize(static_cast<size_t>(converted_length) - 1);
return utf8_string; return utf8_string;
} }