Skip to content

Commit 4308a4f

Browse files
server: decode Windows OEM output to UTF-8 in built-in tools (ggml-org#26597)
a child process writes in the OEM code page, which is not UTF-8 on a western Windows install, so accented output reaches the JSON layer as invalid bytes and gets replaced there, silently losing the characters run() spawns without a console, so the child never inherits the console code page and GetOEMCP is the one that applies decode with MB_ERR_INVALID_CHARS so a wrong code page returns the text untouched instead of emitting replacement characters, and pass text that already decodes as UTF-8 through so a child emitting UTF-8 is never decoded twice the check drops an incomplete trailing sequence before validating, since a streamed chunk can end in the middle of a multi-byte character
1 parent 474c92e commit 4308a4f

1 file changed

Lines changed: 51 additions & 3 deletions

File tree

tools/server/server-tools.cpp

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,60 @@
1717
#include <functional>
1818
#include <memory>
1919

20+
#if defined(_WIN32)
21+
# ifndef NOMINMAX
22+
# define NOMINMAX
23+
# endif
24+
# include <windows.h>
25+
#endif
26+
2027
namespace fs = std::filesystem;
2128

2229
//
2330
// internal helpers
2431
//
2532

33+
#if defined(_WIN32)
34+
// A chunk can end in the middle of a multi-byte sequence, so the incomplete
35+
// tail is dropped before validating what precedes it.
36+
static bool is_utf8_text(const std::string & text) {
37+
return is_valid_utf8(text.substr(0, validate_utf8(text)));
38+
}
39+
40+
// A child process writes its output in the OEM code page, which is not UTF-8
41+
// on a western Windows install, so accented text reaches the JSON layer as
42+
// invalid bytes and is replaced there. Text that already decodes as UTF-8 is
43+
// returned untouched, so a child that emits UTF-8 is never decoded twice.
44+
// run() spawns without a console, so the console code page does not apply.
45+
static std::string console_output_to_utf8(const std::string & text) {
46+
if (text.empty() || is_utf8_text(text)) {
47+
return text;
48+
}
49+
50+
const UINT cp = GetOEMCP();
51+
52+
// fail rather than emit replacement characters when the code page is wrong
53+
const int wide_len = MultiByteToWideChar(cp, MB_ERR_INVALID_CHARS, text.data(), (int) text.size(), nullptr, 0);
54+
if (wide_len <= 0) {
55+
return text;
56+
}
57+
std::wstring wide(wide_len, L'\0');
58+
MultiByteToWideChar(cp, MB_ERR_INVALID_CHARS, text.data(), (int) text.size(), wide.data(), wide_len);
59+
60+
const int utf8_len = WideCharToMultiByte(CP_UTF8, 0, wide.data(), wide_len, nullptr, 0, nullptr, nullptr);
61+
if (utf8_len <= 0) {
62+
return text;
63+
}
64+
std::string utf8(utf8_len, '\0');
65+
WideCharToMultiByte(CP_UTF8, 0, wide.data(), wide_len, utf8.data(), utf8_len, nullptr, nullptr);
66+
return utf8;
67+
}
68+
#else
69+
static std::string console_output_to_utf8(const std::string & text) {
70+
return text;
71+
}
72+
#endif
73+
2674
json server_tool::to_json() const {
2775
return {
2876
{"display_name", display_name},
@@ -246,14 +294,14 @@ class tools_io_basic : public tools_io {
246294
size_t len = strlen(buf);
247295
if (output.size() + len <= max_output) {
248296
output.append(buf, len);
249-
if (on_chunk && !on_chunk(std::string(buf, len))) {
297+
if (on_chunk && !on_chunk(console_output_to_utf8(std::string(buf, len)))) {
250298
proc.terminate();
251299
break;
252300
}
253301
} else {
254302
size_t remaining = max_output - output.size();
255303
output.append(buf, remaining);
256-
if (on_chunk && remaining > 0) on_chunk(std::string(buf, remaining));
304+
if (on_chunk && remaining > 0) on_chunk(console_output_to_utf8(std::string(buf, remaining)));
257305
truncated = true;
258306
}
259307
}
@@ -267,7 +315,7 @@ class tools_io_basic : public tools_io {
267315

268316
res.exit_code = proc.join();
269317

270-
res.output = output;
318+
res.output = console_output_to_utf8(output);
271319
res.timed_out = timed_out.load();
272320
if (truncated) {
273321
res.output += "\n[output truncated]";

0 commit comments

Comments
 (0)