Skip to content

Commit 42d0cee

Browse files
committed
fix: cmd 的转义只加在真正没被保护的那个字符上;示例补 <cstddef>
Windows e2e 两红,Linux examples 一红,两条都是这一轮自己引入的。 ## 一、把引号也一起转义,打断了每一次包获取 上一条提交的做法是:先按子进程的规则引用,再给**每个 cmd 元字符,包括引号本身** 前缀 `^`。理由在纸面上成立 —— 没有裸引号,cmd 就从不进入引用区,于是每个元字符都是 被转义而不是被引用的。 **Windows CI 的读数是:每一次 `install_packages` 都退 1**,包括那两次 JSON 里一个 元字符都没有的(`compat:widget@1.38.1`、`mcpplibs:tpl-demo@1.0.0`)。而这个 job 里 只有那两次调用,也就是**走到这条路的每一次都失败了**。 改成保守的那条规则:引用之后,**按 cmd 看到的引用状态**走一遍(每个 `"` 都翻转它, 因为 cmd 不认 MSVCRT 的 `\"`),只给**落在引用区之外**的元字符加 `^`。引用区里的元 字符本来就是惰性的,而 `^` 在那里是个普通字符。 于是「没有东西要转义」的载荷输出与 `quote_windows` **逐字节相同** —— 那是绝大多数 载荷,也正是被打断的那些。而 `>=2026.3` 里那个 `>` 只多一个 `^`。 判据补了一条:`NothingToEscapeMeansByteIdenticalToPlainQuoting`,直接拿 CI 里失败的 那两个 JSON 当输入。cmd 的模拟器也补上了「`^` 在引用区内是普通字符」这一半 —— 只建 模前一半,它会接受一个 cmd 并不接受的形状。 ## 二、示例:`std::size_t` 要 `#include <cstddef>` `src/cpu/render.cpp` 在 libstdc++ 下编得过、在 libc++ 下编不过: src/cpu/render.cpp:56:52: error: no type named 'size_t' in namespace 'std' 标准头有权带进它需要的其他头,而带进哪些因实现而异。本地默认工具链是 gcc,CI 那一步 用的是 llvm —— **同一台机器上的两个答案**。点名一个类型的翻译单元必须包含声明它的那个 头,不管上一个实现顺手给了什么。 `src/vulkan/render.cpp` 同样补上(它此前靠 `<vulkan/vulkan.h>` 间接得到)。 可迁移的:**本地验示例要用 CI 那一步用的工具链**(`mcpp build --toolchain llvm@…`), 否则验的是另一个标准库。
1 parent 25a51ac commit 42d0cee

4 files changed

Lines changed: 63 additions & 14 deletions

File tree

examples/10-graphics/offscreen/src/cpu/render.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@
77
// `render_device_name`, never the image.
88
#include "render/render.h"
99

10+
// `<cstddef>` FOR `std::size_t`, AND IT IS NOT PEDANTRY.
11+
//
12+
// A standard header is entitled to bring in whichever others it needs, and
13+
// which ones it brings differs between implementations. This file compiled
14+
// against libstdc++ and then failed against libc++ on the same machine:
15+
//
16+
// src/cpu/render.cpp:56:52: error: no type named 'size_t' in namespace 'std'
17+
//
18+
// A translation unit that names a type has to include the header that declares
19+
// it, whatever the last implementation happened to hand it for free.
20+
#include <cstddef>
1021
#include <cmath>
1122

