Files
dingtalk-workspace-cli/internal/helpers/shell_quote_test.go

186 lines
8.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package helpers
import (
"runtime"
"testing"
)
// TestCrossPlatformCoverageShellQuoteArg 表驱动锁定 argv 引用规则。分两类断言:
// - 安全值原样返回(保持恢复命令可读,且在 PowerShell/cmd 下同样可复制);
// - 含任何 shell 元字符的值整体单引号包裹,内嵌单引号按 POSIX 标准写法处理
// (闭合、拼反斜杠转义的单引号、重开)。
//
// 往返正确性另有 Unix 下用真 sh 求值的对照测试(shell_quote_roundtrip_unix_test.go)。
//
// TestCrossPlatformCoverage 前缀是门禁约定:平台覆盖率门禁只跑该前缀(与 TestAllShortcuts)的
// 测试,shell_quote.go 的覆盖全靠这一条,去掉前缀会让 Coverage (macOS)/(Windows) 直接红。
// 详见 drive_latest_incomplete_test.go 头部说明。
func TestCrossPlatformCoverageShellQuoteArg(t *testing.T) {
cases := []struct {
name string
in string
want string
}{
// —— 无需引用:白名单字符 ——
{name: "plain_id", in: "ws-1", want: "ws-1"},
{name: "digits", in: "1234567890", want: "1234567890"},
{name: "upper_lower", in: "abcXYZ", want: "abcXYZ"},
{name: "all_safe_punct", in: "_@+=:,./-", want: "_@+=:,./-"},
{name: "path_like", in: "/tmp/a.b/c-d_e", want: "/tmp/a.b/c-d_e"},
// —— 必须引用:真实业务形态 ——
// workspace 常见形态就是带查询串的 URL:? 与 & 都不在白名单,& 在裸拼接下会拆命令。
{name: "url_with_query", in: "https://alidocs.dingtalk.com/i/nodes/x?spaceId=1&type=doc",
want: `'https://alidocs.dingtalk.com/i/nodes/x?spaceId=1&type=doc'`},
// —— 必须引用:shell 元字符 ——
{name: "space", in: "sp 7", want: `'sp 7'`},
{name: "tab", in: "a\tb", want: "'a\tb'"},
{name: "newline", in: "a\nb", want: "'a\nb'"},
{name: "semicolon", in: "a;id", want: `'a;id'`},
{name: "ampersand", in: "a&b", want: `'a&b'`},
{name: "pipe", in: "a|b", want: `'a|b'`},
{name: "command_substitution", in: "$(id)", want: `'$(id)'`},
{name: "backtick", in: "`id`", want: "'`id`'"},
{name: "variable", in: "$HOME", want: `'$HOME'`},
// % 不在白名单:cmd.exe 会展开 %VAR%,故含 % 的值一律视为需引用。
{name: "windows_variable", in: "%PATH%", want: `'%PATH%'`},
{name: "url_percent_escape", in: "a%20b", want: `'a%20b'`},
{name: "glob", in: "*.txt", want: `'*.txt'`},
{name: "redirect", in: "a>b", want: `'a>b'`},
{name: "backslash", in: `a\b`, want: `'a\b'`},
{name: "double_quote", in: `a"b`, want: `'a"b'`},
{name: "history_expansion", in: "a!b", want: `'a!b'`},
{name: "brace", in: "{a,b}", want: `'{a,b}'`},
{name: "paren", in: "(a)", want: `'(a)'`},
{name: "tilde", in: "~/x", want: `'~/x'`},
{name: "hash", in: "a#b", want: `'a#b'`},
// —— 单引号:唯一无法直接放进单引号串的字符 ——
{name: "single_quote", in: "it's", want: `'it'\''s'`},
{name: "only_single_quote", in: "'", want: `''\'''`},
{name: "two_single_quotes", in: "a'b'c", want: `'a'\''b'\''c'`},
// —— 边界 ——
// 空串必须显式成 '',否则参数会从 argv 里整个消失(--workspace 吞掉下一个 token)。
{name: "empty", in: "", want: `''`},
// 非 ASCII 一律引用:保守优于漏字符,中文目录名很常见。
{name: "cjk", in: "报表", want: `'报表'`},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := ShellQuoteArg(tc.in); got != tc.want {
t.Fatalf("ShellQuoteArg(%q)\n got: %s\nwant: %s", tc.in, got, tc.want)
}
})
}
}
// TestCrossPlatformCoverageDriveLatestScopeValueStrategies 直接测两条平台策略的本体。它们是
// shell_quote.go 里的纯函数、与构建平台无关,因此**在任一平台都能验证 Windows 侧的行为** ——
// 这一点是刻意设计:真 shell 往返测试只能在 Unix 跑,Windows 的安全属性必须由这里钉住,
// 否则「Windows 上不会把不受信任值放进可执行命令」就成了无人复核的断言。
func TestCrossPlatformCoverageDriveLatestScopeValueStrategies(t *testing.T) {
// 这些值在至少一种目标 shell 下会改变命令解析。
hostile := []struct {
name string
value string
}{
{"cmd_command_separator", "sp-7&whoami"}, // cmd.exe:& 分隔命令,单引号不是引号
{"cmd_variable", "%PATH%"}, // cmd.exe:无条件展开
{"workspace_url", "https://x/y?a=1&b=2"}, // 合法 workspace 形态,含 &
{"space", "sp 7"},
{"pwsh_statement_separator", "a;id"}, // PowerShell:; 分隔语句
{"posix_substitution", "$(id)"},
{"backtick", "`id`"},
{"posix_variable", "$HOME"},
{"pipe", "a|b"},
{"redirect", "a>b"},
{"caret", "a^b"}, // cmd.exe 转义符
{"single_quote", "it's"},
{"cjk", "报表"},
{"empty", ""},
}
for _, tc := range hostile {
t.Run("hostile/"+tc.name, func(t *testing.T) {
// POSIX:始终可内联,但必须是引用后的形态(不能等于原值)。
got, ok := driveLatestPosixScopeValue(tc.value)
if !ok {
t.Fatalf("POSIX 策略应始终可内联: %q", tc.value)
}
if got == tc.value {
t.Fatalf("POSIX 策略未引用危险值: %q", tc.value)
}
// Windows:一律拒绝内联 —— 没有对 cmd.exe 与 PowerShell 同时安全的引用形式。
if inline, ok := driveLatestWindowsScopeValue(tc.value); ok {
t.Fatalf("Windows 策略不得内联危险值 %q(得到 %q)", tc.value, inline)
}
})
}
// 安全值:两条策略都原样内联,命令保持可读、跨 shell 可复制。
for _, v := range []string{"ws-1", "sp-7", "folder-root", "modifyTime", "7d", "2026-08-01", "a.b/c-d_e", "a@b"} {
t.Run("bare/"+v, func(t *testing.T) {
if got, ok := driveLatestPosixScopeValue(v); !ok || got != v {
t.Fatalf("POSIX 应原样内联安全值 %q: got %q ok=%v", v, got, ok)
}
if got, ok := driveLatestWindowsScopeValue(v); !ok || got != v {
t.Fatalf("Windows 应原样内联安全值 %q: got %q ok=%v", v, got, ok)
}
})
}
// 空串在 POSIX 下引用成一对空单引号(否则参数会从 argv 消失);Windows 侧归入不可内联。
if got, ok := driveLatestPosixScopeValue(""); !ok || got != "''" {
t.Fatalf("POSIX 空串应引用成一对空单引号: got %q ok=%v", got, ok)
}
}
// TestCrossPlatformCoverageShellBareCharsetHasNoMetacharacters 锁定「安全值可原样内联」的根本
// 前提:白名单里不能含任何一种目标 shell 会特殊解释的字符。
//
// 这比「用某个 shell 跑一遍」更本质 —— 内联路径的安全性不来自引用正确,而来自字符集本身无害;
// 而拒绝内联的路径(Windows 侧)连引用都不需要。三套元字符合并检查:POSIX sh、PowerShell、
// cmd.exe。`%` 曾因 URL 转义(%20)被误列入白名单,而 cmd.exe 会无条件展开 %VAR%,这条测试
// 同时是该缺陷的回归锁。
func TestCrossPlatformCoverageShellBareCharsetHasNoMetacharacters(t *testing.T) {
// 逐字符列出,避免用字符串字面量时漏掉转义细节。
meta := []rune{
'&', '|', '<', '>', ';', '(', ')', '{', '}', '[', ']', // 分隔/分组
'$', '`', '"', '\'', '\\', // 引用与替换
'^', '%', // cmd.exe:转义符与变量展开
'*', '?', '~', '#', '!', // 通配、家目录、注释、历史/取反
' ', '\t', '\n', '\r', // 空白:拆参与换行注入
}
for _, r := range meta {
if !shellNeedsQuote(r) {
t.Fatalf("shell 元字符 %q 落在「无需引用」白名单内,安全值会被原样内联", string(r))
}
}
// 反向哨兵:常规标识符字符必须留在白名单内,否则恢复命令会被无谓地全量引用/降级。
for _, r := range []rune{'a', 'Z', '0', '9', '_', '-', '.', '/', ':', ',', '=', '+', '@'} {
if shellNeedsQuote(r) {
t.Fatalf("常规字符 %q 被判为需引用,会让恢复命令可读性无谓下降", string(r))
}
}
}
// TestCrossPlatformCoverageDriveLatestScopeValueBinding 断言当前构建绑定到了正确的策略。
// 这条测试在每个平台各自成立,把「哪个平台用哪条策略」也纳入 CI(Coverage (Windows) 会跑它)。
func TestCrossPlatformCoverageDriveLatestScopeValueBinding(t *testing.T) {
const hostile = "sp-7&whoami"
inline, ok := driveLatestScopeValue(hostile)
if runtime.GOOS == "windows" {
if ok {
t.Fatalf("windows 构建不得内联 %q(得到 %q)", hostile, inline)
}
return
}
if !ok {
t.Fatalf("posix 构建应可内联 %q", hostile)
}
if inline != `'sp-7&whoami'` {
t.Fatalf("posix 构建应单引号包裹: %s", inline)
}
}