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:
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()
|
||||
@@ -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<size_t>(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<size_t>(converted_length) - 1);
|
||||
return utf8_string;
|
||||
}
|
||||
Reference in new issue
Block a user