1223
namespace {

examples/10-graphics/offscreen/src/vulkan/render.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
#include <vulkan/vulkan.h>
2121

22+
#include <cstddef>
2223
#include <cstdint>
2324
#include <cstring>
2425
#include <vector>

modules/platform/src/shell.cppm

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,20 @@ std::string quote_posix(std::string_view s);
4141
//
4242
// which arrived as a package-provisioning failure naming a package.
4343
//
44-
// The answer is the standard double escape: quote for the child, then prefix
45-
// every cmd metacharacter -- the quotes included -- with `^`. With no `"` left
46-
// unescaped cmd never enters a quoted region, so every metacharacter is
47-
// escaped rather than quoted, which is the only state in which both rules
48-
// hold. cmd removes the carets and the child sees exactly `quote_windows`.
44+
// The answer escapes exactly what needs it: quote for the child, then walk the
45+
// result tracking the quote state CMD sees -- every `"` toggles it, because cmd
46+
// does not know MSVCRT's `\"` -- and prefix `^` to any metacharacter that falls
47+
// outside a quoted region. Inside one it is already inert, and `^` there is a
48+
// literal character rather than an escape.
49+
//
50+
// ESCAPING EVERY METACHARACTER INCLUDING THE QUOTES WAS TRIED FIRST AND
51+
// BROKE EVERY PAYLOAD. It is defensible on paper -- with no bare `"`, cmd never
52+
// enters a quoted region and each metacharacter is escaped rather than quoted
53+
// -- and Windows CI answered `exit 1` for every package fetch, including the
54+
// ones whose JSON contains no metacharacter at all. So the rule here is the
55+
// conservative one: a payload with nothing to escape comes out byte-identical
56+
// to `quote_windows`, and only the character that is actually unprotected
57+
// acquires a caret.
4958
//
5059
// `%` IS NOT ESCAPED AND CANNOT BE. Variable expansion happens before caret
5160
// processing, and the batch-file escape (`%%`) is not available on a command
@@ -101,15 +110,24 @@ std::string quote_posix(std::string_view s) {
101110
std::string quote_windows_through_cmd(std::string_view s) {
102111
const std::string inner = quote_windows(s);
103112
std::string out;
104-
out.reserve(inner.size() * 2);
113+
out.reserve(inner.size() + 8);
114+
// cmd's quote state, which is toggled by EVERY `"` -- it does not know
115+
// MSVCRT's `\"`. Inside a quoted region a metacharacter is already inert
116+
// and `^` is a literal character, so only the characters that fall OUTSIDE
117+
// one are escaped. That keeps the result byte-identical to `quote_windows`
118+
// for every payload with nothing to escape, which is nearly all of them.
119+
bool inQuotes = false;
105120
for (char c : inner) {
106-
switch (c) {
107-
case '"': case '<': case '>': case '&': case '|':
108-
case '^': case '(': case ')':
109-
out.push_back('^');
110-
break;
111-
default:
112-
break;
121+
if (c == '"') { inQuotes = !inQuotes; out.push_back(c); continue; }
122+
if (!inQuotes) {
123+
switch (c) {
124+
case '<': case '>': case '&': case '|':
125+
case '^': case '(': case ')':
126+
out.push_back('^');
127+
break;
128+
default:
129+
break;
130+
}
113131
}
114132
out.push_back(c);
115133
}

tests/unit/test_windows_command_line.cpp

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,10 @@ CmdParse simulate_cmd_c(std::string_view wrapped) {
160160
bool inQuotes = false;
161161
for (std::size_t i = 0; i < line.size(); ++i) {
162162
char c = line[i];
163-
if (c == '^') { // escapes the next character
163+
// `^` escapes the next character OUTSIDE a quoted region; inside one
164+
// it is an ordinary character. Modelling only the first half would let
165+
// this simulator accept a shape cmd does not.
166+
if (c == '^' && !inQuotes) {
164167
if (i + 1 < line.size()) out.passedOn.push_back(line[++i]);
165168
continue;
166169
}
@@ -238,6 +241,22 @@ TEST(WindowsCommandLine, MetacharacterQuotingSurvivesBothParsers) {
238241
<< "the child received something other than the JSON that was meant";
239242
}
240243

244+
TEST(WindowsCommandLine, NothingToEscapeMeansByteIdenticalToPlainQuoting) {
245+
// The conservative half of the rule, and it is the half that was measured
246+
// the hard way: an earlier version escaped every metacharacter INCLUDING
247+
// the quotes, which is defensible on paper and broke every package fetch on
248+
// Windows -- including the ones whose JSON contains no metacharacter. A
249+
// payload with nothing to escape must come out exactly as before.
250+
for (std::string_view plain : {
251+
R"({"targets":["mcpplibs:tpl-demo@1.0.0"],"yes":true})",
252+
R"({"targets":["compat:widget@1.38.1"],"yes":true})",
253+
R"(a plain path C:\Program Files\x)" }) {
254+
EXPECT_EQ(mcpp::platform::shell::quote_windows_through_cmd(plain),
255+
mcpp::platform::shell::quote_windows(plain))
256+
<< "a payload with no metacharacter acquired an escape: " << plain;
257+
}
258+
}
259+
241260
TEST(WindowsCommandLine, MetacharacterQuotingIsUnchangedForPlainText) {
242261
// A payload with nothing to escape must not acquire carets, so the common
243262
// case stays legible in a log.

0 commit comments

Comments
 (0)