From 34a67f445407e15c0fb209c66fb2ecd381b74880 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 14 Sep 2026 06:52:19 +0800 Subject: [PATCH] 0.7.3 --- a started program's arguments and directory arrive as they were given The command line dropped every backslash: a run of them was counted and never written, and an element was converted in place just beyond the line, so quoting overwrote what it had not read. Every element was also quoted, which the command interpreter reads as a command rather than a switch. Elements are now written as they are unless the splitting would alter them, and quoted by the rule it inverts otherwise. A started program read its arguments through the name-narrowing that turns backslashes into slashes; arguments are now narrowed as given (clause 7.6). And the program and its directory were handed over in the \\?\ form, which the command interpreter refuses as a current directory; the prefix is removed wherever the name means the same without it. --- mcpp.toml | 2 +- src/env.cpp | 11 +++++--- src/process.cpp | 75 ++++++++++++++++++++++++++++++++++++++++--------- src/win.cpp | 5 ++-- src/win.h | 5 +++- 5 files changed, 76 insertions(+), 22 deletions(-) diff --git a/mcpp.toml b/mcpp.toml index 6ee0f53..59d93c3 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-windows" -version = "0.7.2" +version = "0.7.3" description = "An implementation of openkal for Windows, written on the Win32 interfaces and the object manager beneath them, using no C runtime symbol." license = "Apache-2.0" diff --git a/src/env.cpp b/src/env.cpp index 05e7acd..18eb531 100644 --- a/src/env.cpp +++ b/src/env.cpp @@ -33,10 +33,10 @@ int g_varc = 0; bool g_ready = false; -const char* store(const wchar_t* w, int wlen, okw_uptr& out_len) { +const char* store(const wchar_t* w, int wlen, okw_uptr& out_len, bool names) { if (g_used + 4 >= kText) { out_len = 0; return ""; } char* at = g_text + g_used; - const okw_uptr n = okw::narrow(w, static_cast(wlen), at, kText - g_used); + const okw_uptr n = okw::narrow(w, static_cast(wlen), at, kText - g_used, names); g_used += n + 1; out_len = n; return at; @@ -53,7 +53,10 @@ void prepare() { if (parts) { for (int i = 0; i < count && g_argc < kMaxArgs - 1; ++i) { okw_uptr len = 0; - g_argv[g_argc] = store(parts[i], wide_length(parts[i]), len); + // ⚠️ AS GIVEN, AND UNTIL 0.7.3 EVERY BACKSLASH CAME OUT AS A SLASH. + // An argument is not a name: `C:\dir' and a pattern's `\d' are what + // the caller wrote, and clause 7.6 requires the vector unaltered. + g_argv[g_argc] = store(parts[i], wide_length(parts[i]), len, false); g_argv_len[g_argc] = len; ++g_argc; } @@ -74,7 +77,7 @@ void prepare() { // bookkeeping and is not a variable a program set. if (p[0] != L'=') { okw_uptr total = 0; - const char* entry = store(p, len, total); + const char* entry = store(p, len, total, true); okw_uptr split = 0; while (split < total && entry[split] != '=') ++split; g_entry[g_varc] = entry; diff --git a/src/process.cpp b/src/process.cpp index 1e2de4a..3ccdf55 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -22,6 +22,35 @@ namespace { constexpr okw_uptr kCommandLine = 32768; // this environment's own bound +// ⚠️ THE NAMES A STARTED PROGRAM IS GIVEN, IN THE FORM EVERY PROGRAM READS. +// +// GetFinalPathNameByHandleW answers with the `\\?\' prefix, which tells this +// system to take the rest verbatim. CreateProcessW accepts it, and the started +// program inherits it as its current directory --- where the command interpreter +// refuses it ("UNC paths are not supported") and runs in the Windows directory +// instead. A batch file, which is how many tools are installed on this system, +// therefore ran somewhere other than where it was started. So the prefix is +// removed wherever the name means the same without it: a drive path within the +// classic bound, and a network share in its `\\server\share' form. A longer name +// keeps the prefix, because without it the name would not be accepted at all. +okw_uptr plain_name(wchar_t* s, okw_uptr n) { + // MAX_PATH, less the separator and the terminator a current directory takes. + constexpr okw_uptr kClassic = 258; + const bool verbatim = n >= 4 && s[0] == L'\\' && s[1] == L'\\' && s[2] == L'?' && s[3] == L'\\'; + if (!verbatim) return n; + const bool drive = n >= 7 && ((s[4] >= L'A' && s[4] <= L'Z') || (s[4] >= L'a' && s[4] <= L'z')) + && s[5] == L':' && s[6] == L'\\'; + const bool share = n >= 8 && (s[4] == L'U' || s[4] == L'u') && (s[5] == L'N' || s[5] == L'n') + && (s[6] == L'C' || s[6] == L'c') && s[7] == L'\\'; + okw_uptr drop = 0; + if (drive && n - 4 <= kClassic) drop = 4; // \\?\C:\dir -> C:\dir + else if (share && n - 6 <= kClassic) drop = 6; // \\?\UNC\host\x -> \\host\x + if (drop == 0) return n; + for (okw_uptr i = drop; i <= n; ++i) s[i - drop] = s[i]; // the terminator too + if (drop == 6) s[0] = L'\\'; + return n - drop; +} + // ⚠️ THE HANDLES A START PLACES ARE INHERITABLE FOR THE LENGTH OF THE START AND NO // LONGER, AND ONE START AT A TIME DOES THIS. // @@ -100,24 +129,35 @@ bool append_utf8(wchar_t* out, okw_uptr cap, okw_uptr& at, const char* s, okw_up return true; } +// ⚠️⚠️ UNTIL 0.7.3 EVERY SEPARATOR WAS DROPPED, AND EVERY ELEMENT WAS QUOTED. +// +// A run of backslashes was counted and never written, so `C:\dir\file' arrived as +// `C:dirfile' and a trailing one ended the quoting early and joined the elements +// after it. And an element that needs no quoting was quoted anyway, which the +// splitting accepts and the command interpreter does not: it reads `"/c"' as a +// command rather than as its switch, and a batch file --- run through the +// interpreter --- received a line it called incorrect. Now an element is written +// as it is unless the splitting would alter it, and otherwise quoted by the rule +// the splitting inverts: backslashes before a quote are doubled and the quote +// escaped, backslashes before the closing quote are doubled, and every other +// backslash is literal. bool append_quoted(wchar_t* out, okw_uptr cap, okw_uptr& at, const wchar_t* s, okw_uptr n) { + bool needs = n == 0; + for (okw_uptr i = 0; i < n && !needs; ++i) + needs = s[i] == L' ' || s[i] == L'\t' || s[i] == L'\n' || s[i] == L'\v' || s[i] == L'"'; + if (!needs) return append_wide(out, cap, at, s, n); + if (!append_wide(out, cap, at, L"\"", 1)) return false; okw_uptr backslashes = 0; for (okw_uptr i = 0; i < n; ++i) { if (s[i] == L'\\') { ++backslashes; continue; } - if (s[i] == L'"') { - for (okw_uptr k = 0; k <= backslashes; ++k) - if (!append_wide(out, cap, at, L"\\", 1)) return false; - backslashes = 0; - } else { - backslashes = 0; - } - // The run of separators preceding this character is emitted with it. - if (at + 1 >= cap) return false; - if (s[i] == L'"') { out[at++] = L'"'; continue; } - out[at++] = s[i]; + const okw_uptr written = s[i] == L'"' ? 2 * backslashes + 1 : backslashes; + for (okw_uptr k = 0; k < written; ++k) + if (!append_wide(out, cap, at, L"\\", 1)) return false; + backslashes = 0; + if (!append_wide(out, cap, at, s + i, 1)) return false; } - for (okw_uptr k = 0; k < backslashes; ++k) + for (okw_uptr k = 0; k < 2 * backslashes; ++k) if (!append_wide(out, cap, at, L"\\", 1)) return false; return append_wide(out, cap, at, L"\"", 1); } @@ -208,6 +248,11 @@ int kal_process_spawn(const kal_spawn* how, // was missing until 0.11 was a caller able to say which. wchar_t cwd[okw::kMaxName]; wchar_t line[kCommandLine]; + // One element, converted before it is quoted into `line'. Its own + // buffer: quoting lengthens an element, so converting it in place just + // beyond what `line' holds had the quoting overwrite what it had not + // yet read. + wchar_t one[kCommandLine]; wchar_t block[kCommandLine]; }; auto* work = static_cast(kal_alloc(sizeof(scratch), alignof(scratch))); @@ -229,6 +274,7 @@ int kal_process_spawn(const kal_spawn* how, image[at++] = relative.buffer[i]; } image[at] = 0; + (void)plain_name(image, at); // The same enquiry the image path comes from, upon the other directory. wchar_t* cwd = work->cwd; @@ -236,6 +282,7 @@ int kal_process_spawn(const kal_spawn* how, FILE_NAME_NORMALIZED | VOLUME_NAME_DOS); if (cn == 0 || cn >= okw::kMaxName - 1) return okw::translate_win32(GetLastError()); cwd[cn] = 0; + (void)plain_name(cwd, cn); // The vector, unaltered, including its first element. wchar_t* line = work->line; @@ -247,8 +294,8 @@ int kal_process_spawn(const kal_spawn* how, // started program split it. Clause 7.6 requires the vector to arrive // unaltered, and the quoting is the inverse of that splitting. okw_uptr produced = 0; - wchar_t* one = line + used + 1; // beyond what is written - const okw_uptr room = kCommandLine - used - 2; + wchar_t* one = work->one; + const okw_uptr room = kCommandLine - 1; if (!append_utf8(one, room, produced, argv[i], argv_lens[i])) return argv_lens[i] >= room ? kal_err_no_space : kal_err_invalid; if (!append_quoted(line, kCommandLine, used, one, produced)) diff --git a/src/win.cpp b/src/win.cpp index 047c836..f2dfae0 100644 --- a/src/win.cpp +++ b/src/win.cpp @@ -112,14 +112,15 @@ wide_name::wide_name(const char* utf8, okw_uptr len) : ok(false) { ok = true; } -okw_uptr narrow(const wchar_t* wide, okw_uptr wide_len, char* out, okw_uptr cap) { +okw_uptr narrow(const wchar_t* wide, okw_uptr wide_len, char* out, okw_uptr cap, bool names) { if (wide_len == 0 || cap == 0) { if (cap) out[0] = 0; return 0; } const int produced = WideCharToMultiByte(CP_UTF8, 0, wide, static_cast(wide_len), out, static_cast(cap - 1), nullptr, nullptr); if (produced <= 0) { out[0] = 0; return 0; } // Reported the way openkal spells it, which is the reverse of the // substitution above and is the only place the difference appears. - for (int i = 0; i < produced; ++i) if (out[i] == '\\') out[i] = '/'; + if (names) + for (int i = 0; i < produced; ++i) if (out[i] == '\\') out[i] = '/'; out[produced] = 0; return static_cast(produced); } diff --git a/src/win.h b/src/win.h index e224f97..d662501 100644 --- a/src/win.h +++ b/src/win.h @@ -277,7 +277,10 @@ struct wide_name { }; // The reverse, for reporting a name this environment produced. -okw_uptr narrow(const wchar_t* wide, okw_uptr wide_len, char* out, okw_uptr cap); +// `names' substitutes openkal's separator for this environment's, which is right +// for a name and wrong for an argument: an argument is not a name, and clause +// 7.6 requires it to arrive as it was given. +okw_uptr narrow(const wchar_t* wide, okw_uptr wide_len, char* out, okw_uptr cap, bool names = true); // A name is a single component or a sequence separated by a forward slash. It // shall not begin with a separator and shall not contain a component that