沙箱策略生成和命令安全解析辅助工具
这一阶段是幕后安全岗,守在外部命令运行前。它先把 bash、PowerShell 命令拆成程序和参数,拒绝变量替换、重定向等容易藏风险的写法,再整理成统一格式,方便和已批准命令比对。参数模板、选项和类型标签负责检查输入是否安全;安全分类器区分只读命令和删除、启动程序等危险动作。最后,沙箱工具合成真正的隔离规则,限制文件读写和联网。
旧版策略参数模型
这些文件定义旧版执行策略语言,用于命令参数、选项和经过严格验证的值类型。
execpolicy-legacy/src/arg_matcher.rs源码 ↗
这个文件像一张“参数检查清单”。系统要判断外部命令能不能安全执行时,不能只看命令名,还要看它后面的参数是什么意思:比如这是要读的文件、要写的文件,还是像 head -n 10 里的正整数。ArgMatcher 枚举就是这些参数模板的集合。每种模板还知道自己应该吃掉几个参数:有的必须正好一个,有的至少一个,有的可以没有也可以很多。文件里还把这些模板接到 Starlark 上,Starlark 是一种嵌入式脚本语言,这样策略脚本里既可以直接写字符串,也可以写专门的匹配器对象。这样做的好处是,策略作者写起来简单,执行检查时又能保留足够的信息,知道哪些参数涉及文件、哪些不能随便解释。
ArgMatcher::cardinality51–64 ↗
fn cardinality(&self) -> ArgMatcherCardinality
作用:告诉系统这个参数模板一次应该匹配几个命令行参数。比如普通字符串只匹配一个参数,而“一组可读文件”可以匹配一个或多个。
数据流:进去的是一个 ArgMatcher 参数模板 → 它按模板种类判断数量规则 → 出来的是 ArgMatcherCardinality,也就是“一定一个”“至少一个”或“零个到多个”。它不改动任何数据。
调用关系:这是后续参数匹配流程的基础工具。别的代码拿到一个 ArgMatcher 后,会先问它需要消耗几个实际参数,再继续检查这些参数的含义。它自己不把工作交给别的项目函数。
ArgMatcher::arg_type66–78 ↗
fn arg_type(&self) -> ArgType
作用:把“参数模板”转换成更直接的“参数类型”。也就是从规则描述,变成后面安全判断更容易使用的分类。
数据流:进去的是一个 ArgMatcher → 它查看具体种类,比如字面量、可读文件、可写文件、正整数等 → 出来的是对应的 ArgType。如果是字面量,会复制里面的字符串并生成 ArgType::Literal;如果是不验证的可变参数,就变成未知类型。
调用关系:它位于策略描述和实际安全含义之间。参数匹配代码可以用 cardinality 决定数量,再用这个函数知道每个参数代表什么风险。这里会调用外部的 Literal 构造方式来生成字面量类型。
调用图:外部调用 1 个(Literal)。
ArgMatcherCardinality::is_exact88–94 ↗
fn is_exact(&self) -> Option<usize>
作用:判断某个数量规则是不是“精确数量”。目前只有“正好一个”能给出明确数字 1。
数据流:进去的是一个数量规则 ArgMatcherCardinality → 它检查是不是 One → 如果是,出来 Some(1);如果是“至少一个”或“零个到多个”,出来 None,表示无法提前给出固定数量。
调用关系:它给需要快速判断固定长度的代码使用。比如某段流程只关心“这个模板是不是一定吃一个参数”,就可以问这个函数,而不用自己重复匹配枚举。
ArgMatcher::alloc_value98–100 ↗
fn alloc_value(self, heap: Heap<'v>) -> Value<'v>
作用:把 ArgMatcher 放进 Starlark 的内存堆里,让策略脚本运行时能把它当成一个脚本里的值来使用。
数据流:进去的是一个 Rust 里的 ArgMatcher 和一个 Starlark 的 Heap(可以理解成脚本运行时专用的内存区域)→ 它调用 alloc_simple 把这个对象放进去 → 出来的是 Starlark 能识别的 Value。原来的匹配器被移动进脚本内存中。
调用关系:这是 Rust 代码和 Starlark 脚本之间的桥。策略加载或执行时,如果要把匹配器暴露给脚本环境,就会走到这里;具体的内存放置工作交给外部的 alloc_simple。
调用图:外部调用 1 个(alloc_simple)。
ArgMatcher::unpack_value_impl111–117 ↗
fn unpack_value_impl(value: Value<'v>) -> starlark::Result<Option<Self>>
作用:把 Starlark 脚本里的值转回 Rust 的 ArgMatcher。这样策略作者可以在脚本里直接写普通字符串,系统会自动把它当成“匹配这个固定字符串”的规则。
数据流:进去的是一个 Starlark 的 Value → 它先看这个值是不是字符串;如果是,就把字符串变成 ArgMatcher::Literal → 如果不是字符串,再看它本身是不是已经是 ArgMatcher 对象,是就复制出来 → 如果两者都不是,就返回 None,表示这个值不能当参数匹配器用。
调用关系:这是读取策略脚本参数时的入口之一。它让脚本写法更自然:普通字符串不需要手动包装。构造字面量匹配器时会用到外部的 Literal 构造方式。
调用图:外部调用 1 个(Literal)。
execpolicy-legacy/src/opt.rs源码 ↗
这个文件像是在给命令行参数做一张“登记表”。一个选项不只是一个字符串,它还要说明:用户在命令行上怎么写它、它是不是必须出现、它需不需要再跟一个值。Opt 就是这张登记表本身,OptMeta 则说明选项的类型:要么是 Flag,也就是开关型选项,比如“有或没有”;要么是 Value,也就是后面还要带一个具体值,并且这个值要符合某种参数类型。文件里还接上了 Starlark(一种嵌入式脚本语言,可以让配置或规则用脚本表达)的值系统。这样脚本里创建或传入的选项,Rust 代码能认出来;Rust 代码里的选项,也能放进脚本运行时的内存里。没有这些定义,程序就很难统一判断一个命令行选项到底长什么样、能不能从脚本传入、又该怎么存放。
Opt::new40–46 ↗
fn new(opt: String, meta: OptMeta, required: bool) -> Self
作用:创建一个新的命令行选项记录。调用者把选项名字、选项类型、是否必填交给它,它返回一个完整的 Opt。
数据流:进去的是三个信息:命令行上的写法,比如 --foo;选项的元信息,比如它是开关还是要带值;以及它是否必须出现。函数只是把这三样原样装进一个新的 Opt 结构里。出来的是一个可以被后续代码保存、检查或传给脚本系统的选项对象。
调用关系:它是这个文件里最直接的构造入口。其他代码需要描述一个命令行选项时,会用它把零散信息打包成统一格式;它自己不把工作转交给别的函数。
Opt::name48–50 ↗
fn name(&self) -> &str
作用:取出这个选项在命令行上的名字。比如一个 Opt 代表 --help,这个函数就返回 --help。
数据流:进去的是一个已经存在的 Opt。函数读取里面的 opt 字段,不复制整个字符串,只借给调用者看一眼。出来的是这个选项名字的文本引用,原来的 Opt 不会被改动。
调用关系:当别的代码只关心“这个选项叫什么”时会用它,比如做显示、匹配或检查。它是一个很轻的读取工具,不调用其他函数,也不改变流程。
Opt::unpack_value_impl61–65 ↗
fn unpack_value_impl(value: Value<'v>) -> starlark::Result<Option<Self>>
作用:把 Starlark 运行时里的一个通用值,尝试取成 Rust 里的 Opt。简单说,就是看看脚本传来的东西是不是一个命令行选项。
数据流:进去的是一个 Starlark 的 Value,它可能装着任何脚本值。函数尝试把它当作 Opt 来识别;如果确实是,就克隆出一份 Opt 返回;如果不是,就返回“没有取到”。结果被包在 Starlark 的错误处理格式里,所以如果运行时需要报错,也能沿用同一套机制。
调用关系:它是 Rust 和 Starlark 之间的“翻译口”。当某段 Rust 代码声明自己想从脚本参数里拿到一个 Opt 时,Starlark 的转换机制会走到这里。它不继续调用本文件里的其他函数。
Opt::alloc_value69–71 ↗
fn alloc_value(self, heap: Heap<'v>) -> Value<'v>
作用:把 Rust 里的 Opt 放进 Starlark 的内存堆里,让脚本运行时可以把它当成一个脚本值使用。
数据流:进去的是一个 Rust 的 Opt 和 Starlark 的 Heap,这里的 Heap 可以理解成脚本运行时专门存放对象的内存区域。函数把这个 Opt 交给 heap.alloc_simple 放进去。出来的是一个 Starlark 的 Value,也就是脚本世界能拿着用的值。
调用关系:它负责从 Rust 世界走向 Starlark 世界。某段代码需要把命令行选项暴露给脚本时会用到它;它把真正的存放动作交给外部的 alloc_simple,也就是 Starlark 堆的基础分配工具。
调用图:外部调用 1 个(alloc_simple)。
execpolicy-legacy/src/arg_type.rs源码 ↗
这个文件解决的是一个很实际的问题:同样是命令行里的一个字符串,它可能是文件名,也可能只是普通选项,还可能是 sed 的小脚本。如果不区分清楚,系统就不知道这个命令会不会读文件、写文件,也就很难安全地限制它。这里的 ArgType 就像给每个参数贴标签:有的必须等于某个固定字面值,有的必须是非空文件名,有的必须是大于 0 的整数,有的要按安全的 sed 命令来检查。validate 会拿真实传进来的字符串和标签要求对照,不合格就返回明确错误。might_write_file 则回答一个关键问题:这个参数有没有可能代表会被写入的文件。文件末尾还把 ArgType 暴露给 Starlark(一种嵌入式脚本语言,用来写配置或规则),让脚本规则也能使用这些参数类型。
ArgType::validate32–70 ↗
fn validate(&self, value: &str) -> Result<()>
作用:检查一个实际参数值是否符合它被声明的类型。有人创建或加载一条命令规则时,会用它提前发现明显不安全或不合法的参数,比如空文件名、不是正整数的数字、或者不安全的 sed 命令。
数据流:进去的是一个 ArgType 标签和一个字符串参数值。函数会根据标签做不同检查:固定文字必须完全一样;可读或可写文件名不能为空;正整数必须能解析成大于 0 的数字;普通未知内容直接放过;SedCommand 会交给 parse_sed_command 做专门检查。出来的是成功结果,或者一个说明原因的错误;它不修改外部数据。
调用关系:它会被名为 new 的构造流程调用,用来在新建规则或对象时把参数先验一遍。遇到 SedCommand 这种更复杂的类型时,它不会自己硬解析,而是把工作交给 parse_sed_command,因为 sed 命令有专门的安全判断规则。
调用图:调用 1 个内部函数(parse_sed_command);被 2 处调用(new, new)。
ArgType::might_write_file72–81 ↗
fn might_write_file(&self) -> bool
作用:判断这种参数类型有没有可能指向一个会被写入的文件。这个判断对安全策略很重要,因为写文件通常比只读文件风险更高。
数据流:进去的是一个 ArgType 标签。函数只看标签本身:如果明确是可写文件,或者类型未知,就返回 true,表示要当成可能写文件来谨慎处理;如果是固定文字、普通非文件内容、正整数、可读文件或安全 sed 命令,就返回 false。它不读取也不改动其他数据。
调用关系:它是给策略判断用的小问答工具:当系统需要知道某个参数会不会带来写文件风险时,可以直接问它。调用图里没有显示它再调用别的函数,它的判断完全来自 ArgType 自己的分类。
execpolicy-legacy/src/sed_command.rs源码 ↗
sed 是一个常见的文本处理工具,可以按命令改写或打印文本。但 sed 命令能力很强,如果随便接受用户输入,就可能做出超出预期的事。这个文件的作用就像门口的安检员:不是去真正执行 sed,而是先看命令长得是不是安全。它现在只认可一种格式:前面是起始行号,中间一个逗号,后面是结束行号,最后以字母 p 结尾,意思是“打印这段行”。比如 122,202p 可以通过。只要格式不完全匹配,或者行号不是数字,就返回错误 SedCommandNotProvablySafe,意思是“这个命令不能证明安全”。这个设计偏保守:不确定就拒绝,避免把复杂 sed 语法带来的风险放进系统。
parse_sed_command4–17 ↗
fn parse_sed_command(sed_command: &str) -> Result<()>
作用:检查一个 sed 命令字符串是否属于系统认可的安全格式。目前它只接受“起始行号,结束行号p”这种打印行范围的命令,例如 122,202p。
数据流:进去的是一段文本形式的 sed 命令。函数先看它是不是以 p 结尾,再把前面的内容按逗号分成两段,然后确认两段都能当作正整数行号来理解;这些条件都满足,就返回成功,不改动任何东西。只要有一步不满足,就把原命令放进错误里返回,告诉调用者这个命令不能证明安全。
调用关系:它会被 validate 调用,通常是在系统正式接受或使用某个配置、规则、命令之前做安全检查。它自己不再把工作交给项目里的其他函数,只使用字符串切分和数字解析这些基础操作;检查结果会帮助上层决定是否继续。
调用图:被 1 处调用(validate)。
沙盒策略构建
这些文件将较高层级的沙盒权限转换为具体的平台特定隔离产物和构建期接线。
linux-sandbox/build.rs源码 ↗
这个文件在程序真正运行前、编译项目时生效。Cargo 是 Rust 的构建工具,它会先运行 build.rs,看看有没有额外的构建规则。这里唯一做的事,就是打印一条给 Cargo 看的指令:请关注 CODEX_BWRAP_SHA256 这个环境变量。环境变量可以理解成“构建时外部传进来的设置”。如果这个值变了,Cargo 就知道之前的构建结果可能不再可信,需要重新跑相关构建步骤。它像是在门口贴了一张提醒纸:这个外部参数一改,屋里的东西就要重新检查。
main1–3 ↗
fn main()
作用:这是构建脚本的入口。它告诉 Cargo 监听 CODEX_BWRAP_SHA256 这个环境变量的变化,以便必要时自动重新构建。
数据流:进去的是构建时的环境信息,尤其是 CODEX_BWRAP_SHA256 这个变量可能存在也可能不存在;函数本身不读取变量值,只是向标准输出打印一条 Cargo 能识别的指令;出来的结果是 Cargo 收到这条指令,并把这个环境变量加入“变化后要重跑”的检查名单。
调用关系:Cargo 在编译这个包之前会调用这个 main。main 不把工作交给项目里的其他函数,只调用 println! 打印一行特殊格式的文本;Cargo 看到这行文本后,就把它当作构建规则使用。
调用图:外部调用 1 个(println!)。
sandboxing/src/policy_transforms.rs源码 ↗
可以把这个文件想成沙箱权限的“调度台”。用户、配置、客户端都可能给出一些额外权限,比如允许联网、允许读写某个目录、禁止读取某些匹配模式的文件。这里先把这些权限清理干净:路径尽量变成标准绝对路径,重复项去掉,不合法的通配符规则直接拒绝。然后它会做三类核心计算:合并权限,把两份许可叠在一起;求交集,只保留“请求过并且被批准”的部分;生成最终沙箱策略,把基础规则和额外规则拼成运行时真正使用的文件系统、网络权限。文件里还特别处理了当前工作目录、临时目录、项目根目录、通配符拒绝规则等容易出错的情况,避免相对路径在不同位置下含义变掉。
normalize_additional_permissions19–69 ↗
fn normalize_additional_permissions(
additional_permissions: AdditionalPermissionProfile,
) -> Result<AdditionalPermissionProfile, String>
作用:把一份额外权限清单整理成更安全、更统一的样子。别人传来的路径可能是相对的、重复的,或者包含不被允许的通配符规则,这个函数负责先把它们规整好。
数据流:进去的是一份额外权限配置。它会丢掉空的网络权限,把文件路径尽量标准化成绝对路径,删除重复的文件权限项,并检查通配符权限只能用于“拒绝读取”。出来的是整理后的权限;如果发现不合法的通配符授权,就返回错误文字。
调用关系:它会在写入路径权限、校验额外权限、处理调用请求时被用到。它自己会调用路径标准化工具 canonicalize_preserving_symlinks,并用向量预留空间来收集整理后的条目。
调用图:被 3 处调用(write_permissions_for_paths, normalize_and_validate_additional_permissions, handle_call);外部调用 3 个(with_capacity, canonicalize_preserving_symlinks, matches!)。
merge_permission_profiles71–123 ↗
fn merge_permission_profiles(
base: Option<&AdditionalPermissionProfile>,
permissions: Option<&AdditionalPermissionProfile>,
) -> Option<AdditionalPermissionProfile>
作用:把两份额外权限合成一份。比如系统已有一份基础额外权限,用户这次又申请了一份,就需要用它来算出总结果。
数据流:进去的是 base 和 permissions 两份可选权限。它会把网络权限按“只要有一边允许联网,就算允许”来合并;文件权限则把两边的条目去重拼起来,并合并通配符扫描深度。出来的是合并后的权限;如果合完什么权限都没有,就返回空。
调用关系:它在记录已批准权限、应用本轮批准权限、计算补丁权限等场景中被调用。它把具体的文件条目合并交给 merge_permission_entries,把通配符扫描深度交给 merge_glob_scan_max_depth。
调用图:调用 2 个内部函数(merge_glob_scan_max_depth, merge_permission_entries);被 5 处调用(record_granted_permissions, record_granted_permissions, apply_granted_turn_permissions, effective_patch_permissions, relative_deny_glob_grants_remain_preapproved_after_materialization)。
intersect_permission_profiles125–195 ↗
fn intersect_permission_profiles(
requested: AdditionalPermissionProfile,
granted: AdditionalPermissionProfile,
cwd: &Path,
) -> AdditionalPermissionProfile
作用:算出“请求的权限”和“实际批准的权限”的交集。也就是说,只有既被请求、又被批准的部分才会留下。
数据流:进去的是 requested、granted 两份权限和当前工作目录 cwd。它会先把请求的文件权限变成可判断的沙箱策略,再逐条检查批准项是否落在请求范围内,同时保留仍然有约束作用的拒绝规则;网络权限则必须两边都明确允许才保留。出来的是最终可接受的额外权限。
调用关系:它会在客户端返回权限申请结果、规范化权限响应、判断权限是否已预批准时被调用。它内部串起路径解析、授权覆盖判断、拒绝规则保留和当前目录相关路径固化这些步骤。
调用图:被 4 处调用(request_permissions_response_from_client_result, normalize_request_permissions_response, permissions_are_preapproved, relative_deny_glob_grants_remain_preapproved_after_materialization)。
merge_glob_scan_max_depth197–215 ↗
fn merge_glob_scan_max_depth(
left_entries: &[FileSystemSandboxEntry],
left_depth: Option<usize>,
right_entries: &[FileSystemSandboxEntry],
right_depth: Option<usize>,
) -> Option<usiz
作用:合并两边的“通配符扫描最大深度”。通配符就是像 *.log 这类匹配很多文件的模式,扫描深度限制是为了避免无限制地到处找文件。
数据流:进去的是左右两组权限条目和各自的深度设置。它先判断两边是否真的有通配符拒绝规则,再决定结果:如果任一边是无限扫描,结果就是无限;如果两边都有上限,取更大的那个;如果只有一边有上限,就用那一边。出来的是合并后的深度,或者空表示不限制或不需要。
调用关系:它被合并权限资料和合并文件系统策略时使用。它把“这一边是否需要扫描、扫描是有限还是无限”的判断交给 effective_glob_scan_depth。
调用图:调用 1 个内部函数(effective_glob_scan_depth);被 2 处调用(merge_file_system_policy_with_additional_permissions, merge_permission_profiles)。
effective_glob_scan_depth217–231 ↗
fn effective_glob_scan_depth(
entries: &[FileSystemSandboxEntry],
depth: Option<usize>,
) -> Option<GlobScanDepth>
作用:判断一组权限里,通配符拒绝规则实际需要怎样的扫描深度。没有通配符拒绝规则时,就不需要扫描深度。
数据流:进去的是文件权限条目和可选的深度数字。它会查找是否存在“拒绝读取”的通配符规则;如果存在且给了深度,就返回有上限的深度;如果存在但没给深度,就表示无限扫描;如果不存在,就返回空。
调用关系:它只服务于 merge_glob_scan_max_depth,是合并扫描深度前的判断小助手。
调用图:被 1 处调用(merge_glob_scan_max_depth);外部调用 2 个(iter, Bounded)。
granted_file_system_entry_within_request239–265 ↗
fn granted_file_system_entry_within_request(
requested: &FileSystemPermissions,
requested_policy: &FileSystemSandboxPolicy,
requested_read_deny_matcher: Option<&ReadDenyMatcher>,
grant
作用:判断某一条已经批准的文件权限,是否真的落在用户请求过的范围里。它防止批准结果偷偷扩大到请求之外。
数据流:进去的是请求的文件权限、请求对应的沙箱策略、可选的读取拒绝匹配器、一条批准权限和当前工作目录。它先排除不能读取的批准项,再解析路径;如果路径被请求里的拒绝规则挡住,就拒绝;否则检查请求权限是否覆盖批准权限。出来的是 true 或 false。
调用关系:它在求权限交集时用来过滤批准条目。它会调用 resolve_permission_path 把特殊路径变成实际路径,调用沙箱策略的 resolve_access_with_cwd 查询请求侧允许什么,再用 access_covers 做最终比较。
调用图:调用 3 个内部函数(resolve_access_with_cwd, access_covers, resolve_permission_path)。
retain_constraining_deny_entries267–288 ↗
fn retain_constraining_deny_entries(
source_entries: &[FileSystemSandboxEntry],
accepted_entries: &[FileSystemSandboxEntry],
cwd: &Path,
output_entries: &mut Vec<FileSystemSandboxEntry
作用:在已经接受了一些允许访问的权限后,保留仍然会限制这些访问的“拒绝”规则。这样不会因为合并权限而把重要的禁止规则弄丢。
数据流:进去的是一组来源权限、已接受的授权项、当前工作目录,以及一个输出列表。它逐条看来源里的拒绝规则,判断它是否会影响已接受的授权;会影响的就把当前目录相关路径固定下来,并加入输出列表且避免重复。出来的是被保留下来的拒绝规则列表,同时输出列表也被改动。
调用关系:它是 intersect_permission_profiles 中保留安全边界的一步。它把“这条拒绝规则是否有约束作用”的判断交给 deny_entry_constrains_accepted_grant,把路径固定交给 materialize_cwd_dependent_entry。
调用图:调用 2 个内部函数(deny_entry_constrains_accepted_grant, materialize_cwd_dependent_entry);外部调用 2 个(new, iter)。
deny_entry_constrains_accepted_grant290–312 ↗
fn deny_entry_constrains_accepted_grant(
deny_entry: &FileSystemSandboxEntry,
accepted_entries: &[FileSystemSandboxEntry],
cwd: &Path,
) -> bool
作用:判断一条拒绝规则是否会影响某条已经接受的允许访问规则。简单说,就是看“禁止区域”和“允许区域”有没有碰到一起。
数据流:进去的是一条拒绝规则、已接受的授权列表和当前工作目录。它只看可读取的授权项,把授权路径解析出来;如果拒绝规则是通配符,就取它前面固定不变的路径部分来比较;如果是普通路径或特殊路径,就解析成实际路径后比较。出来的是是否需要保留这条拒绝规则。
调用关系:它被 retain_constraining_deny_entries 调用,用来决定哪些 deny 条目不能丢。它会依赖路径重叠判断和通配符前缀推断这些小逻辑。
调用图:被 1 处调用(retain_constraining_deny_entries);外部调用 1 个(iter)。
glob_static_prefix_path314–333 ↗
fn glob_static_prefix_path(pattern: &str, cwd: &Path) -> Option<AbsolutePathBuf>
作用:从一个通配符路径里提取“不会变化的前半段”。例如 /a/b/*.txt 的固定前缀大致是 /a/b。
数据流:进去的是通配符字符串和当前工作目录。它先把模式按当前工作目录解析成绝对形式,再找第一个通配符字符,比如 *、?、[、];然后截出前面的目录部分,并转成绝对路径。出来的是固定前缀路径;如果模式一开始就是通配符或无法确定,就返回空。
调用关系:它用于判断通配符拒绝规则是否会约束某个已批准访问范围。它会调用路径解析和绝对路径检查工具,帮助上层做安全判断。
调用图:调用 2 个内部函数(from_absolute_path, resolve_path_against_base);外部调用 1 个(new)。
paths_overlap335–337 ↗
fn paths_overlap(left: &Path, right: &Path) -> bool
作用:判断两个路径范围是否有重叠。这里的重叠指一个路径是不是另一个路径的里面或上层。
数据流:进去的是两个路径。它检查 left 是否以 right 开头,或者 right 是否以 left 开头。出来的是布尔值,表示这两个路径区域是否可能互相影响。
调用关系:它是权限交集和拒绝规则保留过程里的基础判断工具。上层用它来判断允许访问的目录和拒绝访问的目录是否撞在一起。
调用图:外部调用 1 个(starts_with)。
access_covers339–345 ↗
fn access_covers(requested: FileSystemAccessMode, granted: FileSystemAccessMode) -> bool
作用:判断请求里的权限是否足够覆盖批准里的权限。比如批准读权限时,请求侧至少也要允许读。
数据流:进去的是 requested 和 granted 两个访问模式。它按批准项的类型检查:批准读就看请求是否可读,批准写就看请求是否可写,批准拒绝永远不算被覆盖。出来的是 true 或 false。
调用关系:它被 granted_file_system_entry_within_request 调用,是判断批准条目是否越界的最后一道简单比较。它会使用访问模式自带的 can_read 和 can_write 判断能力。
调用图:调用 2 个内部函数(can_read, can_write);被 1 处调用(granted_file_system_entry_within_request)。
materialize_cwd_dependent_entry347–370 ↗
fn materialize_cwd_dependent_entry(
entry: &FileSystemSandboxEntry,
cwd: &Path,
) -> FileSystemSandboxEntry
作用:把依赖当前工作目录的权限项固定成当下的具体路径。这样以后工作目录变了,也不会改变这条权限的含义。
数据流:进去的是一条文件权限和当前工作目录。项目根目录这类特殊路径会尽量解析成普通绝对路径;通配符模式会按当前工作目录变成绝对模式;已经是普通路径或其他特殊路径的就保持原样。出来的是固化后的权限条目。
调用关系:它被 retain_constraining_deny_entries 用来保存拒绝规则时调用。它会调用 resolve_permission_path 处理特殊路径,也会调用路径解析工具处理相对通配符。
调用图:调用 2 个内部函数(resolve_permission_path, resolve_path_against_base);被 1 处调用(retain_constraining_deny_entries);外部调用 1 个(clone)。
resolve_permission_path372–404 ↗
fn resolve_permission_path(path: &FileSystemPath, cwd: &Path) -> Option<AbsolutePathBuf>
作用:把权限里写的各种路径说法,尽量翻译成一个真实的绝对路径。比如“项目根目录”“系统临时目录”“根目录”这些都需要在运行时才能确定。
数据流:进去的是一个文件系统路径描述和当前工作目录。普通绝对路径直接返回;通配符不解析成单一路径;特殊路径会按类型解析,比如 Root 取文件系统根,ProjectRoots 基于当前目录,Tmpdir 读取 TMPDIR 环境变量,SlashTmp 检查 /tmp 是否存在。出来的是绝对路径,解析不了就返回空。
调用关系:它被判断批准是否在请求内、固化当前目录相关权限等流程调用。它承担了把抽象路径变成现实路径的翻译工作。
调用图:调用 2 个内部函数(from_absolute_path, resolve_path_against_base);被 2 处调用(granted_file_system_entry_within_request, materialize_cwd_dependent_entry);外部调用 5 个(ancestors, as_path, from, clone, var_os)。
merge_permission_entries406–417 ↗
fn merge_permission_entries(
base: &[FileSystemSandboxEntry],
permissions: &[FileSystemSandboxEntry],
) -> Vec<FileSystemSandboxEntry>
作用:把两组文件权限条目拼成一组,并去掉重复项。它解决的是“同一条权限被写两次”这种杂乱问题。
数据流:进去的是 base 和 permissions 两个权限条目列表。它按顺序遍历两边,把还没出现过的条目克隆进新列表。出来的是去重后的合并列表。
调用关系:它被 merge_permission_profiles 调用,专门处理文件系统权限条目的合并。更高层不需要关心去重细节。
调用图:被 1 处调用(merge_permission_profiles);外部调用 3 个(with_capacity, iter, len)。
merge_file_system_policy_with_additional_permissions419–443 ↗
fn merge_file_system_policy_with_additional_permissions(
file_system_policy: &FileSystemSandboxPolicy,
additional_permissions: &FileSystemPermissions,
) -> FileSystemSandboxPolicy
作用:把基础文件系统沙箱策略和额外文件权限合在一起。只有受限制的沙箱才会真正追加额外条目。
数据流:进去的是原本的文件系统沙箱策略和额外文件权限。如果原策略是 Restricted,也就是受限制模式,它会复制原策略,追加不重复的额外条目,并合并通配符扫描深度;如果原策略已经是不限制或外部沙箱,就直接返回原策略副本。出来的是合并后的文件系统策略。
调用关系:它被 effective_file_system_sandbox_policy 调用。它内部继续使用 merge_glob_scan_max_depth 来保持通配符拒绝规则的扫描设置一致。
调用图:调用 1 个内部函数(merge_glob_scan_max_depth);被 1 处调用(effective_file_system_sandbox_policy);外部调用 1 个(clone)。
effective_file_system_sandbox_policy445–464 ↗
fn effective_file_system_sandbox_policy(
file_system_policy: &FileSystemSandboxPolicy,
additional_permissions: Option<&AdditionalPermissionProfile>,
) -> FileSystemSandboxPolicy
作用:算出最终真正要用的文件系统沙箱策略。它把基础策略和可选的额外权限结合起来。
数据流:进去的是基础文件系统策略和可选额外权限。如果没有额外权限、没有文件系统权限,或文件系统权限为空,就直接复制基础策略;否则把它们合并。出来的是运行时实际使用的文件系统策略。
调用关系:它会在创建文件系统沙箱上下文、计算补丁权限、构造最终权限资料时被调用。真正的合并动作交给 merge_file_system_policy_with_additional_permissions。
调用图:调用 1 个内部函数(merge_file_system_policy_with_additional_permissions);被 5 处调用(file_system_sandbox_context, effective_patch_permissions, file_system_sandbox_context_uses_active_attempt, file_system_sandboxed_write_allows_additional_write_root, effective_permission_profile);外部调用 1 个(clone)。
merge_network_access466–476 ↗
fn merge_network_access(
base_network_access: bool,
additional_permissions: &AdditionalPermissionProfile,
) -> bool
作用:判断合并额外权限后,网络是否应该被允许。它的规则很简单:原本允许,或者额外权限明确允许,就算允许。
数据流:进去的是基础网络是否可用,以及一份额外权限。它读取额外权限里的 network.enabled,如果基础已经允许或额外权限明确为 true,就返回 true;否则返回 false。
调用关系:它是 effective_network_sandbox_policy 的内部小助手,用来把“基础网络状态”和“额外网络授权”合成一个布尔判断。
effective_network_sandbox_policy478–491 ↗
fn effective_network_sandbox_policy(
network_policy: NetworkSandboxPolicy,
additional_permissions: Option<&AdditionalPermissionProfile>,
) -> NetworkSandboxPolicy
作用:算出最终网络沙箱策略。它决定运行时网络是开放、受限,还是保持原样。
数据流:进去的是基础网络策略和可选额外权限。如果合并后网络应当可用,就返回 Enabled;如果传入了额外权限但没有允许网络,就返回 Restricted;如果没有额外权限,就保持原策略。出来的是最终网络策略。
调用关系:它会在构建文件系统沙箱上下文、计算最终权限资料等场景中被调用。它依赖 merge_network_access 来判断额外权限是否打开了网络。
调用图:被 4 处调用(file_system_sandbox_context, file_system_sandbox_context_uses_active_attempt, file_system_sandboxed_write_allows_additional_write_root, effective_permission_profile)。
effective_permission_profile493–507 ↗
fn effective_permission_profile(
permission_profile: &PermissionProfile,
additional_permissions: Option<&AdditionalPermissionProfile>,
) -> PermissionProfile
作用:把一个完整权限配置转换成“加上额外权限后的最终权限配置”。这是文件系统和网络两部分最终计算的总入口之一。
数据流:进去的是原始 PermissionProfile 和可选额外权限。它先把原始配置拆成运行时文件系统策略和网络策略,分别计算最终文件系统策略、最终网络策略,然后再按原来的执行强制方式组装回 PermissionProfile。出来的是新的完整权限配置。
调用关系:它会在为一次尝试创建沙箱上下文、处理预批准额外权限、执行 transform 流程时被调用。它把文件系统部分交给 effective_file_system_sandbox_policy,把网络部分交给 effective_network_sandbox_policy。
调用图:调用 5 个内部函数(enforcement, from_runtime_permissions_with_enforcement, to_runtime_permissions, effective_file_system_sandbox_policy, effective_network_sandbox_policy);被 3 处调用(file_system_sandbox_context_for_attempt, preapproved_additional_permissions_escalate_intercepted_exec, transform)。
should_require_platform_sandbox509–529 ↗
fn should_require_platform_sandbox(
file_system_policy: &FileSystemSandboxPolicy,
network_policy: NetworkSandboxPolicy,
has_managed_network_requirements: bool,
) -> bool
作用:判断这次运行是否必须启用平台级沙箱。平台级沙箱可以理解成操作系统层面的隔离保护,防止进程越过权限边界。
数据流:进去的是文件系统策略、网络策略,以及是否有托管网络要求。若有托管网络要求,直接要求沙箱;如果网络没有开放,一般也要沙箱,除非文件系统已经交给外部沙箱;如果网络开放,则只有在文件系统受限且没有全盘写入权限时才要求沙箱。出来的是是否需要平台沙箱。
调用关系:它会在生成权限配置标签、决定是否提示 bwrap 系统沙箱问题、选择初始运行方式时被调用。它用网络策略的 is_enabled 和文件系统策略的 has_full_disk_write_access 来做关键判断。
调用图:调用 2 个内部函数(has_full_disk_write_access, is_enabled);被 3 处调用(permission_profile_sandbox_tag, should_warn_about_system_bwrap, select_initial);外部调用 1 个(matches!)。
sandboxing/src/seatbelt.rs源码 ↗
macOS 有一套叫 Seatbelt 的沙盒机制,可以用规则文件告诉系统:这个程序能碰哪些文件、能不能联网、能不能连本机代理。这个文件做的事,就是把项目里更抽象的权限设置,翻译成 Seatbelt 能看懂的规则文字,再拼成调用 /usr/bin/sandbox-exec 时需要的参数。它会先算出哪些目录可读、哪些目录可写、哪些路径必须拒绝;再根据代理配置决定是否只允许访问 localhost 上的代理端口,或者允许 Unix 域套接字(一种本机程序之间通信用的“本地插座”)。它还会把路径做绝对化和规范化,避免相对路径带来误判。最后,create_seatbelt_command_args 把所有规则、路径参数和真正要运行的命令拼到一起,交给上层去启动沙盒里的进程。
is_loopback_host31–33 ↗
fn is_loopback_host(host: &str) -> bool
作用:判断一个主机名是不是“本机”。这里的本机包括 localhost、127.0.0.1 和 ::1,也就是程序只跟自己这台机器通信。
数据流:输入一个主机名字符串 → 它用不区分大小写和固定地址做比较 → 输出 true 或 false,告诉后续逻辑这个地址是不是本机地址。
调用关系:proxy_loopback_ports_from_env 在分析代理地址时会用它。只有确认代理跑在本机,后面才会把对应端口加入沙盒允许列表。
调用图:被 1 处调用(proxy_loopback_ports_from_env)。
proxy_scheme_default_port35–41 ↗
fn proxy_scheme_default_port(scheme: &str) -> u16
作用:给代理地址补默认端口。比如 https 默认 443,socks 默认 1080,其他大多按 http 的 80 来算。
数据流:输入一个协议名,比如 https 或 socks5 → 它查一张很小的规则表 → 输出这个协议通常使用的端口号。
调用关系:它是解析代理地址时的辅助函数。当代理 URL 没写端口时,调用方可以用它猜出应该放行哪个端口。
proxy_loopback_ports_from_env43–76 ↗
fn proxy_loopback_ports_from_env(env: &HashMap<String, String>) -> Vec<u16>
作用:从环境变量里的代理设置中,找出运行在本机的代理端口。这样沙盒可以只放行这些端口,而不是放开整个网络。
数据流:输入一组环境变量 → 它逐个读取常见代理变量,补全缺省协议,解析 URL,确认主机是不是本机,再取出端口并去重排序 → 输出端口号列表。
调用关系:proxy_policy_inputs 会调用它来把 NetworkProxy 里的代理设置变成更具体的网络放行清单。它内部会用 is_loopback_host 判断地址是否安全地指向本机。
调用图:调用 1 个内部函数(is_loopback_host);被 1 处调用(proxy_policy_inputs);外部调用 4 个(new, parse, proxy_url_env_value, format!)。
UnixDomainSocketPolicy::default94–96 ↗
fn default() -> Self
作用:给 Unix 域套接字策略一个安全的默认值:默认不全部放开,只允许一个空的白名单。
数据流:没有外部输入 → 它创建 Restricted 模式,并把允许列表设为空 → 输出这个默认策略。
调用关系:当没有显式网络代理配置时,其他结构可以用这个默认值起步,避免不小心允许所有本机套接字通信。
调用图:外部调用 1 个(vec!)。
proxy_policy_inputs105–153 ↗
fn proxy_policy_inputs(
network: Option<&NetworkProxy>,
extra_allow_unix_sockets: &[AbsolutePathBuf],
) -> ProxyPolicyInputs
作用:把网络代理配置整理成沙盒规则需要的原材料。它关心三件事:允许哪些本机代理端口、是否有代理配置、Unix 域套接字该怎么放行。
数据流:输入可选的 NetworkProxy 和额外允许的 Unix 套接字路径 → 它把代理配置写成环境变量,找出本机端口,检查是否允许本地监听,并整理套接字白名单或全放开模式 → 输出 ProxyPolicyInputs,供后面生成 Seatbelt 网络规则。
调用关系:create_seatbelt_command_args 在总装沙盒参数时会先调用它。它会把端口提取这件事交给 proxy_loopback_ports_from_env,也会用外部的 has_proxy_url_env_vars 判断代理环境变量是否存在。
调用图:调用 1 个内部函数(proxy_loopback_ports_from_env);被 1 处调用(create_seatbelt_command_args);外部调用 4 个(default, new, has_proxy_url_env_vars, iter)。
normalize_path_for_sandbox155–169 ↗
fn normalize_path_for_sandbox(path: &Path) -> Option<AbsolutePathBuf>
作用:把路径整理成沙盒更可靠使用的绝对路径。它拒绝相对路径,避免“当前目录不同导致意思变了”的问题。
数据流:输入一个路径 → 它先确认路径是绝对路径,再尝试转成项目的绝对路径类型,并尽量跟随系统真实路径做规范化;如果规范化失败,就保留原来的绝对路径 → 输出整理后的绝对路径,或在不合格时输出空。
调用关系:build_seatbelt_access_policy 和 canonicalize_glob_static_prefix_for_sandbox 会用它,确保写进 Seatbelt 规则的路径尽量明确、稳定。
调用图:调用 1 个内部函数(from_absolute_path);被 2 处调用(build_seatbelt_access_policy, canonicalize_glob_static_prefix_for_sandbox);外部调用 1 个(is_absolute)。
unix_socket_path_params171–187 ↗
fn unix_socket_path_params(proxy: &ProxyPolicyInputs) -> Vec<UnixSocketPathParam>
作用:把允许访问的 Unix 域套接字路径变成带编号的参数列表。这样规则里可以引用参数名,而不是直接硬编码路径。
数据流:输入整理好的代理策略 → 如果是全放开模式就返回空列表;如果是白名单模式,就按路径字符串去重、排序,并给每个路径分配编号 → 输出一组 UnixSocketPathParam。
调用关系:unix_socket_dir_params 用它生成命令行里的 -D 参数,unix_socket_policy 用它生成 Seatbelt 规则里的路径引用。
调用图:被 2 处调用(unix_socket_dir_params, unix_socket_policy);外部调用 2 个(new, vec!)。
unix_socket_path_param_key189–191 ↗
fn unix_socket_path_param_key(index: usize) -> String
作用:给某个 Unix 套接字路径编号生成固定参数名,比如 UNIX_SOCKET_PATH_0。规则和命令行参数要用同一个名字才能对上。
数据流:输入一个数字编号 → 它把编号拼进固定字符串格式 → 输出参数名字符串。
调用关系:unix_socket_policy 在写规则时调用它,让规则里的 param 名称和路径参数保持一致。
调用图:被 1 处调用(unix_socket_policy);外部调用 1 个(format!)。
unix_socket_dir_params193–203 ↗
fn unix_socket_dir_params(proxy: &ProxyPolicyInputs) -> Vec<(String, PathBuf)>
作用:把允许的 Unix 域套接字路径转换成 sandbox-exec 命令能接收的参数。可以理解为给沙盒规则里的占位符填真实路径。
数据流:输入代理策略 → 它先拿到带编号的套接字路径,再把每个编号转成参数名,并配上实际 PathBuf 路径 → 输出一组键值对。
调用关系:create_seatbelt_command_args 在最后拼 sandbox-exec 参数时会调用它,把这些路径作为 -DKEY=value 传进去。
调用图:调用 1 个内部函数(unix_socket_path_params);被 1 处调用(create_seatbelt_command_args)。
unix_socket_policy208–242 ↗
fn unix_socket_policy(proxy: &ProxyPolicyInputs) -> String
作用:生成允许 Unix 域套接字通信的 Seatbelt 规则。Unix 域套接字是本机进程之间通信的通道,不是普通互联网连接。
数据流:输入代理策略 → 它判断是否需要允许套接字;如果全放开,就生成较宽的本机套接字规则;如果是白名单,就为每个允许路径生成绑定和连接规则 → 输出一段可直接拼进大沙盒规则的文字。
调用关系:dynamic_network_policy_for_network 在生成网络权限时会调用它。它内部先用 unix_socket_path_params 拿路径,再用 unix_socket_path_param_key 生成规则里的参数名。
调用图:调用 2 个内部函数(unix_socket_path_param_key, unix_socket_path_params);被 1 处调用(dynamic_network_policy_for_network);外部调用 3 个(new, format!, matches!)。
dynamic_network_policy245–255 ↗
fn dynamic_network_policy(
sandbox_policy: &SandboxPolicy,
enforce_managed_network: bool,
proxy: &ProxyPolicyInputs,
) -> String
作用:这是一个兼容旧接口的小包装函数,把完整 SandboxPolicy 转成网络策略后再生成网络沙盒规则。
数据流:输入总体沙盒策略、是否强制托管网络、代理信息 → 它从总体策略里提取 NetworkSandboxPolicy → 输出由 dynamic_network_policy_for_network 生成的规则文字。
调用关系:它把真正的工作交给 dynamic_network_policy_for_network。这样旧调用方不用自己先拆出网络策略。
调用图:调用 2 个内部函数(from, dynamic_network_policy_for_network)。
dynamic_network_policy_for_network257–319 ↗
fn dynamic_network_policy_for_network(
network_policy: NetworkSandboxPolicy,
enforce_managed_network: bool,
proxy: &ProxyPolicyInputs,
) -> String
作用:根据网络权限和代理情况,生成最终的网络沙盒规则。它决定是完全禁止网络、放开网络,还是只允许连本机代理端口。
数据流:输入网络策略、是否强制使用受管网络、代理整理结果 → 它检查是否有代理端口、代理配置、本地绑定许可和 Unix 套接字需求;必要时只放行 localhost 端口、DNS 或套接字;如果网络策略本来允许且没有代理限制,就生成较宽的入站和出站规则 → 输出 Seatbelt 网络规则字符串,或者空字符串表示不放行网络。
调用关系:create_seatbelt_command_args 会直接调用它来生成整份沙盒里的网络部分;dynamic_network_policy 也会把旧格式策略转交给它。它在需要套接字规则时会调用 unix_socket_policy。
调用图:调用 2 个内部函数(is_enabled, unix_socket_policy);被 2 处调用(create_seatbelt_command_args, dynamic_network_policy);外部调用 3 个(from, new, format!)。
root_absolute_path321–326 ↗
fn root_absolute_path() -> AbsolutePathBuf
作用:安全地构造根目录“/”的绝对路径对象。根目录用于表示整个磁盘范围。
数据流:没有业务输入 → 它把字符串 / 转成 AbsolutePathBuf;如果这都失败,就直接 panic,因为根目录不可能不是绝对路径 → 输出根目录路径对象。
调用关系:create_seatbelt_command_args 在需要表达“全盘读”或“全盘写,但排除某些位置”时会用到它。
调用图:调用 1 个内部函数(from_absolute_path);外部调用 2 个(new, panic!)。
build_seatbelt_access_policy335–390 ↗
fn build_seatbelt_access_policy(
action: &str,
param_prefix: &str,
roots: Vec<SeatbeltAccessRoot>,
) -> (String, Vec<(String, PathBuf)>)
作用:把“这些根目录可以读或写,但这些子路径不行”翻译成 Seatbelt 的文件访问规则。它是文件读写权限生成的核心拼装器。
数据流:输入动作名,比如 file-read 或 file-write,再输入参数名前缀和一组可访问根目录 → 它为每个根目录生成参数,加入排除子路径和受保护元数据名的限制 → 输出一段 Seatbelt 规则文字,以及这些规则需要的路径参数列表。
调用关系:create_seatbelt_command_args 用它生成读规则和写规则。它会调用 normalize_path_for_sandbox 规范路径,也会调用 seatbelt_protected_metadata_name_regex 生成保护元数据目录的匹配规则。
调用图:调用 2 个内部函数(normalize_path_for_sandbox, seatbelt_protected_metadata_name_regex);被 1 处调用(create_seatbelt_command_args);外部调用 4 个(new, new, format!, vec!)。
seatbelt_protected_metadata_name_regex392–404 ↗
fn seatbelt_protected_metadata_name_regex(root: &AbsolutePathBuf, name: &str) -> String
作用:为某个受保护的元数据名字生成匹配规则。比如要保护某个根目录下的 .codex,就需要匹配这个目录本身和里面所有东西。
数据流:输入根目录和元数据名字 → 它清理根目录末尾多余斜杠,转义特殊字符,再拼出从路径开头到结尾的正则表达式 → 输出 Seatbelt 可用的正则字符串。
调用关系:build_seatbelt_access_policy 在写访问规则时会调用它,用来排除那些虽然在可写目录里、但仍不应该被改动的元数据路径。
调用图:调用 1 个内部函数(to_string_lossy);被 1 处调用(build_seatbelt_access_policy);外部调用 5 个(format!, escape, ends_with, len, pop)。
protected_metadata_names_for_writable_root406–422 ↗
fn protected_metadata_names_for_writable_root(
file_system_sandbox_policy: &FileSystemSandboxPolicy,
writable_root: &WritableRoot,
cwd: &Path,
) -> Vec<String>
作用:算出某个可写根目录里哪些元数据名字还需要特别保护。它避免用户把整个目录设为可写后,顺手改掉项目用来记录状态的隐藏文件。
数据流:输入文件系统沙盒策略、一个可写根目录和当前工作目录 → 它先拿根目录自己声明要保护的名字,再检查全局保护名单;如果某个名字按当前策略并不能安全写入,就加入保护列表 → 输出需要保护的名字列表。
调用关系:生成写权限规则时会用到它,随后这些名字会交给 build_seatbelt_access_policy,变成真正的 Seatbelt 排除条件。
调用图:调用 1 个内部函数(can_write_path_with_cwd)。
build_seatbelt_unreadable_glob_policy424–456 ↗
fn build_seatbelt_unreadable_glob_policy(
file_system_sandbox_policy: &FileSystemSandboxPolicy,
cwd: &Path,
) -> String
作用:把“不可读”的通配符规则变成 Seatbelt 的拒绝规则。这样即使某个大目录允许读,也能把敏感子路径挡住。
数据流:输入文件系统沙盒策略和当前工作目录 → 它取出不可读的 glob 模式;glob 是带 *、? 这类通配符的路径写法;然后把每个模式转成正则,并为读取和删除类写操作都生成 deny 规则 → 输出多行拒绝规则文字。
调用关系:create_seatbelt_command_args 在拼整份策略时会调用它。它会让 canonicalize_glob_static_prefix_for_sandbox 尽量规范化路径前缀,并让 seatbelt_regex_for_unreadable_glob 负责把 glob 翻译成正则。
调用图:调用 3 个内部函数(get_unreadable_globs_with_cwd, canonicalize_glob_static_prefix_for_sandbox, seatbelt_regex_for_unreadable_glob);被 1 处调用(create_seatbelt_command_args);外部调用 4 个(new, new, new, format!)。
canonicalize_glob_static_prefix_for_sandbox458–482 ↗
fn canonicalize_glob_static_prefix_for_sandbox(pattern: &str) -> Option<String>
作用:尽量把 glob 模式中不含通配符的前半段规范化。这样同一个真实路径即使写法不同,也更可能被沙盒规则正确拦住。
数据流:输入一个 glob 路径模式 → 它找到第一个通配符前面的固定路径部分;如果能规范化,就把规范化后的前缀和原来的通配符后缀重新拼起来 → 输出新的模式,或者在无法改进时输出空。
调用关系:build_seatbelt_unreadable_glob_policy 调用它,为每条不可读规则额外生成一个更贴近真实文件系统路径的版本。它内部用 normalize_path_for_sandbox 做路径规范化。
调用图:调用 1 个内部函数(normalize_path_for_sandbox);被 1 处调用(build_seatbelt_unreadable_glob_policy);外部调用 2 个(new, format!)。
seatbelt_regex_for_unreadable_glob484–566 ↗
fn seatbelt_regex_for_unreadable_glob(pattern: &str) -> Option<String>
作用:把项目里的 glob 路径模式翻译成 Seatbelt 能用的正则表达式。正则表达式就是一种更底层的“字符串匹配规则”。
数据流:输入一个 glob 模式 → 它逐个字符翻译:* 匹配单层路径内容,? 匹配一个字符,**/ 可匹配多层目录,方括号字符类也会保留;没有通配符时则表示这个路径和它下面所有内容 → 输出从开头到结尾都固定住的正则,空模式则输出空。
调用关系:build_seatbelt_unreadable_glob_policy 会调用它,把不可读 glob 变成真正能写进 Seatbelt deny 规则的表达式。
调用图:被 1 处调用(build_seatbelt_unreadable_glob_policy);外部调用 3 个(from, new, escape)。
create_seatbelt_command_args_for_legacy_policy569–589 ↗
fn create_seatbelt_command_args_for_legacy_policy(
command: Vec<String>,
sandbox_policy: &SandboxPolicy,
sandbox_policy_cwd: &Path,
enforce_managed_network: bool,
network: Option<&
作用:把旧格式的沙盒策略接到新的参数生成流程上。这样老调用方还能用,但真正生成规则的逻辑只保留一套。
数据流:输入命令、旧的 SandboxPolicy、策略对应的当前目录、网络限制和代理 → 它先把旧文件系统策略转成新的 FileSystemSandboxPolicy,再把网络策略也拆出来 → 输出 create_seatbelt_command_args 生成的 sandbox-exec 参数列表。
调用关系:它是兼容层,自己不拼规则,而是把转换后的参数交给 create_seatbelt_command_args。
调用图:调用 3 个内部函数(from_legacy_sandbox_policy_for_cwd, from, create_seatbelt_command_args)。
create_seatbelt_command_args602–741 ↗
fn create_seatbelt_command_args(args: CreateSeatbeltCommandArgsParams<'_>) -> Vec<String>
作用:生成最终要传给 macOS sandbox-exec 的完整参数。它是这个文件最主要的出口:上层想在沙盒里跑命令,就调用它。
数据流:输入要运行的命令、文件系统权限、网络权限、当前目录、代理配置和额外套接字路径 → 它分别生成读规则、写规则、不可读拒绝规则、网络规则和平台默认规则,再收集所有路径参数,最后拼成 -p 策略文本、-D 参数、-- 分隔符和原命令 → 输出完整的字符串参数列表,不直接启动进程。
调用关系:run_command_under_sandbox 会在真正运行命令前调用它;旧接口 create_seatbelt_command_args_for_legacy_policy 也会转到这里。它把不同子任务分给 build_seatbelt_access_policy、build_seatbelt_unreadable_glob_policy、proxy_policy_inputs、dynamic_network_policy_for_network 和 unix_socket_dir_params。
调用图:调用 5 个内部函数(build_seatbelt_access_policy, build_seatbelt_unreadable_glob_policy, dynamic_network_policy_for_network, proxy_policy_inputs, unix_socket_dir_params);被 2 处调用(run_command_under_sandbox, create_seatbelt_command_args_for_legacy_policy);外部调用 4 个(new, new, format!, vec!)。
Shell 解析基础
这些辅助工具提供特定于 shell 的解析和规范化原语,供更高层级的命令分析使用。
shell-command/src/bash.rs源码 ↗
这个文件像一个“命令安检员”。外部传进来的可能是类似「bash -lc 'ls && pwd'」这样的命令,看起来只是一串字,但里面可能藏着变量展开、命令替换、文件重定向等危险动作。文件先用 tree-sitter(一种把代码拆成语法树的工具)读懂 shell 脚本的结构,再检查这棵树里是不是只有普通命令、普通单词、引号字符串,以及少数安全连接符,比如 &&、||、;、|。如果确认安全,就输出类似「每个命令对应一组参数」的列表;如果发现复杂结构,就返回 None,意思是“不敢简化,不放行”。它还特别处理 here-doc(把一大段文本喂给命令的 shell 写法),只在能确认只有一个命令、且没有额外文件重定向时,提取这个命令的开头参数,供执行策略判断使用。
try_parse_shell13–20 ↗
fn try_parse_shell(shell_lc_arg: &str) -> Option<Tree>
作用:把一段 shell 脚本文本交给 bash 语法解析器,尝试读成一棵语法树。有人想进一步检查命令结构时,会先用它把“字符串”变成“有结构的树”。
数据流:输入是一段 shell 脚本文字 → 它创建 tree-sitter 的解析器,加载 bash 语法规则,然后解析这段文字 → 成功就返回语法树 Tree,失败就返回 None,不改动外部状态。
调用关系:它是后续所有 shell 内容检查的第一步。parse_shell_script_into_commands 和 parse_shell_lc_single_command_prefix 会调用它;外部的 parse_shell_script 也会用它先拿到语法树,再继续做安全筛查。
调用图:被 3 处调用(parse_shell_lc_single_command_prefix, parse_shell_script_into_commands, parse_shell_script);外部调用 1 个(new)。
try_parse_word_only_commands_sequence29–95 ↗
fn try_parse_word_only_commands_sequence(tree: &Tree, src: &str) -> Option<Vec<Vec<String>>>
作用:检查一棵 shell 语法树是不是只包含“普通命令加普通参数”,并把它拆成多条命令。它的重点不是支持所有 shell,而是把不确定、有风险的写法直接拒绝。
数据流:输入是语法树和原始脚本文字 → 它先看树里有没有解析错误,再遍历每个节点,只允许命令、单词、字符串、数字、简单连接符等白名单内容 → 如果所有命令都能安全拆出参数,就输出 Vec<Vec<String>>;如果发现括号、重定向、替换、奇怪操作符等,就返回 None。
调用关系:它接在 try_parse_shell 后面工作,是“安全过滤”的核心。parse_shell_script_into_commands 和外部 parse_shell_script 会调用它;它把每个 command 节点交给 parse_plain_command_from_node 去提取具体参数。
调用图:调用 1 个内部函数(parse_plain_command_from_node);被 2 处调用(parse_shell_script_into_commands, parse_shell_script);外部调用 3 个(root_node, new, vec!)。
parse_shell_script_into_commands98–101 ↗
fn parse_shell_script_into_commands(script: &str) -> Option<Vec<Vec<String>>>
作用:提供一个简单入口:给它一段 shell 脚本,它返回安全拆出的命令列表。调用者不用关心语法树细节。
数据流:输入是一段脚本文字 → 它先调用 try_parse_shell 得到语法树,再调用 try_parse_word_only_commands_sequence 做安全检查和拆分 → 输出命令参数列表,或在解析失败、内容不安全时输出 None。
调用关系:这是普通 shell 脚本拆分流程的门面函数。parse_shell_lc_plain_commands、测试辅助 tests::parse_seq,以及一些判断命令风险和内存用途的外部逻辑会通过它拿到结构化命令。
调用图:调用 2 个内部函数(try_parse_shell, try_parse_word_only_commands_sequence);被 3 处调用(memories_usage_kinds_from_command, parse_shell_lc_plain_commands, parse_seq)。
extract_bash_command103–116 ↗
fn extract_bash_command(command: &[String]) -> Option<(&str, &str)>
作用:从一个完整命令数组里识别「bash/zsh/sh -lc 脚本」或「bash/zsh/sh -c 脚本」这种形式。它只负责确认外层确实是常见 shell,并取出里面真正的脚本文字。
数据流:输入是类似 [shell, flag, script] 的字符串数组 → 它要求数组刚好三个元素,flag 必须是 -lc 或 -c,shell 路径还要能识别成 Bash、Zsh 或 Sh → 符合就返回 shell 名和脚本文字;不符合就返回 None。
调用关系:它是处理 shell 包装命令的入口检查。parse_shell_lc_plain_commands、parse_shell_lc_single_command_prefix 以及多个外部审批、策略、格式化函数会先调用它,确认这真的是 shell 执行脚本的情况。
调用图:被 6 处调用(canonicalize_command_for_approval, parse_shell_lc_plain_commands, parse_shell_lc_single_command_prefix, extract_shell_command, parse_shell_lc_commands, format_unified_exec_interaction);外部调用 1 个(matches!)。
parse_shell_lc_plain_commands121–124 ↗
fn parse_shell_lc_plain_commands(command: &[String]) -> Option<Vec<Vec<String>>>
作用:专门处理「bash -lc '...'」这类命令,并在脚本足够简单安全时拆出里面的多条普通命令。它常用于执行策略判断:到底里面要跑哪些程序。
数据流:输入是完整命令数组 → 它先用 extract_bash_command 抽出脚本,再用 parse_shell_script_into_commands 解析并安全拆分 → 输出多条命令的参数列表,或在不是 shell 命令、脚本复杂时返回 None。
调用关系:它把“外层 shell 命令识别”和“内层脚本解析”串起来。命令审批、执行策略、危险命令判断、已知安全命令判断等外部流程会调用它。
调用图:调用 2 个内部函数(extract_bash_command, parse_shell_script_into_commands);被 6 处调用(canonicalize_command_for_approval, commands_for_exec_policy, commands_for_intercepted_exec_policy, parse_zsh_lc_plain_commands, command_might_be_dangerous, is_known_safe_command)。
parse_shell_lc_single_command_prefix128–144 ↗
fn parse_shell_lc_single_command_prefix(command: &[String]) -> Option<Vec<String>>
作用:处理带 here-doc 的 shell 命令,只在脚本里确实只有一个命令时,提取这个命令的开头参数。here-doc 可以理解为“把一段大文本从标准输入喂给命令”。
数据流:输入是完整命令数组 → 它先抽出 shell 脚本并解析语法树,确认没有解析错误,必须存在 heredoc_redirect,且不能有普通文件重定向 → 再确认只有一个 command 节点,并提取这个命令的字面参数 → 成功返回参数列表,否则返回 None。
调用关系:它服务于执行策略里一种特殊但常见的写法,比如「python3 <<'PY' ...」。commands_for_exec_policy 和 commands_for_intercepted_exec_policy 会调用它;它内部把找节点、检查节点、提取参数分别交给 find_single_command_node、has_named_descendant_kind 和 parse_heredoc_command_words。
调用图:调用 5 个内部函数(extract_bash_command, find_single_command_node, has_named_descendant_kind, parse_heredoc_command_words, try_parse_shell);被 3 处调用(commands_for_exec_policy, commands_for_intercepted_exec_policy, parse_shell_lc_single_command_prefix_supports_heredoc)。
parse_plain_command_from_node146–202 ↗
fn parse_plain_command_from_node(cmd: tree_sitter::Node, src: &str) -> Option<Vec<String>>
作用:从一个普通 command 语法节点里取出命令名和参数。它只接受字面量单词、数字、简单单双引号字符串,以及安全的拼接参数。
数据流:输入是一个 command 节点和原始脚本文字 → 它逐个看命令的子节点,把 command_name、word、number、string、raw_string、concatenation 转成字符串参数 → 如果遇到变量展开、命令替换或不认识的结构,就返回 None;否则输出参数数组。
调用关系:它是 try_parse_word_only_commands_sequence 拆每条命令时的具体工人。遇到双引号内容会交给 parse_double_quoted_string,遇到单引号内容会交给 parse_raw_string。
调用图:调用 2 个内部函数(parse_double_quoted_string, parse_raw_string);被 1 处调用(try_parse_word_only_commands_sequence);外部调用 5 个(kind, named_children, walk, new, new)。
parse_heredoc_command_words204–239 ↗
fn parse_heredoc_command_words(cmd: Node<'_>, src: &str) -> Option<Vec<String>>
作用:从带 here-doc 的单个命令里提取真正的可执行程序和参数,同时忽略 here-doc 本身附带的输入内容。它保证参数必须是字面量,不能夹带展开。
数据流:输入是 command 节点和原始脚本文字 → 它读取命令名、普通单词和数字,确认它们没有子结构,也就是没有变量展开之类的东西;对 heredoc 相关节点则允许跳过 → 如果提取到至少一个参数就返回参数列表,否则返回 None。
调用关系:它只被 parse_shell_lc_single_command_prefix 调用,用在确认脚本只有一个 here-doc 命令之后。它会借助 is_literal_word_or_number 判断单词是否纯字面量,也用 is_allowed_heredoc_attachment_kind 判断哪些 here-doc 附件可以忽略。
调用图:调用 2 个内部函数(is_allowed_heredoc_attachment_kind, is_literal_word_or_number);被 1 处调用(parse_shell_lc_single_command_prefix);外部调用 5 个(kind, named_children, walk, new, matches!)。
is_literal_word_or_number241–247 ↗
fn is_literal_word_or_number(node: Node<'_>) -> bool
作用:判断一个 word 或 number 节点是不是纯字面量。简单说,就是确认它不是外表像单词、里面却藏着变量展开或别的结构。
数据流:输入是一个语法节点 → 它先确认节点类型是 word 或 number,再检查它没有任何命名子节点 → 是纯文本就返回 true,否则返回 false。
调用关系:parse_heredoc_command_words 会用它给 here-doc 命令的命令名和参数做安全检查,防止把复杂 shell 表达式误当成普通参数。
调用图:被 1 处调用(parse_heredoc_command_words);外部调用 3 个(named_children, walk, matches!)。
is_allowed_heredoc_attachment_kind249–258 ↗
fn is_allowed_heredoc_attachment_kind(kind: &str) -> bool
作用:判断某个语法节点类型是不是 here-doc 相关、可以在提取命令参数时忽略的附件。它像一张小白名单。
数据流:输入是节点类型名字 → 它和 heredoc_body、heredoc_redirect、herestring_redirect 等允许类型逐一匹配 → 匹配就返回 true,不匹配返回 false。
调用关系:parse_heredoc_command_words 遇到非参数节点时会问它:这个东西是不是允许的 here-doc 附属结构。如果是,就跳过;不是,就拒绝整个命令。
调用图:被 1 处调用(parse_heredoc_command_words);外部调用 1 个(matches!)。
find_single_command_node260–277 ↗
fn find_single_command_node(root: Node<'_>) -> Option<Node<'_>>
作用:在一棵语法树里寻找唯一的 command 节点。它用于确认脚本里只有一个真正要执行的命令。
数据流:输入是根节点 → 它用栈遍历所有命名子节点,看到 command 就记下来;如果看到第二个 command,立刻返回 None → 最后如果正好一个,就返回这个节点。
调用关系:parse_shell_lc_single_command_prefix 用它来防止把多命令脚本当成单命令处理。这样像「python <<EOF ... EOF; echo done」这种写法就不会被误放行。
调用图:被 1 处调用(parse_shell_lc_single_command_prefix);外部调用 1 个(vec!)。
has_named_descendant_kind279–291 ↗
fn has_named_descendant_kind(node: Node<'_>, kind: &str) -> bool
作用:检查某个节点下面是否存在指定类型的子孙节点。它是一个通用的小搜索工具。
数据流:输入是起始节点和要找的节点类型名字 → 它遍历这棵子树,找到类型相同的节点就返回 true → 遍历完没找到就返回 false。
调用关系:parse_shell_lc_single_command_prefix 用它检查脚本里是否有 here-doc,以及是否混入了普通文件重定向。它帮助决定这个特殊脚本能不能安全简化。
调用图:被 1 处调用(parse_shell_lc_single_command_prefix);外部调用 1 个(vec!)。
parse_double_quoted_string293–309 ↗
fn parse_double_quoted_string(node: Node, src: &str) -> Option<String>
作用:把一个双引号字符串节点转成普通字符串,但只接受里面全是纯文本的情况。像「$HOME」这种变量展开会被拒绝。
数据流:输入是 string 节点和原始脚本文字 → 它确认所有命名子节点都是 string_content,再取出原文并去掉前后的双引号 → 成功返回引号里的内容,发现展开或格式不对就返回 None。
调用关系:parse_plain_command_from_node 在处理双引号参数时调用它。它是防止双引号里藏变量、命令替换等 shell 特性的关键关卡。
调用图:被 1 处调用(parse_plain_command_from_node);外部调用 4 个(kind, named_children, utf8_text, walk)。
parse_raw_string311–321 ↗
fn parse_raw_string(node: Node, src: &str) -> Option<String>
作用:把一个单引号字符串节点转成普通字符串。单引号在 shell 里通常表示原样文本,所以这里主要负责去掉外层引号。
数据流:输入是 raw_string 节点和原始脚本文字 → 它确认节点类型正确,取出原文,然后去掉前后的单引号 → 成功返回内部文本,不符合格式就返回 None。
调用关系:parse_plain_command_from_node 在处理单引号参数和拼接参数时调用它,让「'hi there'」能变成真正的参数「hi there」。
调用图:被 1 处调用(parse_plain_command_from_node);外部调用 2 个(kind, utf8_text)。
tests::parse_seq328–330 ↗
fn parse_seq(src: &str) -> Option<Vec<Vec<String>>>
作用:测试里的小帮手,用更短的名字调用 parse_shell_script_into_commands。它让测试用例更像在直接描述输入和期待输出。
数据流:输入是一段测试用 shell 脚本 → 它直接交给 parse_shell_script_into_commands → 输出解析结果或 None。
调用关系:多个测试函数会调用它,避免每个测试都重复写完整函数名。它只在测试编译时存在。
调用图:调用 1 个内部函数(parse_shell_script_into_commands)。
tests::accepts_single_simple_command333–336 ↗
fn accepts_single_simple_command()
作用:确认最简单的命令「ls -1」能被接受并拆成命令名和参数。
数据流:输入是测试字符串「ls -1」 → 通过 tests::parse_seq 解析 → 断言输出等于 [[ls, -1]]。
调用关系:这是基础正例测试,验证 parse_shell_script_into_commands 的最小可用行为没有坏。
调用图:外部调用 2 个(assert_eq!, parse_seq)。
tests::accepts_multiple_commands_with_allowed_operators339–349 ↗
fn accepts_multiple_commands_with_allowed_operators()
作用:确认用 &&、;、| 连接的多条普通命令可以被拆开。这里验证“安全连接符”确实被允许。
数据流:输入是一串包含 ls、pwd、echo、wc 的脚本 → 调用 tests::parse_seq → 断言输出按源码顺序分成四条命令。
调用关系:它覆盖 try_parse_word_only_commands_sequence 的多命令遍历和排序行为,确保管道和顺序符不会导致命令顺序错乱。
调用图:外部调用 3 个(assert_eq!, parse_seq, vec!)。
tests::extracts_double_and_single_quoted_strings352–364 ↗
fn extracts_double_and_single_quoted_strings()
作用:确认单双引号里的普通文本会被取出来当成一个参数,而不是保留引号或拆成多个词。
数据流:输入分别是 echo 加双引号文本、echo 加单引号文本 → 通过 tests::parse_seq 解析 → 断言参数变成没有外层引号的字符串。
调用关系:它直接验证 parse_double_quoted_string 和 parse_raw_string 通过 parse_plain_command_from_node 生效。
调用图:外部调用 2 个(assert_eq!, parse_seq)。
tests::accepts_double_quoted_strings_with_newlines367–378 ↗
fn accepts_double_quoted_strings_with_newlines()
作用:确认双引号里的换行符可以保留下来。提交信息这类参数经常会包含多行文本。
数据流:输入是 git commit -m 加一个含换行的双引号字符串 → 调用 tests::parse_seq → 断言最后一个参数保留 line1 和 line2 中间的换行。
调用关系:它验证 parse_double_quoted_string 不是简单按行切割,而是按语法节点读取完整字符串内容。
调用图:外部调用 2 个(assert_eq!, parse_seq)。
tests::accepts_mixed_quote_concatenation381–390 ↗
fn accepts_mixed_quote_concatenation()
作用:确认 shell 里相邻的普通文本、单引号、双引号可以拼成同一个参数。比如路径被拆开写,也应该还原成一个路径参数。
数据流:输入是 echo 后面由多段引号和普通字符拼起来的路径 → 解析后检查输出 → 断言它们合成「/usr/local/bin」。
调用关系:它验证 parse_plain_command_from_node 对 concatenation 节点的处理,包括混合调用 parse_double_quoted_string 和 parse_raw_string。
调用图:外部调用 1 个(assert_eq!)。
tests::rejects_double_quoted_strings_with_expansions393–396 ↗
fn rejects_double_quoted_strings_with_expansions()
作用:确认双引号里出现变量展开时会被拒绝。因为「$USER」这类内容要到执行时才知道,不能安全地提前当成固定参数。
数据流:输入是包含 ${USER} 或 $HOME 的双引号命令 → 尝试解析 → 断言结果是 None。
调用关系:它保护 parse_double_quoted_string 的安全边界,确保双引号只接受纯文本 string_content。
调用图:外部调用 1 个(assert!)。
tests::accepts_numbers_as_words399–409 ↗
fn accepts_numbers_as_words()
作用:确认数字参数也能像普通单词一样被接受。很多命令参数本来就是数字。
数据流:输入是「echo 123 456」 → 通过 tests::parse_seq 解析 → 断言数字被保留成字符串参数。
调用关系:它验证 parse_plain_command_from_node 对 number 节点的支持。
调用图:外部调用 2 个(assert_eq!, parse_seq)。
tests::rejects_parentheses_and_subshells412–415 ↗
fn rejects_parentheses_and_subshells()
作用:确认括号和子 shell 会被拒绝。子 shell 可以改变执行结构,不能当成简单命令列表处理。
数据流:输入包含「(ls)」或连接符后面的括号命令 → 尝试解析 → 断言结果是 None。
调用关系:它验证 try_parse_word_only_commands_sequence 的节点白名单确实会挡住不允许的结构。
调用图:外部调用 1 个(assert!)。
tests::rejects_redirections_and_unsupported_operators418–421 ↗
fn rejects_redirections_and_unsupported_operators()
作用:确认文件重定向和后台执行符号不会被普通命令解析器接受。这些写法可能读写文件或改变执行方式。
数据流:输入包含「> out.txt」或单个「&」 → 尝试解析 → 断言解析失败。
调用关系:它覆盖 try_parse_word_only_commands_sequence 对标点和操作符的拒绝逻辑。
调用图:外部调用 1 个(assert!)。
tests::rejects_command_and_process_substitutions_and_expansions424–429 ↗
fn rejects_command_and_process_substitutions_and_expansions()
作用:确认命令替换、反引号、变量展开等动态 shell 功能都会被拒绝。它们执行前看不出最终参数,风险更高。
数据流:输入分别包含 $(pwd)、反引号、$HOME、双引号内变量 → 尝试解析 → 断言全部返回 None。
调用关系:它验证语法树白名单和字符串解析的组合防线,防止复杂 shell 特性混进普通参数列表。
调用图:外部调用 1 个(assert!)。
tests::rejects_variable_assignment_prefix432–434 ↗
fn rejects_variable_assignment_prefix()
作用:确认命令前面的环境变量赋值会被拒绝。比如「FOO=bar ls」会改变命令运行环境,不是单纯参数。
数据流:输入是带变量赋值前缀的命令 → 尝试解析 → 断言结果是 None。
调用关系:它验证 try_parse_word_only_commands_sequence 不允许 variable_assignment 这类节点。
调用图:外部调用 1 个(assert!)。
tests::rejects_trailing_operator_parse_error437–439 ↗
fn rejects_trailing_operator_parse_error()
作用:确认以连接符结尾的残缺命令会被拒绝。比如「ls &&」语法不完整。
数据流:输入是「ls &&」 → 解析时语法树带错误 → 断言结果是 None。
调用关系:它验证 try_parse_word_only_commands_sequence 一开始检查 root.has_error 的行为。
调用图:外部调用 1 个(assert!)。
tests::rejects_empty_command_position_with_leading_operator442–444 ↗
fn rejects_empty_command_position_with_leading_operator()
作用:确认开头就是连接符、前面没有命令的脚本会被拒绝。
数据流:输入是「&& ls」 → 尝试解析 → 断言解析失败。
调用关系:它验证解析错误和安全检查不会把空命令位置忽略掉。
调用图:外部调用 1 个(assert!)。
tests::rejects_empty_command_position_with_double_separator447–449 ↗
fn rejects_empty_command_position_with_double_separator()
作用:确认两个分号连在一起造成的空命令位置会被拒绝。
数据流:输入是「ls ;; pwd」 → 尝试解析 → 断言结果是 None。
调用关系:它覆盖语法错误处理,确保命令序列必须每个位置都有真实命令。
调用图:外部调用 1 个(assert!)。
tests::rejects_empty_command_position_with_empty_pipeline_segment452–454 ↗
fn rejects_empty_command_position_with_empty_pipeline_segment()
作用:确认管道中间缺命令时会被拒绝。比如「ls | | wc」中间是空的。
数据流:输入是空管道段脚本 → 尝试解析 → 断言结果是 None。
调用关系:它验证管道虽然是允许连接符,但必须连接真实、可解析的命令。
调用图:外部调用 1 个(assert!)。
tests::parse_zsh_lc_plain_commands457–461 ↗
fn parse_zsh_lc_plain_commands()
作用:确认 zsh 的「-lc」形式也会被当作可识别 shell 命令处理,不只支持 bash。
数据流:输入是数组 [zsh, -lc, ls] → 调用 parse_shell_lc_plain_commands → 断言输出是一条 ls 命令。
调用关系:它验证 extract_bash_command 对 ShellType::Zsh 的支持,以及后续普通脚本解析链路。
调用图:调用 1 个内部函数(parse_shell_lc_plain_commands);外部调用 2 个(assert_eq!, vec!)。
tests::accepts_concatenated_flag_and_value464–476 ↗
fn accepts_concatenated_flag_and_value()
作用:确认像「-g"*.py"」这种参数拼接写法能被还原成一个参数。很多命令行工具允许 flag 和值贴在一起写。
数据流:输入是 rg 命令,里面有双引号拼接参数 → 通过 tests::parse_seq 解析 → 断言「-g".py"」变成「-g.py」。
调用关系:它验证 parse_plain_command_from_node 对 concatenation 节点和 parse_double_quoted_string 的配合。
调用图:外部调用 2 个(assert_eq!, parse_seq)。
tests::accepts_concatenated_flag_with_single_quotes479–490 ↗
fn accepts_concatenated_flag_with_single_quotes()
作用:确认单引号参与的参数拼接也能正确处理。
数据流:输入是 grep 命令,里面有「-g'.txt'」 → 解析后 → 断言它变成单个参数「-g.txt」。
调用关系:它验证 parse_plain_command_from_node 对拼接里的 raw_string 处理,也就是 parse_raw_string 的路径。
调用图:外部调用 2 个(assert_eq!, parse_seq)。
tests::rejects_concatenation_with_variable_substitution493–497 ↗
fn rejects_concatenation_with_variable_substitution()
作用:确认拼接参数里如果包含变量展开,会被拒绝。即使外面看起来像普通参数,也不能把动态内容当固定值。
数据流:输入是 rg 命令,拼接字符串里有 $VAR 或 ${VAR} → 尝试解析 → 断言结果是 None。
调用关系:它保护 concatenation 分支里的双引号解析,确保 parse_double_quoted_string 不放过变量展开。
调用图:外部调用 1 个(assert!)。
tests::rejects_concatenation_with_command_substitution500–504 ↗
fn rejects_concatenation_with_command_substitution()
作用:确认拼接参数里如果包含命令替换,会被拒绝。命令替换可能执行额外命令,风险很高。
数据流:输入是 rg 命令,拼接字符串里有 $(pwd) 或 $(echo ...) → 尝试解析 → 断言结果是 None。
调用关系:它验证 parse_plain_command_from_node 在处理 concatenation 时不会因为拼接而绕过安全检查。
调用图:外部调用 1 个(assert!)。
tests::parse_shell_lc_single_command_prefix_supports_heredoc507–523 ↗
fn parse_shell_lc_single_command_prefix_supports_heredoc()
作用:确认带 here-doc 的单命令脚本可以提取出命令前缀,比如只拿到 python3。这里同时测试带引号和不带引号的 here-doc 标记。
数据流:输入是 zsh -lc 加 python3 here-doc 脚本 → 调用 parse_shell_lc_single_command_prefix → 断言输出是 [python3]。
调用关系:它验证 parse_shell_lc_single_command_prefix 的主成功路径,包括识别 here-doc、找到唯一命令、提取命令参数。
调用图:调用 1 个内部函数(parse_shell_lc_single_command_prefix);外部调用 2 个(assert_eq!, vec!)。
tests::parse_shell_lc_single_command_prefix_rejects_multi_command_scripts526–533 ↗
fn parse_shell_lc_single_command_prefix_rejects_multi_command_scripts()
作用:确认 here-doc 后面如果还有第二条命令,不能被当成单命令前缀处理。
数据流:输入是 bash -lc,脚本先跑 python3 here-doc 再 echo done → 调用 parse_shell_lc_single_command_prefix → 断言返回 None。
调用关系:它验证 find_single_command_node 的限制:只要有多个 command 节点,特殊提取流程就必须失败。
调用图:外部调用 2 个(assert_eq!, vec!)。
tests::parse_shell_lc_single_command_prefix_rejects_non_heredoc_redirects536–543 ↗
fn parse_shell_lc_single_command_prefix_rejects_non_heredoc_redirects()
作用:确认普通文件重定向不会走 here-doc 单命令提取逻辑。写文件这类行为不能被忽略。
数据流:输入是「echo hello > /tmp/out.txt」 → 调用 parse_shell_lc_single_command_prefix → 断言返回 None。
调用关系:它验证 parse_shell_lc_single_command_prefix 必须看到 heredoc_redirect,普通 file_redirect 不够条件。
调用图:外部调用 2 个(assert_eq!, vec!)。
tests::parse_shell_lc_single_command_prefix_rejects_heredoc_with_extra_file_redirect546–553 ↗
fn parse_shell_lc_single_command_prefix_rejects_heredoc_with_extra_file_redirect()
作用:确认即使有 here-doc,只要同时还有额外文件重定向,也会被拒绝。因为这可能产生沙箱外文件写入。
数据流:输入是 python3 here-doc 后又重定向到 /tmp/out.txt → 调用 parse_shell_lc_single_command_prefix → 断言返回 None。
调用关系:它验证 has_named_descendant_kind 对 file_redirect 的检查,防止 here-doc 特例掩盖其他危险重定向。
调用图:外部调用 2 个(assert_eq!, vec!)。
tests::parse_shell_lc_single_command_prefix_rejects_heredoc_with_variable_assignment556–563 ↗
fn parse_shell_lc_single_command_prefix_rejects_heredoc_with_variable_assignment()
作用:确认 here-doc 命令前如果带环境变量赋值,会被拒绝。因为这会改变执行环境,不只是运行某个程序。
数据流:输入是 PATH=... cat <<EOF 的脚本 → 调用 parse_shell_lc_single_command_prefix → 断言返回 None。
调用关系:它验证 parse_heredoc_command_words 不接受 variable_assignment 这类非字面参数结构。
调用图:外部调用 2 个(assert_eq!, vec!)。
tests::parse_shell_lc_single_command_prefix_rejects_herestring_with_chaining566–573 ↗
fn parse_shell_lc_single_command_prefix_rejects_herestring_with_chaining()
作用:确认带命令串联和普通重定向的脚本不会被 here-doc 前缀提取误收。
数据流:输入是 echo 写文件再用 && 串 cat 的脚本 → 调用 parse_shell_lc_single_command_prefix → 断言返回 None。
调用关系:它从整体上验证特殊前缀提取流程不会把复杂脚本简化成一条看似安全的命令。
调用图:外部调用 2 个(assert_eq!, vec!)。
tests::parse_shell_lc_single_command_prefix_rejects_herestring_with_substitution576–583 ↗
fn parse_shell_lc_single_command_prefix_rejects_herestring_with_substitution()
作用:确认 here-string 里如果包含命令替换,会被拒绝。here-string 是把一小段字符串喂给命令的写法,也可能藏动态执行。
数据流:输入是 python3 <<< "$(rm -rf /)" → 调用 parse_shell_lc_single_command_prefix → 断言返回 None。
调用关系:它验证 parse_heredoc_command_words 和字面量检查不会允许带替换的输入结构混进去。
调用图:外部调用 2 个(assert_eq!, vec!)。
tests::parse_shell_lc_single_command_prefix_rejects_arithmetic_shift_non_heredoc_script586–593 ↗
fn parse_shell_lc_single_command_prefix_rejects_arithmetic_shift_non_heredoc_script()
作用:确认没有 here-doc 的算术展开脚本不会被这个特殊函数接受。
数据流:输入是「echo $((1<<2))」 → 调用 parse_shell_lc_single_command_prefix → 断言返回 None。
调用关系:它验证 parse_shell_lc_single_command_prefix 的入口条件:必须存在 heredoc_redirect,否则直接失败。
调用图:外部调用 2 个(assert_eq!, vec!)。
tests::parse_shell_lc_single_command_prefix_rejects_heredoc_command_with_word_expansion596–603 ↗
fn parse_shell_lc_single_command_prefix_rejects_heredoc_command_with_word_expansion()
作用:确认 here-doc 命令本身的参数里如果有算术展开,会被拒绝。即使输入部分是 here-doc,命令参数也必须是纯字面量。
数据流:输入是「python3 $((1<<2)) <<'PY' ...」 → 调用 parse_shell_lc_single_command_prefix → 断言返回 None。
调用关系:它验证 is_literal_word_or_number 在 parse_heredoc_command_words 中发挥作用,防止展开表达式被当成普通参数。
调用图:外部调用 2 个(assert_eq!, vec!)。
shell-command/src/powershell.rs源码 ↗
PowerShell 命令外面常包着一层壳,比如“powershell -Command 真正的脚本”。这个文件做的事,就是先看清这层壳是不是 PowerShell,再把里面的脚本拿出来,交给更懂 PowerShell 语法的解析器检查。它还会在脚本前加一小段设置,把控制台输出尽量改成 UTF-8(一种常见文字编码),这样中文或特殊符号不容易变成乱码。另外,它也会在系统路径里寻找 powershell.exe 或 pwsh.exe,并真的运行一条很小的测试命令,确认找到的程序能用。可以把它理解成 PowerShell 的“门卫和翻译”:先确认来的是谁,再取出真正的话,必要时还调整说话编码。
prefix_powershell_script_with_utf815–33 ↗
fn prefix_powershell_script_with_utf8(command: &[String]) -> Vec<String>
作用:给 PowerShell 的脚本前面加一段“请用 UTF-8 输出”的设置,减少命令输出乱码。只有确认这确实是一条 PowerShell 调用时才会改;不是的话原样返回。
数据流:输入是一串命令参数,比如 powershell、-Command、脚本内容。它先调用 extract_powershell_command 找出里面的脚本;如果找不到,就把原命令复制出去。如果找到了,就检查脚本开头是否已经有 UTF-8 设置,没有就拼上,最后返回一份新的命令参数列表,原输入不被直接修改。
调用关系:它在真正运行命令前被 run 调用,用来给 PowerShell 命令做输出编码保护。它把“识别 PowerShell 并取脚本”的活交给 extract_powershell_command;测试 prefixes_powershell_command_with_best_effort_utf8 会验证它确实加了前缀。
调用图:调用 1 个内部函数(extract_powershell_command);被 3 处调用(run, run, prefixes_powershell_command_with_best_effort_utf8);外部调用 1 个(format!)。
extract_powershell_command43–71 ↗
fn extract_powershell_command(command: &[String]) -> Option<(&str, &str)>
作用:判断一串命令参数是不是 PowerShell 调用,并取出 -Command 或 -c 后面的真实脚本。这样后续代码不用猜哪一段才是真正要执行的内容。
数据流:输入是命令参数列表。它先要求参数数量够多,再看第一个参数是不是 powershell 或 pwsh 这类 PowerShell 程序;接着从后面逐个看参数,只接受常见的 -NoLogo、-NoProfile、-Command、-c。遇到 -Command 或 -c 后,就返回“外层程序名”和“脚本正文”;如果格式不对或有陌生参数,就返回空。
调用关系:这是本文件的基础识别函数。prefix_powershell_script_with_utf8 用它找到脚本并加编码前缀;parse_powershell_command_into_plain_commands 用它先拆掉 PowerShell 外壳;canonicalize_command_for_approval 和 parse_command_impl 也会借它理解待审批或待解析的命令。多组测试覆盖了大小写参数、完整路径、pwsh 和 -c 简写。
调用图:被 8 处调用(canonicalize_command_for_approval, parse_command_impl, parse_powershell_command_into_plain_commands, prefix_powershell_script_with_utf8, extracts_basic_powershell_command, extracts_full_path_powershell_command, extracts_lowercase_flags, extracts_with_noprofile_and_alias);外部调用 1 个(matches!)。
parse_powershell_command_into_plain_commands78–83 ↗
fn parse_powershell_command_into_plain_commands(
command: &[String],
) -> Option<Vec<Vec<String>>>
作用:把一条外层 PowerShell 调用,拆成更像普通命令参数的形式,方便安全策略判断它到底想做什么。它只负责拆外壳,真正理解 PowerShell 脚本语法的工作交给专门解析器。
数据流:输入是一串命令参数。它先用 extract_powershell_command 取出 PowerShell 程序名和脚本正文;如果取不出来,就返回空。取出来后,把程序名和脚本交给 try_parse_powershell_ast_commands,让它按 PowerShell 的语法树解析,最后得到一组一组的普通命令参数。
调用关系:commands_for_exec_policy 会在判断执行策略时用它,把 PowerShell 包装命令转成可检查的普通命令。它前接 extract_powershell_command,后接 try_parse_powershell_ast_commands;Windows 上的两个测试验证了单条命令和管道里的多条命令都能被拆出来。
调用图:调用 1 个内部函数(extract_powershell_command);被 3 处调用(commands_for_exec_policy, parses_multiple_plain_powershell_commands, parses_plain_powershell_commands);外部调用 1 个(try_parse_powershell_ast_commands)。
try_find_powershell_executable_blocking86–88 ↗
fn try_find_powershell_executable_blocking() -> Option<AbsolutePathBuf>
作用:尝试在电脑上找到可用的 Windows PowerShell,也就是 powershell.exe。名字里的 blocking 表示它会同步等待查找和测试完成,调用者要等结果。
数据流:它没有外部输入。它把候选名字 powershell.exe 交给 try_find_powershellish_executable_in_path;后者会在系统 PATH(一组可执行程序搜索目录)里找,并验证能运行。成功时返回绝对路径,失败时返回空。
调用关系:它是测试和命令生成流程里获取 powershell.exe 的入口。它把实际搜索和可用性检查交给 try_find_powershellish_executable_in_path;相关测试会用它拿到真实程序路径,再验证生成的 PowerShell 命令能被安全检查识别。
调用图:调用 1 个内部函数(try_find_powershellish_executable_in_path);被 3 处调用(commands_generated_by_shell_command_handler_can_be_matched_by_is_known_safe_command, parser_process_handles_multiple_requests, parser_process_rejects_stop_parsing_forms)。
try_find_pwsh_executable_blocking100–122 ↗
fn try_find_pwsh_executable_blocking() -> Option<AbsolutePathBuf>
作用:尝试找到 PowerShell Core 的程序 pwsh.exe。pwsh.exe 不一定随系统自带,所以它比找 powershell.exe 多走一步:先问 pwsh 自己安装在哪里,再退回到 PATH 里找。
数据流:它没有外部输入。它先通过 cmd 运行 pwsh -NoProfile -Command $PSHOME,试图拿到 pwsh 的安装目录;如果拿到了,就在这个目录下拼出 pwsh.exe 的路径,并用 is_powershellish_executable_available 试运行确认可用。若这条路失败,就调用 try_find_powershellish_executable_in_path 在 PATH 里搜索。成功返回绝对路径,失败返回空。
调用关系:安全检查和 PowerShell 兼容性测试会用它找到 pwsh.exe。它自己负责先尝试“问安装目录”,再把普通搜索交给 try_find_powershellish_executable_in_path,把可执行性验证交给 is_powershellish_executable_available。
调用图:调用 3 个内部函数(is_powershellish_executable_available, try_find_powershellish_executable_in_path, resolve_path_against_base);被 7 处调用(commands_generated_by_shell_command_handler_can_be_matched_by_is_known_safe_command, windows_powershell_full_path_is_safe, accepts_full_path_powershell_invocations, allows_read_only_pipelines_and_git_usage, recognizes_safe_powershell_wrappers, rejects_git_global_override_options, uses_invoked_powershell_variant_for_parsing);外部调用 1 个(new)。
try_find_powershellish_executable_in_path124–142 ↗
fn try_find_powershellish_executable_in_path(candidates: &[&str]) -> Option<AbsolutePathBuf>
作用:在系统 PATH 里寻找指定名字的 PowerShell 类程序,并确认它真的能运行。这样不会因为找到了一个坏路径或不可执行文件就误以为可用。
数据流:输入是一组候选文件名,比如 powershell.exe 或 pwsh.exe。它逐个用 which 查找实际路径;找到后,用 is_powershellish_executable_available 跑一个小测试命令。测试通过后,再把路径转成项目要求的绝对路径类型并返回;所有候选都失败就返回空。
调用关系:try_find_powershell_executable_blocking 和 try_find_pwsh_executable_blocking 都把 PATH 搜索这件事交给它。它再调用 which 做路径查找,调用 is_powershellish_executable_available 做“能不能用”的最后确认。
调用图:调用 2 个内部函数(is_powershellish_executable_available, from_absolute_path);被 2 处调用(try_find_powershell_executable_blocking, try_find_pwsh_executable_blocking);外部调用 1 个(which)。
is_powershellish_executable_available144–151 ↗
fn is_powershellish_executable_available(powershell_or_pwsh_exe: &std::path::Path) -> bool
作用:检查某个 powershell.exe 或 pwsh.exe 路径是不是真的可用。它不是只看文件在不在,而是实际运行一条最简单的命令来验证。
数据流:输入是一个可执行文件路径。它启动这个程序,并传入 -NoLogo、-NoProfile、-Command、Write-Output ok 这些参数;如果进程成功结束,就返回 true,否则返回 false。启动失败也会当作不可用。
调用关系:这是查找 PowerShell 时的“验货员”。try_find_powershellish_executable_in_path 找到候选路径后会叫它验一下;try_find_pwsh_executable_blocking 在根据 $PSHOME 拼出 pwsh.exe 路径后也会先让它确认。
调用图:被 2 处调用(try_find_powershellish_executable_in_path, try_find_pwsh_executable_blocking);外部调用 1 个(new)。
tests::extracts_basic_powershell_command162–170 ↗
fn extracts_basic_powershell_command()
作用:测试最普通的 powershell -Command 脚本能被正确识别和取出。它保证基础用法不会被后续改动弄坏。
数据流:它构造一条包含 powershell、-Command、Write-Host hi 的参数列表,交给 extract_powershell_command。之后检查取出的脚本是不是正好等于 Write-Host hi;测试本身不改外部状态。
调用关系:它由测试运行器执行,直接覆盖 extract_powershell_command 的基本路径。这个测试像样例一样说明:只要外壳和 -Command 格式正确,函数就应该取到脚本正文。
调用图:调用 1 个内部函数(extract_powershell_command);外部调用 2 个(assert_eq!, vec!)。
tests::extracts_lowercase_flags173–182 ↗
fn extracts_lowercase_flags()
作用:测试小写参数,比如 -nologo 和 -command,也能被接受。这样用户或系统传参大小写不同,也不会导致识别失败。
数据流:它构造一条使用小写标志的 PowerShell 命令,传给 extract_powershell_command。函数返回后,测试检查脚本内容仍然是 Write-Host hi。
调用关系:它由测试运行器调用,用来保护 extract_powershell_command 的大小写兼容行为。它说明这个解析不是死盯着某一种大小写写法。
调用图:调用 1 个内部函数(extract_powershell_command);外部调用 2 个(assert_eq!, vec!)。
tests::extracts_full_path_powershell_command185–194 ↗
fn extracts_full_path_powershell_command()
作用:测试第一个参数是 PowerShell 的完整路径时也能识别,而不只识别 powershell 这个短名字。现实中程序经常会传完整路径,所以这很重要。
数据流:它根据当前系统构造一个 powershell.exe 的完整路径样例,再加上 -Command 和脚本内容。它把这串参数交给 extract_powershell_command,并检查脚本被正确取出。
调用关系:它由测试运行器执行,覆盖 extract_powershell_command 对完整路径的支持。它也间接说明识别 PowerShell 时会经过 shell 类型检测,而不是简单比较字符串。
调用图:调用 1 个内部函数(extract_powershell_command);外部调用 3 个(assert_eq!, cfg!, vec!)。
tests::extracts_with_noprofile_and_alias197–206 ↗
fn extracts_with_noprofile_and_alias()
作用:测试带 -NoProfile 参数、并使用 -c 这个 -Command 简写的 pwsh 命令也能被解析。这样更贴近日常命令行写法。
数据流:它构造 pwsh、-NoProfile、-c、脚本内容这组参数,传给 extract_powershell_command。然后检查取出的脚本是不是 Get-ChildItem | Select-String foo。
调用关系:它由测试运行器调用,专门保护 extract_powershell_command 对 pwsh、额外常见参数和 -c 简写的支持。
调用图:调用 1 个内部函数(extract_powershell_command);外部调用 2 个(assert_eq!, vec!)。
tests::prefixes_powershell_command_with_best_effort_utf8209–226 ↗
fn prefixes_powershell_command_with_best_effort_utf8()
作用:测试给 PowerShell 脚本自动加 UTF-8 输出设置这件事确实发生。它防止以后改代码时又让输出乱码问题回来。
数据流:它构造一条普通 powershell -Command 命令,传给 prefix_powershell_script_with_utf8。函数返回后,测试检查最后的脚本字符串前面已经多了 UTF8_OUTPUT_PREFIX,其他参数保持原样。
调用关系:它由测试运行器执行,直接验证 prefix_powershell_script_with_utf8 的主要行为。这个测试依赖 extract_powershell_command 能先识别出脚本位置。
调用图:调用 1 个内部函数(prefix_powershell_script_with_utf8);外部调用 2 个(assert_eq!, vec!)。
tests::does_not_duplicate_utf8_prefix229–237 ↗
fn does_not_duplicate_utf8_prefix()
作用:测试如果脚本开头已经有 UTF-8 输出设置,就不要重复加第二遍。这样生成的脚本不会越来越臃肿,也避免重复设置带来困惑。
数据流:它构造一条脚本已经带 UTF8_OUTPUT_PREFIX 的 PowerShell 命令,然后检查处理后的结果仍然和原命令一样。输入是一份参数列表,输出预期是不发生额外变化。
调用关系:它由测试运行器执行,用来保护“不要重复加前缀”这个细节。它和 prefixes_powershell_command_with_best_effort_utf8 一起覆盖了加前缀和不该加前缀两种情况。
调用图:外部调用 2 个(assert_eq!, vec!)。
tests::parses_plain_powershell_commands241–251 ↗
fn parses_plain_powershell_commands()
作用:在 Windows 上测试一条简单 PowerShell 命令能被拆成普通命令参数。它确认外层 powershell.exe 包装不会挡住安全解析。
数据流:它输入 powershell.exe、-NoProfile、-Command、echo hi 这组参数,调用 parse_powershell_command_into_plain_commands。返回结果应该是一组命令:echo 和 hi 两个参数。
调用关系:它只在 Windows 测试环境启用,由测试运行器调用。它验证 parse_powershell_command_into_plain_commands 能先用 extract_powershell_command 拆外壳,再交给 PowerShell AST 解析器得到普通命令。
调用图:调用 1 个内部函数(parse_powershell_command_into_plain_commands);外部调用 1 个(assert_eq!)。
tests::parses_multiple_plain_powershell_commands255–271 ↗
fn parses_multiple_plain_powershell_commands()
作用:在 Windows 上测试带管道的 PowerShell 脚本能被拆成多条普通命令。管道就是把前一个命令的输出交给后一个命令,像流水线一样。
数据流:它输入一条包含 Write-Output foo | Measure-Object 的 PowerShell 调用,交给 parse_powershell_command_into_plain_commands。预期输出是两组命令参数:一组是 Write-Output foo,另一组是 Measure-Object。
调用关系:它由 Windows 上的测试运行器执行,覆盖 parse_powershell_command_into_plain_commands 对多段 PowerShell 管道的解析能力。它保证安全策略看到的不只是一整串脚本文字,而是能分辨出里面的每个命令。
调用图:调用 1 个内部函数(parse_powershell_command_into_plain_commands);外部调用 1 个(assert_eq!)。
core/src/command_canonicalization.rs源码 ↗
在执行命令前,系统通常要看用户是否已经批准过类似命令。问题是,同一件事可能有很多外包装,比如 /bin/bash -lc "git status" 和 bash -lc "git status",看起来不一样,实际都是在跑 git status。这个文件做的事就是把这些外包装尽量拆掉,留下更容易比较的核心命令。它会先尝试识别简单的 shell 命令,如果能安全拆成一个普通命令,就直接返回这个命令本身。要是脚本比较复杂,没法可靠拆开,它就保留完整脚本文本,并加上一个特殊前缀,像给它贴标签一样,说明“这是 bash 脚本”或“这是 PowerShell 脚本”。如果完全认不出来,就原样返回。这样既能减少重复审批,又不会把复杂脚本误拆导致安全判断出错。
canonicalize_command_for_approval14–38 ↗
fn canonicalize_command_for_approval(command: &[String]) -> Vec<String>
作用:这个函数把一组命令参数变成适合做审批缓存匹配的标准形式。别人会用它来判断“这个命令是不是和以前批准过的命令本质一样”。
数据流:进去的是一串命令参数,比如 shell 程序名、选项、脚本文本。它先让 parse_shell_lc_plain_commands 尝试从 bash -lc 这类简单包装里拆出真正的单条命令;如果成功,就直接输出那条命令。否则它再用 extract_bash_command 或 extract_powershell_command 识别 bash 或 PowerShell 脚本;识别到后,会输出带特殊前缀的新参数列表,保留脚本文本。三个办法都不行时,它输出原始命令的拷贝,不改动输入本身。
调用关系:它处在命令审批流程的前面,像一个“统一口径”的入口。它自己不执行命令,也不决定是否批准,只是把命令整理好,交给后面的审批缓存去比较。整理时,它把具体识别工作交给 parse_shell_lc_plain_commands、extract_bash_command 和 extract_powershell_command;最后用新建的数组返回标准化结果。
调用图:调用 3 个内部函数(extract_bash_command, parse_shell_lc_plain_commands, extract_powershell_command);外部调用 1 个(vec!)。
shell-command/src/parse_command.rs源码 ↗
这个文件像一个“命令翻译员”。输入是一组命令参数,比如 bash -lc 'rg TODO src | head',它会尽量看出真正有意义的部分:rg TODO src 是搜索,cat README.md 是读文件,ls src 是列目录。它会处理 bash、zsh、PowerShell 包起来的命令,也会把管道里的 head、wc、sed 这类只负责排版的小工具尽量忽略掉,把重点放在主命令上。它还会记住前面的 cd,这样 cd foo && cat bar.txt 会被理解成读取 foo/bar.txt。解析不确定时,它会保守地返回 Unknown,避免误导用户;如果一串命令里有任何看不懂的危险部分,也会整体退回“未知”。文件里大量测试直接放在解析函数后面,是因为这种解析规则细碎,靠例子保护最可靠。
shlex_join10–13 ↗
fn shlex_join(tokens: &[String]) -> String
作用:把拆开的命令词重新拼成一条适合显示的命令字符串。它会尽量保留引号,让用户看到的命令不容易误解。
数据流:进去的是一组字符串参数 → 它交给 shlex 的 join 工具按 shell 习惯加空格和引号 → 出来是一条命令文本;如果参数里有不能正常显示的 NUL 字节,就返回一个提示占位文本。
调用关系:很多展示命令的地方会用它,比如审批、执行开始和结束事件、守护进程事件处理等;本文件内部的未知命令兜底和多种解析器也靠它生成可读的 cmd 字段。
调用图:被 16 处调用(build_command_execution_approval_request_item, build_command_execution_begin_item, build_command_execution_end_item, build_item_from_guardian_event, apply_bespoke_event_handling, command_assessment_action, handle_call, prompt, execve_permission_request_hook_short_circuits_prompt, parse_grep_like (+6 more));外部调用 1 个(try_join)。
extract_shell_command16–18 ↗
fn extract_shell_command(command: &[String]) -> Option<(&str, &str)>
作用:从外层 shell 命令里抠出真正要跑的脚本。比如 bash -lc 'cat a' 或 powershell -Command 'Get-ChildItem',重点是后面的脚本。
数据流:进去的是命令参数数组 → 先尝试按 bash/zsh 形式提取,再尝试按 PowerShell 形式提取 → 出来是 shell 名称和脚本文本,提不出来就返回空。
调用关系:single_unknown_for_command 用它让 Unknown 显示内层脚本而不是外层 shell;strip_bash_lc_and_escape 也会用它做显示清理。
调用图:调用 1 个内部函数(extract_bash_command);被 2 处调用(single_unknown_for_command, strip_bash_lc_and_escape)。
parse_command30–48 ↗
fn parse_command(command: &[String]) -> Vec<ParsedCommand>
作用:这是对外最主要的入口:把任意命令解析成一个或多个 ParsedCommand 摘要。外部代码想知道命令大概在读、搜、列文件,通常就调用它。
数据流:进去的是命令参数数组 → 它先调用 parse_command_impl 做详细解析,再去掉连续重复摘要;如果结果里出现 Unknown,就保守地把整条命令压成一个 Unknown → 出来是给界面和审批流程看的摘要列表。
调用关系:它被命令审批、用户 shell 执行、统一执行流程、测试断言等调用;内部把脏活交给 parse_command_impl,最后用 single_unknown_for_command 做兜底。
调用图:调用 1 个内部函数(parse_command_impl);被 10 处调用(build_item_from_guardian_event, request_command_approval, execute_user_shell_command, shell, unified_exec, create_expected_elicitation_request_params, assert_parsed, supports_tail_n_last_lines, exec_end_without_begin_uses_event_command, begin_exec_with_source);外部调用 2 个(with_capacity, vec!)。
single_unknown_for_command50–60 ↗
fn single_unknown_for_command(command: &[String]) -> ParsedCommand
作用:在解析失败或不安全时,生成一个“我看不懂这条命令”的摘要。它保证用户至少能看到原始命令文本。
数据流:进去的是命令参数数组 → 如果能剥掉 bash 或 PowerShell 外壳,就显示内层脚本;否则用 shlex_join 拼整条命令 → 出来是 ParsedCommand::Unknown。
调用关系:parse_command 在发现任何未知片段时会用这个思路兜底;它依赖 extract_shell_command 和 shlex_join。
调用图:调用 2 个内部函数(extract_shell_command, shlex_join)。
tests::shlex_split_safe71–73 ↗
fn shlex_split_safe(s: &str) -> Vec<String>
作用:测试用的小帮手,把一整条命令字符串拆成参数数组。拆不动时就退回按空白分割,避免测试直接崩掉。
数据流:进去是一条命令字符串 → 先用 shlex_split 按 shell 引号规则拆;失败就用空格粗略拆 → 出来是 Vec<String>。
调用关系:大量单元测试用它准备输入,再交给 assert_parsed 或 parse_command。
调用图:外部调用 1 个(split)。
tests::vec_str75–77 ↗
fn vec_str(args: &[&str]) -> Vec<String>
作用:测试用的小帮手,把字符串切片快速变成 Vec<String>。这样测试用例写起来更短。
数据流:进去是一组 &str → 每个都复制成 String → 出来是 Vec<String>。
调用关系:许多测试用它构造命令参数,然后交给 assert_parsed。
tests::assert_parsed79–82 ↗
fn assert_parsed(args: &[String], expected: Vec<ParsedCommand>)
作用:测试用的统一断言:检查某条命令解析结果是不是预期。它让每个测试只关心输入和期待输出。
数据流:进去是命令参数和预期 ParsedCommand 列表 → 调用 parse_command 得到实际结果 → 用 assert_eq 比较,不一致就让测试失败。
调用关系:几乎所有解析规则测试都通过它验证 parse_command。
调用图:调用 1 个内部函数(parse_command);外部调用 1 个(assert_eq!)。
tests::git_status_is_unknown85–92 ↗
fn git_status_is_unknown()
作用:确认普通 git status 不会被误判成搜索或读文件。它应该显示为未知操作。
数据流:进去是测试里写死的 git status → 交给 assert_parsed → 期待输出 Unknown。
调用关系:这是 parse_command 的保守策略测试之一,防止 summarize_main_tokens 过度解释 git 命令。
调用图:外部调用 3 个(assert_parsed, vec_str, vec!)。
tests::supports_git_grep_and_ls_files95–133 ↗
fn supports_git_grep_and_ls_files()
作用:确认 git grep 会被识别为搜索,git ls-files 会被识别为列文件。这样常见 Git 查询能在界面里显示得更友好。
数据流:进去是多条 git grep 和 git ls-files 示例 → 拆词后交给 assert_parsed → 期待 Search 或 ListFiles,并带上查询词和路径。
调用关系:覆盖 summarize_main_tokens 里 git 分支和 parse_grep_like 的行为。
调用图:外部调用 3 个(assert_parsed, shlex_split_safe, vec!)。
tests::handles_git_pipe_wc136–144 ↗
fn handles_git_pipe_wc()
作用:确认 git status | wc -l 这种管道不会被强行解释。因为它的真实意图不够明确。
数据流:进去是 bash -lc 包住的管道命令 → 解析后 → 期待整段脚本是 Unknown。
调用关系:测试 parse_shell_lc_commands、parse_shell_script 和未知管道兜底。
调用图:外部调用 3 个(assert_parsed, vec_str, vec!)。
tests::bash_lc_redirect_not_quoted147–155 ↗
fn bash_lc_redirect_not_quoted()
作用:确认带重定向的 echo foo > bar 不会被错误改写。重定向会改文件,解析器应谨慎。
数据流:进去是 bash -lc 'echo foo > bar' → 解析后 → 输出 Unknown,cmd 保留原脚本。
调用关系:保护 shell 包装解析和 Unknown 显示逻辑。
调用图:外部调用 3 个(assert_parsed, vec_str, vec!)。
tests::handles_complex_bash_command_head158–167 ↗
fn handles_complex_bash_command_head()
作用:确认一长串版本检查和统计命令不会被拆成误导性的简单摘要。复杂命令宁可显示 Unknown。
数据流:进去是多个命令用 && 和管道串起来的脚本 → parse_command 处理 → 期待 Unknown。
调用关系:覆盖 parse_shell_script 对复杂、多阶段脚本的保守处理。
调用图:外部调用 3 个(assert_parsed, vec_str, vec!)。
tests::handles_complex_bash_command184–194 ↗
fn handles_complex_bash_command()
作用:确认 rg 搜索后接 head 限制输出时,摘要聚焦在搜索本身。head 只是截取显示,不应抢主角。
数据流:进去是 rg ... | head -n 200 → 解析器丢掉小格式化命令 head → 输出 Search。
调用关系:覆盖 parse_shell_script、drop_small_formatting_commands 和 is_small_formatting_command。
调用图:外部调用 3 个(assert_parsed, vec_str, vec!)。
tests::supports_rg_files_with_path_and_pipe197–206 ↗
fn supports_rg_files_with_path_and_pipe()
作用:确认 rg --files 后面接 sed 时,仍能识别为列文件。sed 只是后处理输出。
数据流:进去是 rg --files webview/src | sed -n → 过滤 sed → 输出 ListFiles,path 显示为 webview。
调用关系:覆盖 rg --files 分支、短路径显示和管道过滤。
调用图:外部调用 3 个(assert_parsed, vec_str, vec!)。
tests::supports_rg_files_then_head209–218 ↗
fn supports_rg_files_then_head()
作用:确认 rg --files | head -n 50 会被理解成列文件。head 只是限制结果数量。
数据流:进去是 bash -lc 的 rg --files 管道 → 解析并丢掉 head → 输出 ListFiles。
调用关系:测试 parse_shell_script 与 is_small_formatting_command 对 head 的配合。
调用图:外部调用 3 个(assert_parsed, vec_str, vec!)。
tests::keeps_mutating_xargs_pipeline221–229 ↗
fn keeps_mutating_xargs_pipeline()
作用:确认带 xargs perl -pi 替换文件内容的管道不会被简化成普通搜索。因为这类命令会改文件,必须保守。
数据流:进去是 rg -l ... | xargs perl -pi -e ... → 解析器识别 xargs 后面的 perl 原地修改 → 输出 Unknown。
调用关系:覆盖 is_small_formatting_command、is_mutating_xargs_command 和 xargs 修改检测。
调用图:外部调用 3 个(assert_parsed, vec_str, vec!)。
tests::collapses_plain_pipeline_when_any_stage_is_unknown232–242 ↗
fn collapses_plain_pipeline_when_any_stage_is_unknown()
作用:确认普通参数形式的管道里只要有未知阶段,就整体变 Unknown。这样不会把危险后半段藏起来。
数据流:进去是已拆词的 rg | xargs perl 管道 → parse_command 发现未知 → 输出整条命令的 Unknown。
调用关系:测试 parse_command 最外层“有 Unknown 就整体兜底”的规则。
调用图:外部调用 3 个(assert_parsed, shlex_split_safe, vec!)。
tests::collapses_pipeline_with_helper_when_later_stage_is_unknown245–253 ↗
fn collapses_pipeline_with_helper_when_later_stage_is_unknown()
作用:确认前面是可识别命令、后面出现未知命令时,仍要整体未知。不能只展示前面的安全部分。
数据流:进去是 rg --files | nl -ba | foo → foo 不认识 → parse_command 输出整条 Unknown。
调用关系:覆盖 parse_command 的安全兜底和 simplify_once 对 nl 的简化边界。
调用图:外部调用 3 个(assert_parsed, shlex_split_safe, vec!)。
tests::rg_files_with_matches_flags_are_search256–297 ↗
fn rg_files_with_matches_flags_are_search()
作用:确认 rg -l、--files-with-matches、-L 等是搜索,不是列文件。虽然它们输出文件名,本质是在按内容匹配。
数据流:进去是多条 rg/rga 匹配命令 → 解析 → 输出 Search,并保留查询词和路径。
调用关系:保护 summarize_main_tokens 的 rg/rga 搜索判断。
调用图:外部调用 3 个(assert_parsed, shlex_split_safe, vec!)。
tests::supports_cat300–310 ↗
fn supports_cat()
作用:确认 cat 文件会被识别为读文件。用户能看到它读取的是哪个文件。
数据流:进去是 bash -lc 'cat webview/README.md' → 解析 → 输出 Read,name 是 README.md,path 是完整相对路径。
调用关系:覆盖 shell 提取和 summarize_main_tokens 的 cat 分支。
调用图:外部调用 3 个(assert_parsed, vec_str, vec!)。
tests::zsh_lc_supports_cat313–323 ↗
fn zsh_lc_supports_cat()
作用:确认 zsh -lc 包住的 cat 也能解析。不同 shell 外壳不应影响摘要。
数据流:进去是 zsh -lc 'cat README.md' → 剥掉 zsh 外壳 → 输出 Read。
调用关系:覆盖 extract_bash_command 支持 zsh 以及 parse_shell_script。
调用图:外部调用 3 个(assert_parsed, vec_str, vec!)。
tests::supports_bat326–336 ↗
fn supports_bat()
作用:确认 bat 这个文件查看器会被识别为读文件,即使带主题参数。
数据流:进去是 bat --theme TwoDark README.md → 跳过 theme 的值 → 输出 Read README.md。
调用关系:覆盖 summarize_main_tokens 的 bat 分支和 single_non_flag_operand。
调用图:外部调用 3 个(assert_parsed, vec_str, vec!)。
tests::supports_batcat339–349 ↗
fn supports_batcat()
作用:确认 batcat 也按读文件处理。不同系统上 bat 可能叫 batcat。
数据流:进去是 batcat README.md → 解析 → 输出 Read。
调用关系:覆盖 summarize_main_tokens 对 batcat 的别名支持。
调用图:外部调用 3 个(assert_parsed, vec_str, vec!)。
tests::supports_less352–362 ↗
fn supports_less()
作用:确认 less 查看文件会被识别为读文件,搜索参数不应被当成文件名。
数据流:进去是 less -p TODO README.md → 跳过 -p 的搜索词 → 输出 Read README.md。
调用关系:覆盖 less 分支和带值选项的跳过逻辑。
调用图:外部调用 3 个(assert_parsed, vec_str, vec!)。
tests::supports_more365–375 ↗
fn supports_more()
作用:确认 more README.md 会被识别为读文件。more 和 less 一样是查看内容的工具。
数据流:进去是 more README.md → 解析唯一文件参数 → 输出 Read。
调用关系:覆盖 summarize_main_tokens 的 more 分支。
调用图:外部调用 3 个(assert_parsed, vec_str, vec!)。
tests::cd_then_cat_is_single_read378–387 ↗
fn cd_then_cat_is_single_read()
作用:确认 cd foo && cat foo.txt 会显示为读取 foo/foo.txt。解析器要记住工作目录变化。
数据流:进去是 cd 后接 cat → 记录 cwd 为 foo → 输出 Read,路径拼上 foo。
调用关系:覆盖 parse_command_impl 的 cd_target 和 join_paths 配合。
调用图:外部调用 3 个(assert_parsed, shlex_split_safe, vec!)。
tests::cd_with_double_dash_then_cat_is_read390–399 ↗
fn cd_with_double_dash_then_cat_is_read()
作用:确认 cd -- -weird 这种奇怪目录名也能处理。-- 后面的 -weird 是目录,不是选项。
数据流:进去是 cd -- -weird && cat foo.txt → cd_target 取 -weird → 输出路径 -weird/foo.txt。
调用关系:覆盖 cd_target 对 -- 的处理。
调用图:外部调用 3 个(assert_parsed, shlex_split_safe, vec!)。
tests::cd_with_multiple_operands_uses_last402–411 ↗
fn cd_with_multiple_operands_uses_last()
作用:确认 cd 后有多个非选项参数时使用最后一个。这个测试固定当前解析器的兼容行为。
数据流:进去是 cd dir1 dir2 && cat foo.txt → cd_target 记住最后的 dir2 → 输出 dir2/foo.txt。
调用关系:覆盖 cd_target 的参数扫描规则。
调用图:外部调用 3 个(assert_parsed, shlex_split_safe, vec!)。
tests::bash_cd_then_bar_is_same_as_bar414–422 ↗
fn bash_cd_then_bar_is_same_as_bar()
作用:确认 bash 脚本里 cd 后接未知命令时,不会把它误判成某个可识别操作。未知仍然未知。
数据流:进去是 bash -lc 'cd foo && bar' → shell 解析后无法识别 bar → 输出整段 Unknown。
调用关系:覆盖 parse_shell_script 的 cd 简化和 Unknown 兜底。
调用图:外部调用 3 个(assert_parsed, shlex_split_safe, vec!)。
tests::bash_cd_then_cat_is_read425–434 ↗
fn bash_cd_then_cat_is_read()
作用:确认 bash -lc 中的 cd 也会影响后面读文件的路径。外层 shell 不应丢失目录信息。
数据流:进去是 bash -lc 'cd foo && cat foo.txt' → 记录 cwd → 输出 Read foo/foo.txt。
调用关系:覆盖 parse_shell_lc_commands、parse_shell_script、cd_target 和 join_paths。
调用图:外部调用 3 个(assert_parsed, shlex_split_safe, vec!)。
tests::supports_ls_with_pipe437–446 ↗
fn supports_ls_with_pipe()
作用:确认 ls 接 sed 后仍识别为列文件。sed 是修饰输出,不是主要目的。
数据流:进去是 ls -la | sed -n ... → 丢掉 sed → 输出 ListFiles,cmd 是 ls -la。
调用关系:覆盖 ls 分支和小格式化命令过滤。
调用图:外部调用 3 个(assert_parsed, vec_str, vec!)。
tests::supports_eza_exa_tree_du449–478 ↗
fn supports_eza_exa_tree_du()
作用:确认 eza、exa、tree、du 这些查看目录结构或大小的命令会被当成列文件。它们都在帮助用户看文件列表或目录概况。
数据流:进去是四类命令示例 → 跳过各自选项 → 输出 ListFiles,并提取路径。
调用关系:覆盖 summarize_main_tokens 中 eza/exa/tree/du 的分支。
调用图:外部调用 3 个(assert_parsed, shlex_split_safe, vec!)。
tests::supports_head_n481–491 ↗
fn supports_head_n()
作用:确认 head -n 50 Cargo.toml 会被识别为读文件。虽然只读前几行,本质仍是看文件。
数据流:进去是 head -n 50 Cargo.toml → 跳过行数参数 → 输出 Read Cargo.toml。
调用关系:覆盖 summarize_main_tokens 的 head 分支。
调用图:外部调用 3 个(assert_parsed, vec_str, vec!)。
tests::supports_head_file_only494–504 ↗
fn supports_head_file_only()
作用:确认 head Cargo.toml 也会被识别为读文件。没有 -n 时默认读文件开头。
数据流:进去是 head Cargo.toml → 找到唯一文件参数 → 输出 Read。
调用关系:覆盖 head 简单文件形式。
调用图:外部调用 3 个(assert_parsed, vec_str, vec!)。
tests::supports_cat_sed_n507–517 ↗
fn supports_cat_sed_n()
作用:确认 cat 文件后接 sed 截取行时,仍显示读这个文件。管道后处理不改变主意图。
数据流:进去是 cat tui/Cargo.toml | sed -n ... → 主命令识别为 cat → 输出 Read tui/Cargo.toml。
调用关系:覆盖 parse_shell_script 对管道读文件场景的处理。
调用图:外部调用 3 个(assert_parsed, vec_str, vec!)。
tests::supports_tail_n_plus520–530 ↗
fn supports_tail_n_plus()
作用:确认 tail -n +522 README.md 会被识别为读文件。+522 表示从某行开始读。
数据流:进去是 tail -n +522 README.md → 验证行号参数 → 输出 Read。
调用关系:覆盖 tail 分支对 +数字形式的支持。
调用图:外部调用 3 个(assert_parsed, vec_str, vec!)。
tests::supports_tail_n_last_lines533–544 ↗
fn supports_tail_n_last_lines()
作用:确认 tail -n 30 README.md 会被识别为读文件。读取最后几行也属于看文件。
数据流:进去是 bash -lc 包住的 tail 命令 → 直接调用 parse_command → 比较输出 Read。
调用关系:这个测试直接覆盖 parse_command 和 tail 分支。
调用图:调用 1 个内部函数(parse_command);外部调用 2 个(assert_eq!, vec_str)。
tests::supports_tail_file_only547–557 ↗
fn supports_tail_file_only()
作用:确认 tail README.md 也会被识别为读文件。没有参数时默认读文件末尾。
数据流:进去是 tail README.md → 找到文件名 → 输出 Read。
调用关系:覆盖 tail 的简单文件形式。
调用图:外部调用 3 个(assert_parsed, vec_str, vec!)。
tests::supports_npm_run_build_is_unknown560–567 ↗
fn supports_npm_run_build_is_unknown()
作用:确认 npm run build 不被随便解释。构建命令可能做很多事,摘要应保守。
数据流:进去是 npm run build → 没有匹配已知读搜列规则 → 输出 Unknown。
调用关系:覆盖 summarize_main_tokens 默认 Unknown 分支。
调用图:外部调用 3 个(assert_parsed, vec_str, vec!)。
tests::supports_grep_recursive_current_dir570–579 ↗
fn supports_grep_recursive_current_dir()
作用:确认 grep -R 在当前目录搜索会被识别为搜索。它要显示查询词和路径。
数据流:进去是 grep -R CODEX_SANDBOX_ENV_VAR -n . → parse_grep_like 提取模式和路径 → 输出 Search。
调用关系:覆盖 grep 分支和 parse_grep_like。
调用图:外部调用 3 个(assert_parsed, vec_str, vec!)。
tests::supports_grep_recursive_specific_file582–597 ↗
fn supports_grep_recursive_specific_file()
作用:确认 grep 搜具体文件时路径显示得简洁。完整路径会缩成有用的文件名。
数据流:进去是 grep -R 查询词 -n core/src/spawn.rs → 提取 query,path 缩成 spawn.rs → 输出 Search。
调用关系:覆盖 parse_grep_like 与 short_display_path 的配合。
调用图:外部调用 3 个(assert_parsed, vec_str, vec!)。
tests::supports_egrep_and_fgrep600–617 ↗
fn supports_egrep_and_fgrep()
作用:确认 egrep 和 fgrep 也按 grep 类搜索处理。它们是 grep 的常见变体。
数据流:进去是 egrep/fgrep 示例 → 解析查询词和路径 → 输出 Search。
调用关系:覆盖 summarize_main_tokens 对 grep 变体的支持。
调用图:外部调用 3 个(assert_parsed, shlex_split_safe, vec!)。
tests::grep_files_with_matches_flags_are_search620–653 ↗
fn grep_files_with_matches_flags_are_search()
作用:确认 grep -l、-L 等输出匹配文件名的形式仍算搜索。因为它们依赖内容匹配。
数据流:进去是多条 grep 匹配文件命令 → parse_grep_like 提取 query/path → 输出 Search。
调用关系:保护 grep 分支不会把这类命令误判成 ListFiles。
调用图:外部调用 3 个(assert_parsed, shlex_split_safe, vec!)。
tests::supports_grep_query_with_slashes_not_shortened656–667 ↗
fn supports_grep_query_with_slashes_not_shortened()
作用:确认 grep 的查询词里有斜杠时不会被当成路径缩短。模式 src/main.rs 必须原样保留。
数据流:进去是 grep -R src/main.rs -n . → 查询词保持 src/main.rs → 路径是 .。
调用关系:覆盖 parse_grep_like 中“只缩短路径,不缩短查询词”的规则。
调用图:外部调用 3 个(assert_parsed, shlex_split_safe, vec!)。
tests::supports_grep_weird_backtick_in_query670–679 ↗
fn supports_grep_weird_backtick_in_query()
作用:确认查询词里有反引号时仍能安全显示。显示时需要正确加引号。
数据流:进去是 grep -R COD`EX_SANDBOX -n → shlex_join 给特殊字符加引号 → 输出 Search。
调用关系:覆盖 parse_grep_like 和 shlex_join 的组合。
调用图:外部调用 3 个(assert_parsed, shlex_split_safe, vec!)。
tests::supports_cd_and_rg_files682–690 ↗
fn supports_cd_and_rg_files()
作用:确认 cd 后接 rg --files 会识别为列文件。这里 cd 只改变运行目录,不需要额外显示成命令。
数据流:进去是 cd codex-rs && rg --files → 记录 cwd 后解析 rg → 输出 ListFiles。
调用关系:覆盖 parse_command_impl 的 cd 跳过和 rg --files 分支。
调用图:外部调用 3 个(assert_parsed, shlex_split_safe, vec!)。
tests::supports_single_string_script_with_cd_and_pipe693–703 ↗
fn supports_single_string_script_with_cd_and_pipe()
作用:确认一整段 bash 脚本里先 cd 再 rg 搜索再 head,仍能提取搜索摘要。常见自动化命令会这样写。
数据流:进去是 bash -lc 单字符串脚本 → 解析 shell 语法、跳过 cd 和 head → 输出 Search。
调用关系:覆盖 parse_shell_script、cd_target、drop_small_formatting_commands 和 rg 搜索分支。
调用图:外部调用 3 个(assert_parsed, vec_str, vec!)。
tests::supports_python_walks_files706–715 ↗
fn supports_python_walks_files()
作用:确认 python -c 里调用 os.listdir 会被识别为列文件。即使命令不是 ls,也是在看文件列表。
数据流:进去是 python -c "import os; print(os.listdir('.'))" → python_walks_files 发现 os.listdir → 输出 ListFiles。
调用关系:覆盖 is_python_command 和 python_walks_files。
调用图:外部调用 3 个(assert_parsed, vec_str, vec!)。
tests::supports_python3_walks_files718–727 ↗
fn supports_python3_walks_files()
作用:确认 python3 -c 里调用 glob.glob 也会被识别为列文件。glob 是按模式找文件。
数据流:进去是 python3 -c 使用 glob.glob → 检测到文件遍历关键词 → 输出 ListFiles。
调用关系:覆盖 python3 命令名和 python_walks_files 的关键词列表。
调用图:外部调用 3 个(assert_parsed, vec_str, vec!)。
tests::python_without_file_walk_is_unknown730–738 ↗
fn python_without_file_walk_is_unknown()
作用:确认普通 python 打印 hello 不会误判为列文件。只有明显遍历文件的脚本才特殊处理。
数据流:进去是 python -c "print('hello')" → 没发现文件遍历关键词 → 输出 Unknown。
调用关系:保护 python_walks_files 的边界。
调用图:外部调用 3 个(assert_parsed, vec_str, vec!)。
tests::small_formatting_always_true_commands742–749 ↗
fn small_formatting_always_true_commands()
作用:确认 wc、tr、cut 等纯排版小工具在管道里会被丢掉。它们通常不是用户真正想做的事。
数据流:进去是一组命令名和带参数形式 → 调 is_small_formatting_command → 期待都为 true。
调用关系:直接测试 is_small_formatting_command 的固定名单。
调用图:外部调用 1 个(assert!)。
tests::awk_behavior752–762 ↗
fn awk_behavior()
作用:确认 awk 没有文件时算排版工具,有文件时算读文件。awk 既能处理管道,也能直接读文件。
数据流:进去是不同 awk 形式 → 检查 is_small_formatting_command → 有数据文件时期待 false。
调用关系:覆盖 is_small_formatting_command 和 awk_data_file_operand。
调用图:外部调用 1 个(assert!)。
tests::head_behavior765–778 ↗
fn head_behavior()
作用:确认 head 没有文件时算小格式化命令,有文件时算读文件候选。这样管道里的 head 会被忽略。
数据流:进去是 head 的多种参数形式 → 调 is_small_formatting_command → 根据是否有文件返回 true 或 false。
调用关系:覆盖 is_small_formatting_command 的 head 规则。
调用图:外部调用 1 个(assert!)。
tests::tail_behavior781–805 ↗
fn tail_behavior()
作用:确认 tail 和 head 类似:只有行数或字节数时算格式化,有文件时不是。这样不会丢掉真正的文件读取。
数据流:进去是 tail 的多种 -n/-c 形式 → 检查是否被认为小格式化命令 → 输出布尔断言结果。
调用关系:覆盖 is_small_formatting_command 的 tail 规则。
调用图:外部调用 1 个(assert!)。
tests::sed_behavior808–833 ↗
fn sed_behavior()
作用:确认 sed -n 合法行号加文件时不算小格式化,而普通 sed 算。因为 sed 可以是读文件,也可以只是处理管道。
数据流:进去是 sed 的多种范围和文件形式 → 调 is_small_formatting_command → 合法读文件形式返回 false。
调用关系:覆盖 is_small_formatting_command 与 sed_read_path、is_valid_sed_n_arg。
调用图:外部调用 1 个(assert!)。
tests::empty_tokens_is_not_small836–839 ↗
tests::supports_nl_then_sed_reading842–852 ↗
fn supports_nl_then_sed_reading()
作用:确认 nl 给文件加行号再接 sed 截取时,仍识别为读文件。nl 直接带文件时不是普通排版小工具。
数据流:进去是 nl -ba 文件 | sed -n 范围 → 解析 nl 的文件参数 → 输出 Read。
调用关系:覆盖 summarize_main_tokens 的 nl 分支和管道处理。
调用图:外部调用 3 个(assert_parsed, vec_str, vec!)。
tests::supports_sed_n855–865 ↗
fn supports_sed_n()
作用:确认 sed -n '范围p' 文件会被识别为读文件。很多人用它查看文件片段。
数据流:进去是 sed -n '2000,2200p' 文件 → sed_read_path 找到文件 → 输出 Read。
调用关系:覆盖 sed_read_path 和 sed 分支。
调用图:外部调用 3 个(assert_parsed, vec_str, vec!)。
tests::supports_awk_with_file868–878 ↗
fn supports_awk_with_file()
作用:确认 awk 脚本后跟文件时会被识别为读文件。它是在读取 Cargo.toml 的内容。
数据流:进去是 awk '{print $1}' Cargo.toml → awk_data_file_operand 取第二个非选项参数 → 输出 Read。
调用关系:覆盖 awk_data_file_operand 和 summarize_main_tokens 的 awk 分支。
调用图:外部调用 3 个(assert_parsed, vec_str, vec!)。
tests::filters_out_printf881–892 ↗
fn filters_out_printf()
作用:确认 printf 这种只打印标题的命令会被过滤,重点落在后面的 cat 文件上。这样摘要不被装饰文字干扰。
数据流:进去是 printf 标题 ; cat 文件 → 丢掉 printf → 输出 Read cat 的文件。
调用关系:覆盖 parse_shell_script、drop_small_formatting_commands 和 printf 小工具规则。
调用图:外部调用 3 个(assert_parsed, vec_str, vec!)。
tests::drops_yes_in_pipelines895–905 ↗
fn drops_yes_in_pipelines()
作用:确认 yes | rg --files 会聚焦 rg --files。yes 只是给后面命令喂输入,不是主操作。
数据流:进去是 yes 管道到 rg --files → 过滤 yes → 输出 ListFiles。
调用关系:覆盖 normalize_tokens 和 is_small_formatting_command 对 yes 的处理。
调用图:外部调用 3 个(assert_parsed, vec_str, vec!)。
tests::supports_sed_n_then_nl_as_search908–921 ↗
fn supports_sed_n_then_nl_as_search()
作用:确认 sed 读文件片段后接 nl 加行号,摘要仍是读文件。nl 不应让读文件信息丢失。
数据流:进去是 sed -n 范围 文件 | nl -ba → 解析 sed 为 Read,丢掉后面的 nl → 输出 Read。
调用关系:覆盖 sed_read_path、simplify_once 和管道摘要选择。
调用图:外部调用 3 个(assert_parsed, shlex_split_safe, vec!)。
tests::preserves_rg_with_spaces924–933 ↗
fn preserves_rg_with_spaces()
作用:确认 rg 搜索词里有空格时能保留完整查询词。显示命令时也要正确加引号。
数据流:进去是 yes | rg -n 'foo bar' -S → 丢掉 yes,解析 rg → 输出 query 为 foo bar。
调用关系:覆盖 shlex_split_safe、summarize_main_tokens 和 shlex_join。
调用图:外部调用 3 个(assert_parsed, shlex_split_safe, vec!)。
tests::ls_with_glob936–944 ↗
fn ls_with_glob()
作用:确认 ls 的 glob 排除参数不会被当成路径。比如 -I '*.test.js' 是选项值。
数据流:进去是 ls -I '*.test.js' → 跳过 -I 的值 → 输出 ListFiles,path 为空。
调用关系:覆盖 first_non_flag_operand 和 ls 的 flags_with_vals。
调用图:外部调用 3 个(assert_parsed, shlex_split_safe, vec!)。
tests::strips_true_in_sequence947–964 ↗
fn strips_true_in_sequence()
作用:确认 true 这种占位成功命令会被删掉。它不代表用户真正要查看什么。
数据流:进去是 true && rg --files 或 rg --files && true → simplify_once 删除 true → 输出 ListFiles。
调用关系:覆盖 parse_command_impl 和 simplify_once。
调用图:外部调用 3 个(assert_parsed, shlex_split_safe, vec!)。
tests::strips_true_inside_bash_lc967–985 ↗
fn strips_true_inside_bash_lc()
作用:确认 bash -lc 里面的 true 也会被删掉。shell 包装不应改变这个简化规则。
数据流:进去是 bash -lc 包住的 true/rg 组合 → parse_shell_script 删除 true → 输出 ListFiles。
调用关系:覆盖 parse_shell_lc_commands、parse_shell_script 和 simplify_once。
调用图:外部调用 3 个(assert_parsed, vec_str, vec!)。
tests::shorten_path_on_windows988–997 ↗
fn shorten_path_on_windows()
作用:确认 Windows 风格反斜杠路径也能取出文件名。跨平台显示要正常。
数据流:进去是 cat "pkg\src\main.rs" → short_display_path 统一处理分隔符 → 输出 name 为 main.rs。
调用关系:覆盖 cat 分支和 short_display_path 对反斜杠的处理。
调用图:外部调用 3 个(assert_parsed, shlex_split_safe, vec!)。
tests::head_with_no_space1000–1009 ↗
fn head_with_no_space()
作用:确认 head -n50 这种选项和值连在一起的写法能识别。用户常这样写。
数据流:进去是 bash -lc 'head -n50 Cargo.toml' → head 分支识别 -n50 → 输出 Read。
调用关系:覆盖 summarize_main_tokens 的 head 紧凑参数形式。
调用图:外部调用 3 个(assert_parsed, shlex_split_safe, vec!)。
tests::bash_dash_c_pipeline_parsing1012–1022 ↗
fn bash_dash_c_pipeline_parsing()
作用:确认 bash -c 和 bash -lc 一样能解析管道。两种都是让 bash 执行后面的脚本。
数据流:进去是 bash -c 'rg --files | head -n 1' → 剥外壳并丢掉 head → 输出 ListFiles。
调用关系:覆盖 extract_bash_command、parse_shell_lc_commands 和 parse_shell_script。
调用图:外部调用 3 个(assert_parsed, vec_str, vec!)。
tests::tail_with_no_space1025–1034 ↗
fn tail_with_no_space()
作用:确认 tail -n+10 这种紧凑写法能识别为读文件。+10 表示从第 10 行开始。
数据流:进去是 bash -lc 'tail -n+10 README.md' → tail 分支识别 -n+10 → 输出 Read。
调用关系:覆盖 summarize_main_tokens 的 tail 紧凑参数形式。
调用图:外部调用 3 个(assert_parsed, shlex_split_safe, vec!)。
tests::grep_with_query_and_path1037–1046 ↗
fn grep_with_query_and_path()
作用:确认 grep -R TODO src 能提取查询词和路径。这是最常见的搜索形式。
数据流:进去是 grep -R TODO src → parse_grep_like 跳过 -R → 输出 Search,query TODO,path src。
调用关系:覆盖 grep 分支和 parse_grep_like。
调用图:外部调用 3 个(assert_parsed, shlex_split_safe, vec!)。
tests::supports_ag_ack_pt_rga1049–1082 ↗
fn supports_ag_ack_pt_rga()
作用:确认 ag、ack、pt、rga 这些搜索工具也会被识别为搜索。它们和 rg/grep 类似。
数据流:进去是四种搜索工具示例 → 提取第一个非选项为查询词、第二个为路径 → 输出 Search。
调用关系:覆盖 summarize_main_tokens 的 ag/ack/pt 分支和 rga 分支。
调用图:外部调用 3 个(assert_parsed, shlex_split_safe, vec!)。
tests::ag_ack_pt_files_with_matches_flags_are_search1085–1110 ↗
fn ag_ack_pt_files_with_matches_flags_are_search()
作用:确认 ag/ack/pt 的 -l 形式仍是搜索。输出文件名不等于简单列目录。
数据流:进去是 ag/ack/pt -l TODO src → 跳过 -l → 输出 Search。
调用关系:保护 ag/ack/pt 解析规则。
调用图:外部调用 3 个(assert_parsed, shlex_split_safe, vec!)。
tests::rg_with_equals_style_flags1113–1122 ↗
fn rg_with_equals_style_flags()
作用:确认 --colors=never 这种等号形式选项会被跳过。它不是查询词也不是路径。
数据流:进去是 rg --colors=never -n foo src → skip_flag_values 忽略等号选项 → 输出 query foo,path src。
调用关系:覆盖 skip_flag_values 和 rg 分支。
调用图:外部调用 3 个(assert_parsed, shlex_split_safe, vec!)。
tests::cat_with_double_dash_and_sed_ranges1125–1145 ↗
fn cat_with_double_dash_and_sed_ranges()
作用:确认 cat -- 文件和 sed -n 范围 文件都能读到正确文件。-- 后面即使像选项也应当是文件名。
数据流:进去是两条命令 → cat 通过单位置参数取文件,sed 通过合法范围取文件 → 都输出 Read。
调用关系:覆盖 positional_operands 对 -- 的处理和 sed_read_path。
调用图:外部调用 3 个(assert_parsed, shlex_split_safe, vec!)。
tests::drop_trailing_nl_in_pipeline1148–1157 ↗
fn drop_trailing_nl_in_pipeline()
作用:确认管道末尾的 nl -ba 会被去掉。它只是加行号,不改变 rg --files 的主意图。
数据流:进去是 rg --files | nl -ba → simplify_once 或过滤逻辑移除 nl → 输出 ListFiles。
调用关系:覆盖 simplify_once 对 nl 只有选项时的简化。
调用图:外部调用 3 个(assert_parsed, shlex_split_safe, vec!)。
tests::ls_with_time_style_and_path1160–1169 ↗
fn ls_with_time_style_and_path()
作用:确认 ls --time-style=long-iso ./dist 能跳过等号选项并提取路径。dist 这类构建目录会被短路径规则特别处理。
数据流:进去是 ls 带 time-style 和 ./dist → 忽略等号选项,路径缩短 → 输出 ListFiles。
调用关系:覆盖 ls 分支、positional_operands 和 short_display_path。
调用图:外部调用 3 个(assert_parsed, shlex_split_safe, vec!)。
tests::fd_file_finder_variants1172–1190 ↗
fn fd_file_finder_variants()
作用:确认 fd 既能表示列文件,也能表示按名字搜索。fd -t f src/ 是列,fd main src 是搜 main。
数据流:进去是两条 fd 命令 → parse_fd_query_and_path 判断参数像路径还是查询词 → 输出 ListFiles 或 Search。
调用关系:覆盖 parse_fd_query_and_path、is_pathish 和 fd 分支。
调用图:外部调用 3 个(assert_parsed, shlex_split_safe, vec!)。
tests::find_basic_name_filter1193–1202 ↗
fn find_basic_name_filter()
作用:确认 find . -name '*.rs' 会被识别为搜索文件名。-name 后面的模式是查询条件。
数据流:进去是 find 命令 → parse_find_query_and_path 取根路径 . 和模式 *.rs → 输出 Search。
调用关系:覆盖 find 分支和 parse_find_query_and_path。
调用图:外部调用 3 个(assert_parsed, shlex_split_safe, vec!)。
tests::find_type_only_path1205–1213 ↗
fn find_type_only_path()
作用:确认 find src -type f 没有名字过滤时,会被识别为列文件。它是在列出 src 下的文件。
数据流:进去是 find src -type f → 找到根路径 src,没有 query → 输出 ListFiles。
调用关系:覆盖 parse_find_query_and_path 的无查询分支。
调用图:外部调用 3 个(assert_parsed, shlex_split_safe, vec!)。
tests::bin_bash_lc_sed1216–1225 ↗
fn bin_bash_lc_sed()
作用:确认 /bin/bash 这种带路径的 shell 也能被剥掉。不能只认识 bash 这个短名字。
数据流:进去是 /bin/bash -lc 包住 sed 读文件 → 提取内层脚本 → 输出 Read。
调用关系:覆盖 extract_bash_command 与 parse_shell_lc_commands。
调用图:外部调用 3 个(assert_parsed, shlex_split_safe, vec!)。
tests::bin_zsh_lc_sed1227–1236 ↗
fn bin_zsh_lc_sed()
作用:确认 /bin/zsh 这种带路径的 zsh 也能解析。zsh 外壳不影响 sed 读文件摘要。
数据流:进去是 /bin/zsh -lc 包住 sed → 提取并解析 → 输出 Read。
调用关系:覆盖 zsh 路径形式的 shell 提取。
调用图:外部调用 3 个(assert_parsed, shlex_split_safe, vec!)。
tests::powershell_command_is_stripped1239–1246 ↗
fn powershell_command_is_stripped()
作用:确认 PowerShell 的 -Command 外壳会被去掉。摘要里应显示真正执行的 Get-ChildItem。
数据流:进去是 powershell -Command Get-ChildItem → extract_powershell_command 提取脚本 → 输出 Unknown 脚本文本。
调用关系:覆盖 parse_command_impl 对 PowerShell 的特殊处理。
调用图:外部调用 3 个(assert_parsed, vec_str, vec!)。
tests::pwsh_with_noprofile_and_c_alias_is_stripped1249–1256 ↗
fn pwsh_with_noprofile_and_c_alias_is_stripped()
作用:确认 pwsh -NoProfile -c 形式也能剥掉外壳。-c 是 -Command 的常见短写。
数据流:进去是 pwsh -NoProfile -c Write-Host hi → 提取脚本 → 输出 Unknown。
调用关系:覆盖 PowerShell 提取函数和 parse_command_impl。
调用图:外部调用 3 个(assert_parsed, vec_str, vec!)。
tests::powershell_with_path_is_stripped1259–1272 ↗
fn powershell_with_path_is_stripped()
作用:确认带完整路径的 powershell.exe 也能识别。Windows 和非 Windows 测试路径都覆盖到。
数据流:进去是平台相关的 powershell.exe 路径加 -c 脚本 → 提取 Write-Host hi → 输出 Unknown。
调用关系:覆盖 extract_powershell_command 对可执行文件路径的支持。
调用图:外部调用 4 个(cfg!, assert_parsed, vec_str, vec!)。
parse_command_impl1275–1336 ↗
fn parse_command_impl(command: &[String]) -> Vec<ParsedCommand>
作用:这是解析工作的主发动机。它负责剥 shell、拆连接符、记住 cd、调用具体命令识别器,并做简化。
数据流:进去是命令参数数组 → 先处理 bash/zsh 和 PowerShell,再规范化 token;遇到 &&、||、|、; 就拆段;逐段识别并把 cd 影响加到读文件路径上;最后反复简化无意义片段 → 出来是 ParsedCommand 列表。
调用关系:parse_command 和 detect_skill_doc_read 会调用它;它把具体判断交给 summarize_main_tokens,把路径拼接交给 join_paths,把简化交给 simplify_once。
调用图:调用 9 个内部函数(cd_target, contains_connectors, join_paths, normalize_tokens, parse_shell_lc_commands, simplify_once, split_on_connectors, summarize_main_tokens, extract_powershell_command);被 2 处调用(detect_skill_doc_read, parse_command);外部调用 3 个(from, new, vec!)。
simplify_once1338–1394 ↗
fn simplify_once(commands: &[ParsedCommand]) -> Option<Vec<ParsedCommand>>
作用:对已经解析出的摘要做一次清理,删掉明显没信息量的命令。比如 echo、cd、true、纯 nl 等。
数据流:进去是一组 ParsedCommand → 按规则找一个可删除项并生成新列表 → 如果删不了就返回空;一次只简化一步。
调用关系:parse_command_impl 和 parse_shell_script 会循环调用它,直到没有东西可简化。
调用图:被 2 处调用(parse_command_impl, parse_shell_script);外部调用 4 个(with_capacity, iter, len, split)。
is_valid_sed_n_arg1397–1417 ↗
fn is_valid_sed_n_arg(arg: Option<&str>) -> bool
作用:判断 sed -n 的脚本是不是像 10p 或 1,20p 这种合法行号范围。只有这种形式才安全地当成“读文件片段”。
数据流:进去是可选字符串 → 检查是否以 p 结尾,前面是否是一段或两段纯数字 → 出来是真或假。
调用关系:sed_read_path 用它确认 sed 命令是不是读文件片段,而不是任意 sed 脚本。
调用图:被 1 处调用(sed_read_path)。
sed_read_path1419–1460 ↗
fn sed_read_path(args: &[String]) -> Option<String>
作用:从 sed -n '范围p' 文件 里找出被读取的文件路径。它让 sed 查看文件片段时能显示成 Read。
数据流:进去是 sed 后面的参数 → 截断连接符之后的内容,确认有 -n 和合法范围,跳过 -e/-f 等选项值 → 出来是文件路径,找不到就返回空。
调用关系:summarize_main_tokens 用它解析 sed;is_small_formatting_command 也用它判断 sed 是读文件还是管道小工具。
调用图:调用 3 个内部函数(is_valid_sed_n_arg, skip_flag_values, trim_at_connector);被 2 处调用(is_small_formatting_command, summarize_main_tokens);外部调用 1 个(matches!)。
normalize_tokens1465–1482 ↗
fn normalize_tokens(cmd: &[String]) -> Vec<String>
作用:把命令参数先做一层简单清洗。比如去掉 yes | 前缀,或把 bash -lc '脚本' 拆成脚本里的词。
数据流:进去是原始 token → 碰到 yes/no 管道就去掉前缀;碰到 bash/zsh -c/-lc 就用 shlex 拆内层脚本;否则原样返回 → 出来是更适合解析的 token。
调用关系:parse_command_impl 在拆连接符前调用它。
调用图:被 1 处调用(parse_command_impl);外部调用 1 个(split)。
contains_connectors1484–1488 ↗
fn contains_connectors(tokens: &[String]) -> bool
作用:检查一组 token 里有没有 shell 连接符。连接符就是 |、&&、||、; 这些把命令串起来的符号。
数据流:进去是 token 列表 → 扫描是否含连接符 → 出来是真或假。
调用关系:parse_command_impl 用它决定是否需要调用 split_on_connectors。
调用图:被 1 处调用(parse_command_impl)。
split_on_connectors1490–1506 ↗
fn split_on_connectors(tokens: &[String]) -> Vec<Vec<String>>
作用:按 |、&&、||、; 把一长串命令切成一段段小命令。这样后面可以逐段识别。
数据流:进去是 token 列表 → 遇到连接符就结束当前段,非连接符继续收集 → 出来是多个 token 子列表。
调用关系:parse_command_impl 在发现连接符后调用它。
调用图:被 1 处调用(parse_command_impl);外部调用 2 个(new, take)。
trim_at_connector1508–1514 ↗
fn trim_at_connector(tokens: &[String]) -> Vec<String>
作用:只保留连接符之前的参数。它用于某个命令解析时,避免把管道后面的内容误当成这个命令的参数。
数据流:进去是 token 列表 → 找第一个 |、&&、||、; 的位置 → 出来是它前面的 token。
调用关系:sed、grep、fd、find、awk、python、rg 等解析函数都会用它先切掉后续管道。
调用图:被 7 处调用(awk_data_file_operand, parse_fd_query_and_path, parse_find_query_and_path, parse_grep_like, python_walks_files, sed_read_path, summarize_main_tokens)。
short_display_path1521–1532 ↗
fn short_display_path(path: &str) -> String
作用:把路径缩成更适合展示的短名字。比如 webview/src 显示 webview,core/src/main.rs 显示 main.rs。
数据流:进去是路径字符串 → 统一斜杠、去掉末尾斜杠,再从后往前跳过 build、dist、node_modules、src 等噪音目录 → 出来是一个短显示名。
调用关系:fd、find 和大量命令摘要在填写 path 或 name 时都会调用它。
调用图:被 3 处调用(parse_fd_query_and_path, parse_find_query_and_path, summarize_main_tokens)。
skip_flag_values1535–1564 ↗
fn skip_flag_values(args: &'a [String], flags_with_vals: &[&str]) -> Vec<&'a String>
作用:过滤掉带值选项和它们的值。比如 -I '.js' 里的 '.js' 不应该被当成路径。
数据流:进去是参数列表和“哪些选项会吃掉下一个值”的名单 → 扫描参数,跳过这些选项和值,也跳过 --flag=value;遇到 -- 后保留后面的位置参数 → 出来是剩余参数引用列表。
调用关系:sed、awk、fd、rg、ag、bat、nl 等解析路径或查询词前会用它清理参数。
调用图:被 4 处调用(awk_data_file_operand, parse_fd_query_and_path, sed_read_path, summarize_main_tokens);外部调用 1 个(new)。
first_non_flag_operand1566–1571 ↗
fn first_non_flag_operand(args: &[String], flags_with_vals: &[&str]) -> Option<String>
作用:取第一个不是选项的位置参数。常用于命令只有一个路径参数的情况。
数据流:进去是参数和带值选项名单 → 调 positional_operands 得到所有位置参数 → 返回第一个的复制值,找不到返回空。
调用关系:summarize_main_tokens 用它解析 ls、tree、du、git ls-files 等命令的路径。
调用图:调用 1 个内部函数(positional_operands);被 1 处调用(summarize_main_tokens)。
single_non_flag_operand1573–1580 ↗
fn single_non_flag_operand(args: &[String], flags_with_vals: &[&str]) -> Option<String>
作用:只在刚好有一个位置参数时返回它。这样 cat a b 这种多个文件不会被误说成只读一个文件。
数据流:进去是参数和带值选项名单 → 取所有位置参数 → 如果正好一个就返回,否则返回空。
调用关系:summarize_main_tokens 用它解析 cat、bat、less、more 这类读单个文件的命令。
调用图:调用 1 个内部函数(positional_operands);被 1 处调用(summarize_main_tokens)。
positional_operands1582–1614 ↗
fn positional_operands(args: &'a [String], flags_with_vals: &[&str]) -> Vec<&'a String>
作用:从参数里挑出真正的位置参数,也就是不是选项、不是选项值的内容。它是很多路径提取函数的基础。
数据流:进去是参数和带值选项名单 → 跳过 -x、--flag=value、会吃下一个值的选项;遇到 -- 后全部当位置参数 → 出来是位置参数引用列表。
调用关系:first_non_flag_operand 和 single_non_flag_operand 都把实际筛选工作交给它。
调用图:被 2 处调用(first_non_flag_operand, single_non_flag_operand);外部调用 1 个(new)。
parse_grep_like1616–1671 ↗
fn parse_grep_like(main_cmd: &[String], args: &[String]) -> ParsedCommand
作用:解析 grep、egrep、fgrep 和 git grep 这类搜索命令。它负责找出搜索词和搜索位置。
数据流:进去是完整主命令和 grep 后面的参数 → 跳过常见选项,处理 -e/-f 指定模式,收集非选项参数 → 出来是 ParsedCommand::Search,cmd 用 shlex_join,query 保持原样,path 做短显示。
调用关系:summarize_main_tokens 在遇到 grep 系和 git grep 时调用它。
调用图:调用 2 个内部函数(shlex_join, trim_at_connector);被 1 处调用(summarize_main_tokens);外部调用 1 个(new)。
awk_data_file_operand1673–1696 ↗
fn awk_data_file_operand(args: &[String]) -> Option<String>
作用:判断 awk 命令有没有直接读取某个数据文件,并找出这个文件。没有文件时 awk 多半只是管道里的处理器。
数据流:进去是 awk 后面的参数 → 截断连接符,跳过 -F、-v、-f 等选项和值;如果有脚本文件或脚本文本后还有数据文件,就返回数据文件 → 否则返回空。
调用关系:is_small_formatting_command 用它判断 awk 是否可丢;summarize_main_tokens 用它生成 Read。
调用图:调用 2 个内部函数(skip_flag_values, trim_at_connector);被 2 处调用(is_small_formatting_command, summarize_main_tokens)。
python_walks_files1698–1715 ↗
fn python_walks_files(args: &[String]) -> bool
作用:判断 python -c 脚本是不是在遍历文件。比如 os.listdir、glob.glob、Path.rglob 这些都说明它在看文件列表。
数据流:进去是 python 后面的参数 → 找到 -c 后的脚本文本 → 搜索一组文件遍历关键词 → 出来是真或假。
调用关系:summarize_main_tokens 在识别 python 命令后调用它决定 ListFiles 还是 Unknown。
调用图:调用 1 个内部函数(trim_at_connector);被 1 处调用(summarize_main_tokens)。
is_python_command1717–1723 ↗
fn is_python_command(cmd: &str) -> bool
作用:判断命令名是不是 Python 解释器。它支持 python、python2、python3 以及带小版本号的名字。
数据流:进去是命令名字符串 → 和已知 python 名称、前缀比较 → 出来是真或假。
调用关系:summarize_main_tokens 用它进入 Python 特殊解析分支。
调用图:被 1 处调用(summarize_main_tokens)。
cd_target1725–1748 ↗
fn cd_target(args: &[String]) -> Option<String>
作用:从 cd 命令参数里找出目标目录。它要处理 -L、-P、-- 这类选项和奇怪目录名。
数据流:进去是 cd 后面的参数 → 跳过常见选项,-- 后直接取下一个,多个普通参数时记住最后一个 → 出来是目录字符串或空。
调用关系:parse_command_impl 和 parse_shell_script 用它跟踪当前目录,再通过 join_paths 修正读文件路径。
调用图:被 2 处调用(parse_command_impl, parse_shell_script);外部调用 1 个(matches!)。
is_pathish1750–1757 ↗
fn is_pathish(s: &str) -> bool
作用:粗略判断一个字符串像不像路径。比如 .、..、./x、../x、含斜杠或反斜杠都算。
数据流:进去是字符串 → 检查特殊目录名和路径分隔符 → 出来是真或假。
调用关系:parse_fd_query_and_path 用它区分 fd 的单个参数到底是查询词还是路径。
调用图:被 1 处调用(parse_fd_query_and_path)。
parse_fd_query_and_path1759–1790 ↗
fn parse_fd_query_and_path(tail: &[String]) -> (Option<String>, Option<String>)
作用:解析 fd 命令的查询词和路径。fd 既可以找文件名,也可以只列某个目录。
数据流:进去是 fd 后面的参数 → 去掉连接符,跳过 -t、-e、--exclude 等选项值,收集非选项参数 → 一个参数时看它像路径还是查询词,两个以上时第一个当查询、第二个当路径 → 出来是 query/path 两个可选值。
调用关系:summarize_main_tokens 的 fd 分支调用它,并据此返回 Search 或 ListFiles。
调用图:调用 4 个内部函数(is_pathish, short_display_path, skip_flag_values, trim_at_connector);被 1 处调用(summarize_main_tokens)。
parse_find_query_and_path1792–1816 ↗
fn parse_find_query_and_path(tail: &[String]) -> (Option<String>, Option<String>)
作用:解析 find 命令的根路径和常见过滤条件。它主要识别 -name、-iname、-path、-regex。
数据流:进去是 find 后面的参数 → 找第一个像路径的普通参数作为 path,再找常见过滤选项后面的模式作为 query → 出来是 query/path。
调用关系:summarize_main_tokens 的 find 分支调用它决定是 Search 还是 ListFiles。
调用图:调用 2 个内部函数(short_display_path, trim_at_connector);被 1 处调用(summarize_main_tokens)。
parse_shell_lc_commands1818–1822 ↗
fn parse_shell_lc_commands(original: &[String]) -> Option<Vec<ParsedCommand>>
作用:专门处理 bash/zsh 这类 shell 包起来的命令。它把外壳剥掉,再解析里面的脚本。
数据流:进去是原始命令参数 → extract_bash_command 找到内层脚本 → 调 parse_shell_script → 出来是脚本的 ParsedCommand 列表;不是 bash/zsh 就返回空。
调用关系:parse_command_impl 一开始会调用它;它把真正脚本解析交给 parse_shell_script。
调用图:调用 2 个内部函数(extract_bash_command, parse_shell_script);被 1 处调用(parse_command_impl)。
parse_shell_script1825–1951 ↗
fn parse_shell_script(script: &str) -> Vec<ParsedCommand>
作用:解析一段 Bash 风格脚本文本。它比简单按空格拆更强,会尝试理解管道、&&、cd 和小格式化命令。
数据流:进去是一段脚本字符串 → 先用 shell 解析器得到命令序列;去掉 head、wc、printf 等小工具;逐条调用 summarize_main_tokens;记录 cd 并修正读文件路径;再简化 true、nl 等;必要时调整 cmd 显示文本 → 出来是 ParsedCommand 列表,失败时返回 Unknown。
调用关系:parse_shell_lc_commands 和 memories_usage_kinds_from_command 会调用它;它依赖 try_parse_shell、try_parse_word_only_commands_sequence、drop_small_formatting_commands、summarize_main_tokens、simplify_once 等。
调用图:调用 7 个内部函数(try_parse_shell, try_parse_word_only_commands_sequence, cd_target, drop_small_formatting_commands, join_paths, simplify_once, summarize_main_tokens);被 2 处调用(memories_usage_kinds_from_command, parse_shell_lc_commands);外部调用 4 个(from, new, split, vec!)。
is_small_formatting_command1956–2023 ↗
fn is_small_formatting_command(tokens: &[String]) -> bool
作用:判断一个命令是不是管道里的“小修饰工具”。比如 head、wc、cut 通常只是让输出更短或更整齐,不是主操作。
数据流:进去是一段命令 token → 根据命令名和参数判断:固定名单直接算小工具;awk/sed/head/tail 要看有没有文件;xargs 要看会不会修改文件 → 出来是真或假。
调用关系:drop_small_formatting_commands 用它过滤 shell 脚本中的辅助命令;相关测试也直接验证它。
调用图:调用 3 个内部函数(awk_data_file_operand, is_mutating_xargs_command, sed_read_path)。
is_mutating_xargs_command2025–2027 ↗
fn is_mutating_xargs_command(tokens: &[String]) -> bool
作用:判断 xargs 后面接的子命令是否会修改文件。会修改时不能把 xargs 当成普通格式化工具丢掉。
数据流:进去是 xargs 命令 token → 先找出 xargs 真正要执行的子命令 → 判断子命令是否带原地修改或替换选项 → 出来是真或假。
调用关系:is_small_formatting_command 在处理 xargs 时调用它。
调用图:调用 1 个内部函数(xargs_subcommand);被 1 处调用(is_small_formatting_command)。
xargs_subcommand2029–2053 ↗
fn xargs_subcommand(tokens: &[String]) -> Option<&[String]>
作用:从 xargs 参数里找出它最终要运行的子命令。比如 xargs -I{} perl -pi ... 里真正子命令是 perl。
数据流:进去是 xargs 的完整 token → 跳过 xargs 自己的选项和值,遇到 -- 或第一个非选项后取剩余部分 → 出来是子命令 token 切片或空。
调用关系:is_mutating_xargs_command 用它拿到子命令后再判断是否会改文件。
调用图:被 1 处调用(is_mutating_xargs_command);外部调用 1 个(matches!)。
xargs_is_mutating_subcommand2055–2065 ↗
fn xargs_is_mutating_subcommand(tokens: &[String]) -> bool
作用:判断 xargs 执行的具体子命令是不是修改型命令。重点看 perl/ruby/sed 的原地编辑和 rg 的替换。
数据流:进去是子命令 token → 看命令名;perl/ruby 检查 -i/-pi,sed 检查 -i 或 --in-place,rg 检查 --replace → 出来是真或假。
调用关系:is_mutating_xargs_command 通过 xargs_subcommand 拿到子命令后会用它。
调用图:调用 1 个内部函数(xargs_has_in_place_flag)。
xargs_has_in_place_flag2067–2071 ↗
fn xargs_has_in_place_flag(tokens: &[String]) -> bool
作用:检查参数里有没有原地修改文件的 -i 或 -pi 选项。这个小判断用于识别 perl、ruby、sed 的危险写法。
数据流:进去是参数 token → 扫描是否等于或以 -i、-pi 开头 → 出来是真或假。
调用关系:xargs_is_mutating_subcommand 用它判断 perl、ruby、sed 是否会直接改文件。
调用图:被 1 处调用(xargs_is_mutating_subcommand)。
drop_small_formatting_commands2073–2076 ↗
fn drop_small_formatting_commands(mut commands: Vec<Vec<String>>) -> Vec<Vec<String>>
作用:从一组 shell 命令里删掉小格式化工具。这样管道摘要会聚焦真正的读、搜、列文件命令。
数据流:进去是多个命令 token 列表 → 对每个调用 is_small_formatting_command,不是小工具的留下 → 出来是过滤后的命令列表。
调用关系:parse_shell_script 在解析 shell 命令序列后调用它。
调用图:被 1 处调用(parse_shell_script)。
summarize_main_tokens2078–2503 ↗
fn summarize_main_tokens(main_cmd: &[String]) -> ParsedCommand
作用:把单个命令 token 翻译成一个 ParsedCommand。它是识别 ls、rg、grep、cat、sed、python 等具体命令的核心表。
数据流:进去是一段不含外层连接符的命令 token → 看第一个词是什么;按命令类型提取查询词、路径或文件名;能识别就返回 ListFiles/Search/Read,不能识别就返回 Unknown → 出来的 cmd 通常用 shlex_join 生成。
调用关系:parse_command_impl 和 parse_shell_script 都会逐段调用它;它内部调用 parse_grep_like、parse_fd_query_and_path、parse_find_query_and_path、sed_read_path、awk_data_file_operand、python_walks_files 等专门小解析器。
调用图:调用 13 个内部函数(awk_data_file_operand, first_non_flag_operand, is_python_command, parse_fd_query_and_path, parse_find_query_and_path, parse_grep_like, python_walks_files, sed_read_path, shlex_join, short_display_path (+3 more));被 2 处调用(parse_command_impl, parse_shell_script);外部调用 3 个(from, new, matches!)。
is_abs_like2505–2518 ↗
fn is_abs_like(path: &str) -> bool
作用:判断路径是不是绝对路径。它同时考虑 Unix 路径、Windows 盘符路径和 UNC 网络路径。
数据流:进去是路径字符串 → 先用标准 Path 判断,再额外检查 C:\ 和 \\server 这类 Windows 形式 → 出来是真或假。
调用关系:join_paths 用它决定相对路径是否需要拼到当前目录后面。
调用图:被 1 处调用(join_paths);外部调用 1 个(new)。
join_paths2520–2530 ↗
fn join_paths(base: &str, rel: &str) -> String
作用:把当前目录和相对路径拼成一个路径。遇到绝对路径时不乱拼,直接使用原路径。
数据流:进去是 base 和 rel → 如果 rel 是绝对路径就原样返回;base 为空也直接返回 rel;否则用 PathBuf 拼接 → 出来是路径字符串。
调用关系:parse_command_impl 和 parse_shell_script 在处理 cd 后的读文件路径时调用它;它依赖 is_abs_like。
调用图:调用 1 个内部函数(is_abs_like);被 2 处调用(parse_command_impl, parse_shell_script);外部调用 1 个(from)。
命令安全分类
这些文件组织并实现跨平台的安全和危险命令检测,包括 Windows 上使用的 PowerShell AST 辅助工具。
shell-command/src/command_safety/mod.rs源码 ↗
可以把这个文件理解成一本小册子的目录页。项目里需要判断用户输入的 shell 命令是不是安全,比如能不能执行、会不会有危险操作。真正干活的代码分散在几个子文件里:有的判断命令危险不危险,有的判断命令安全不安全,还有一个专门尝试解析 PowerShell 命令。这个文件把这些零件挂到同一个“command_safety”模块下面,并决定哪些零件公开给其他地方用。它还特别照顾 Windows:只有在 Windows 系统上,才会编译 Windows 专用的安全命令列表。最后,它把 PowerShell 解析函数在当前 crate 内重新导出,方便同一个项目里的其他代码直接拿来用,而不用知道内部文件怎么摆放。
shell-command/src/command_safety/powershell_parser.rs源码 ↗
这个文件像一个“翻译窗口”:Rust 程序不自己猜 PowerShell 脚本的意思,而是启动一个长期运行的 PowerShell 子进程,让它用官方语法树来解析脚本。为了省时间,它会按 PowerShell 可执行文件路径缓存子进程,避免每次检查都重新启动 PowerShell。主程序把待解析脚本转成 PowerShell 喜欢的 UTF-16 小端格式,再做 Base64 编码,通过标准输入发给子进程;子进程再用一行 JSON 从标准输出回传结果。每个请求都有编号,防止读到错乱的回复。如果子进程坏了,代码会丢掉旧进程并重试一次。解析结果分三类:成功拆出命令、语法形式不支持、解析失败。文件底部还有 Windows 测试,确认同一个解析进程能连续处理请求,也会拒绝 PowerShell 的特殊“停止解析”形式。
parse_with_powershell_ast27–35 ↗
fn parse_with_powershell_ast(executable: &str, script: &str) -> PowershellParseOutcome
作用:这是外部调用 PowerShell 解析器的主要入口。它负责找到或创建一个可复用的 PowerShell 解析进程,然后把脚本交过去解析。
数据流:进去的是 PowerShell 可执行文件路径和脚本文本;它先拿到一个全局缓存表,并用互斥锁(一把锁,防止多个任务同时改同一份缓存)保护起来;然后把缓存表、路径和脚本交给内部函数处理,出来的是“成功命令列表 / 不支持 / 失败”三种结果之一。
调用关系:它被 try_parse_powershell_ast_commands 和更上层的 parse_powershell_script 使用。它自己不直接和子进程说话,而是把真正的缓存和重试工作交给 parse_with_cached_process。
调用图:调用 1 个内部函数(parse_with_cached_process);被 2 处调用(try_parse_powershell_ast_commands, parse_powershell_script);外部调用 1 个(new)。
try_parse_powershell_ast_commands37–45 ↗
fn try_parse_powershell_ast_commands(
executable: &str,
script: &str,
) -> Option<Vec<Vec<String>>>
作用:这是一个更方便的包装函数:调用者只关心有没有拿到命令列表,不想处理“不支持”和“失败”的区别时会用它。
数据流:进去的是 PowerShell 路径和脚本;它调用 parse_with_powershell_ast;如果结果里有命令列表,就返回 Some(commands),否则把“不支持”和“失败”都变成 None。
调用关系:它站在更友好的接口层,背后依赖 parse_with_powershell_ast。它不做解析,只把底层较细的结果压缩成“有结果 / 没结果”。
调用图:调用 1 个内部函数(parse_with_powershell_ast)。
parse_with_cached_process54–89 ↗
fn parse_with_cached_process(
parser_processes: &mut HashMap<String, PowershellParserProcess>,
executable: &str,
script: &str,
) -> PowershellParseOutcome
作用:这个函数负责复用 PowerShell 子进程,并在旧进程坏掉时自动重试一次。它避免每次解析都启动 PowerShell,提升速度。
数据流:进去的是解析进程缓存表、PowerShell 路径和脚本;它用路径作为钥匙查缓存,没有就 spawn 一个新进程;然后调用该进程的 parse。若第一次通信失败,就删除旧进程再建一个重试;最后输出解析结果,或返回失败。
调用关系:它由 parse_with_powershell_ast 调用,是缓存策略的核心。它会调用 PowershellParserProcess::spawn 创建子进程,再让 PowershellParserProcess::parse 完成一次具体解析。
调用图:调用 1 个内部函数(spawn);被 1 处调用(parse_with_powershell_ast)。
encode_powershell_base6491–97 ↗
fn encode_powershell_base64(script: &str) -> String
作用:这个函数把普通字符串变成 PowerShell 的 EncodedCommand 和协议都能接受的 Base64 字符串。简单说,就是把文字打包成 PowerShell 看得懂的安全传输格式。
数据流:进去的是脚本文本;它先把文本按 UTF-16 编码(一种 Windows/PowerShell 常用的文字存法)拆成小端字节,再做 Base64 编码(一种把二进制变成普通可打印字符的编码);出来的是编码后的字符串。
调用关系:PowershellParserProcess::parse 用它编码用户脚本。encoded_parser_script 也间接使用同样的编码思路来准备启动时注入 PowerShell 的解析脚本。
调用图:被 1 处调用(parse);外部调用 1 个(with_capacity)。
encoded_parser_script99–103 ↗
fn encoded_parser_script() -> &'static str
作用:这个函数提供已经编码好的内置 PowerShell 解析脚本,用来启动解析子进程。它只编码一次,之后重复使用。
数据流:进去没有普通参数;它读取编译进程序里的 powershell_parser.ps1 文本,第一次调用时把它编码成 Base64 并缓存;出来的是这个编码字符串的引用。
调用关系:PowershellParserProcess::spawn 在启动 PowerShell 时会调用它,把解析脚本通过 -EncodedCommand 交给 PowerShell 执行。
PowershellParserProcess::spawn115–148 ↗
fn spawn(executable: &str) -> std::io::Result<Self>
作用:这个函数启动一个专门用来解析脚本的 PowerShell 子进程。它像开了一个长期服务窗口,后面可以反复往里递脚本。
数据流:进去的是 PowerShell 可执行文件路径;它用一组参数启动 PowerShell:不显示标志、不加载用户配置、不交互,并运行内置解析脚本;同时打开子进程的标准输入和标准输出,关闭错误输出;出来的是一个 PowershellParserProcess,里面保存子进程、输入管道、输出管道和下一个请求编号。若拿不到管道,会杀掉刚启动的子进程并返回错误。
调用关系:parse_with_cached_process 需要新解析器时调用它。测试函数也直接调用它来验证进程能工作。它会用 encoded_parser_script 准备启动命令,用 take_child_stdin 和 take_child_stdout 接管通信管道,失败时交给 kill_child 清理。
调用图:调用 4 个内部函数(encoded_parser_script, kill_child, take_child_stdin, take_child_stdout);被 3 处调用(parse_with_cached_process, parser_process_handles_multiple_requests, parser_process_rejects_stop_parsing_forms);外部调用 3 个(null, piped, new)。
PowershellParserProcess::parse150–184 ↗
fn parse(&mut self, script: &str) -> std::io::Result<PowershellParseOutcome>
作用:这个函数通过已经启动好的 PowerShell 子进程解析一段脚本。它负责发请求、等回复、检查回复是不是对应当前请求。
数据流:进去的是一段脚本文本和这个解析进程自身的状态;它给请求分配编号,把脚本编码成 Base64,序列化成 JSON,加换行后写入子进程 stdin;然后从 stdout 读一行 JSON,反序列化成响应,检查响应编号是否匹配;出来的是解析结果。如果子进程关闭、JSON 坏了或编号不对,就返回 I/O 错误。
调用关系:它通常由 parse_with_cached_process 调用。它内部依赖 encode_powershell_base64、serialize_request 和 deserialize_response,并最终用 PowershellParserResponse::into_outcome 把协议响应变成上层能理解的解析结果。
调用图:调用 3 个内部函数(deserialize_response, encode_powershell_base64, serialize_request);外部调用 6 个(read_line, flush, write_all, new, new, format!)。
PowershellParserProcess::drop188–190 ↗
fn drop(&mut self)
作用:这是解析进程对象被丢弃时自动运行的清理动作。它确保后台 PowerShell 子进程不会变成没人管的残留进程。
数据流:进去的是即将被销毁的 PowershellParserProcess;它取出里面的 child 子进程并尝试终止和等待结束;出来没有返回值,但系统资源被释放了。
调用关系:当缓存删除旧进程、程序结束或对象离开作用域时会触发它。它把实际杀进程的细节交给 kill_child。
调用图:调用 1 个内部函数(kill_child)。
take_child_stdin193–200 ↗
fn take_child_stdin(child: &mut Child) -> std::io::Result<ChildStdin>
作用:这个函数从刚启动的子进程里拿到标准输入管道。没有这根管道,Rust 就没法把解析请求发给 PowerShell。
数据流:进去的是一个可修改的子进程对象;它尝试取走 child.stdin;成功则输出 ChildStdin,失败则输出一个 BrokenPipe 错误,表示通信管道断了或没开出来。
调用关系:PowershellParserProcess::spawn 启动子进程后马上调用它。它只负责拿输入管道,不负责启动或解析。
调用图:被 1 处调用(spawn)。
take_child_stdout202–209 ↗
fn take_child_stdout(child: &mut Child) -> std::io::Result<BufReader<ChildStdout>>
作用:这个函数从刚启动的子进程里拿到标准输出管道,并包上一层按行读取的工具。没有它,Rust 就收不到 PowerShell 的解析结果。
数据流:进去的是一个可修改的子进程对象;它尝试取走 child.stdout,成功后包装成 BufReader,方便一行一行读;失败则输出 BrokenPipe 错误。
调用关系:PowershellParserProcess::spawn 调用它来建立返回通道。后续 PowershellParserProcess::parse 会通过这个 BufReader 读取每次解析的 JSON 响应。
调用图:被 1 处调用(spawn)。
serialize_request211–218 ↗
fn serialize_request(request: &PowershellParserRequest) -> std::io::Result<String>
作用:这个函数把一次解析请求变成 JSON 文本。JSON 可以理解成一种常见的“键值格式小纸条”,方便 Rust 和 PowerShell 互相传话。
数据流:进去的是 PowershellParserRequest,里面有请求编号和编码后的脚本;它用 serde_json 转成字符串;成功输出 JSON 字符串,失败则包装成 I/O 错误,说明请求没法发送。
调用关系:PowershellParserProcess::parse 在写入子进程 stdin 前会调用它。它是 Rust 到 PowerShell 这条通信路上的打包步骤。
deserialize_response220–227 ↗
fn deserialize_response(response_line: &str) -> std::io::Result<PowershellParserResponse>
作用:这个函数把 PowerShell 回来的一行 JSON 文本还原成 Rust 能用的响应结构。它是收信后的拆包步骤。
数据流:进去的是一行响应文本;它尝试按 PowershellParserResponse 的格式解析;成功输出响应对象,失败则输出 InvalidData 错误,表示子进程回的内容不是约定格式。
调用关系:PowershellParserProcess::parse 从 stdout 读到一行后调用它。它把文本交给后续的 PowershellParserResponse::into_outcome 继续判断成功、不支持还是失败。
PowershellParserResponse::into_outcome244–259 ↗
fn into_outcome(self) -> PowershellParseOutcome
作用:这个函数把底层协议里的响应状态,转换成项目内部更清楚的解析结果。它还会过滤掉空命令、空参数这类无效结果。
数据流:进去的是一个响应对象,包含 id、status 和可选命令列表;如果 status 是 ok,并且命令列表存在且每个命令、每个词都非空,就输出 Commands;如果 ok 但内容不合格,或 status 是 unsupported,就输出 Unsupported;其他未知状态输出 Failed。
调用关系:PowershellParserProcess::parse 在确认响应编号正确后使用它。它是“协议语言”到“业务判断语言”的转换点。
kill_child262–265 ↗
fn kill_child(child: &mut Child)
作用:这个函数负责尽量干净地结束 PowerShell 子进程。它先发终止信号,再等待进程真正退出。
数据流:进去的是一个子进程对象;它调用 kill 尝试杀掉进程,再调用 wait 等待收尾;它忽略清理过程中的错误,因为这里的目标是尽力释放资源。
调用关系:PowershellParserProcess::drop 会调用它做日常清理。PowershellParserProcess::spawn 在启动后发现管道有问题时,也会调用它避免留下半残的子进程。
tests::parser_process_handles_multiple_requests274–298 ↗
fn parser_process_handles_multiple_requests()
作用:这个测试确认同一个 PowerShell 解析进程可以连续处理多次请求。也就是说,缓存长连接不是只适合用一次。
数据流:进去没有外部参数;测试先寻找本机 PowerShell,可用时启动解析进程;然后解析两段不同脚本,并把结果和预期命令列表比较;如果结果不一致,测试失败。
调用关系:它直接调用 PowershellParserProcess::spawn 和 parse,验证 parse_with_cached_process 依赖的核心能力:一个子进程能安全地反复收发请求。
调用图:调用 2 个内部函数(spawn, try_find_powershell_executable_blocking);外部调用 1 个(assert_eq!)。
tests::parser_process_rejects_stop_parsing_forms301–312 ↗
fn parser_process_rejects_stop_parsing_forms()
作用:这个测试确认遇到 PowerShell 的“停止解析”写法时,解析器会把它判为不支持,而不是冒险猜测。这个写法会让后面的内容按特殊规则处理,安全检查很容易误判。
数据流:进去没有外部参数;测试找到 PowerShell 后启动解析进程,传入带有 --% 的脚本;然后检查结果必须是 Unsupported;如果解析器错误地给出命令列表,测试失败。
调用关系:它直接覆盖 PowershellParserProcess::spawn 和 parse 的行为边界,保证上层安全检查在碰到危险或难判断的 PowerShell 语法时会保守处理。
调用图:调用 2 个内部函数(spawn, try_find_powershell_executable_blocking);外部调用 1 个(assert_eq!)。
shell-command/src/command_safety/windows_dangerous_commands.rs源码 ↗
在 Windows 上,同一件事可以绕很多路做:PowerShell 可以 Start-Process 打开网址,cmd 可以用 start 打开网页,explorer、rundll32、mshta、浏览器本身也能把网址交给系统执行;删除文件时还可能带上强制参数,减少提示和保护。这个文件把这些常见危险模式集中检查。入口函数先看是不是 PowerShell,再看是不是 cmd,最后看是不是直接启动图形程序。它不会完整理解所有脚本语言,只做“足够实用”的拆词和匹配:找可执行文件名、找网址、找删除命令和强制参数。文件后半部分是一组测试,用很多容易漏掉的写法来验证,比如命令用分号、逗号、没有空格的 & 串起来,或者参数大小写不同。
is_dangerous_command_windows8–21 ↗
fn is_dangerous_command_windows(command: &[String]) -> bool
作用:这是 Windows 危险命令检查的总入口。别人只要把准备执行的命令参数交给它,它就回答“危险”或“不危险”。
数据流:输入是一串命令参数,第一个通常是程序名,后面是参数。它先交给 PowerShell 检查,再交给 cmd 检查,最后检查是否直接启动 explorer、浏览器等图形程序;任何一步发现危险就立刻返回 true,否则返回 false。
调用关系:上层的 command_might_be_dangerous 会调用它来做 Windows 专项判断。它自己不深挖细节,而是把工作分给 is_dangerous_powershell、is_dangerous_cmd 和 is_direct_gui_launch。
调用图:调用 3 个内部函数(is_dangerous_cmd, is_dangerous_powershell, is_direct_gui_launch);被 1 处调用(command_might_be_dangerous)。
is_dangerous_powershell23–38 ↗
fn is_dangerous_powershell(command: &[String]) -> bool
作用:判断这条命令是不是 PowerShell,并且里面有没有危险写法。PowerShell 很灵活,所以这里先把它的脚本文字尽量拆成词再看。
数据流:输入是一串命令参数。它先取出程序名,确认是不是 powershell 或 pwsh;如果是,就解析后面的 PowerShell 调用参数,得到一组脚本词;再把这些词交给更细的规则检查,最后返回是否危险。
调用关系:它由 is_dangerous_command_windows 调用,是 Windows 总检查中的第一关。它依赖 is_powershell_executable 识别程序名,依赖 parse_powershell_invocation 拆出脚本,再交给 is_dangerous_powershell_words 做实际判断。
调用图:调用 3 个内部函数(is_dangerous_powershell_words, is_powershell_executable, parse_powershell_invocation);被 1 处调用(is_dangerous_command_windows)。
is_dangerous_powershell_words40–90 ↗
fn is_dangerous_powershell_words(words: &[String]) -> bool
作用:检查已经拆好的 PowerShell 词列表,看里面有没有“打开网址”或“强制删除”的危险组合。它关心的是词之间形成的意图,而不是某个单词单独出现。
数据流:输入是 PowerShell 脚本拆出来的词。它先统一小写并去掉引号,再检查是否包含网址;如果网址和 Start-Process、Invoke-Item、ShellExecute、mshta、浏览器、explorer 等启动方式同时出现,就判危险;最后还会检查 Remove-Item、rm、del 等删除命令是否配了 -Force。
调用关系:它由 is_dangerous_powershell 调用,是 PowerShell 检查的核心规则区。它会借助 args_have_url 找网址,借助 is_browser_executable 识别浏览器名,并把强制删除判断交给 has_force_delete_cmdlet;调用图也把它标成会被自身关联到,实际可理解为规则扫描过程中的内部匹配关系。
调用图:调用 3 个内部函数(args_have_url, has_force_delete_cmdlet, is_browser_executable);被 2 处调用(is_dangerous_powershell_words, is_dangerous_powershell);外部调用 1 个(matches!)。
is_dangerous_cmd92–157 ↗
fn is_dangerous_cmd(command: &[String]) -> bool
作用:判断一条命令是不是通过 Windows 的 cmd.exe 执行,并检查其中有没有危险片段。重点是 cmd /c 或 /r 后面的真实命令。
数据流:输入是一串命令参数。它先确认程序名是 cmd 或 cmd.exe,再跳过 /c、/r 等启动参数,拿到后面的命令正文;如果正文是一整串文字,就尽量拆成多个词;再按 &, &&, |, || 这些连接符分段,检查每段是否是 start 打开网址、del /f 强制删除、或 rmdir /s /q 静默递归删目录。
调用关系:它由 is_dangerous_command_windows 调用,是 Windows 总检查中的 cmd 分支。它使用 executable_basename 识别 cmd 名称,也会把粘在词里的命令连接符拆开,避免漏掉 echo hi&del /f 这种写法。
调用图:调用 1 个内部函数(executable_basename);被 1 处调用(is_dangerous_command_windows);外部调用 1 个(split)。
is_direct_gui_launch159–188 ↗
fn is_direct_gui_launch(command: &[String]) -> bool
作用:检查命令是不是直接启动了会打开网页或执行系统外壳动作的图形程序。比如 explorer、mshta、rundll32 或浏览器带着网址运行。
数据流:输入是一串命令参数。它拿出程序名,转成小写的基础文件名;如果是 explorer、mshta、rundll32 加 url.dll 文件协议处理器,或常见浏览器,并且参数里有 http/https 网址,就返回 true;否则返回 false。
调用关系:它由 is_dangerous_command_windows 在 PowerShell 和 cmd 检查之后调用,用来兜住“不经过脚本外壳、直接打开程序”的情况。它依赖 executable_basename、args_have_url 和 is_browser_executable。
调用图:调用 3 个内部函数(args_have_url, executable_basename, is_browser_executable);被 1 处调用(is_dangerous_command_windows);外部调用 1 个(matches!)。
split_embedded_cmd_operators190–223 ↗
fn split_embedded_cmd_operators(token: &str) -> Vec<String>
作用:把 cmd 命令里黏在普通文字上的连接符拆出来。这样 echo hi&del 这种没有空格的写法,也能被看成两段命令。
数据流:输入是一个字符串词。它从左到右扫描,遇到 &、&&、|、|| 就把前面的文字、连接符、后面的文字分开放进列表,最后去掉空白片段并返回拆好的字符串列表。
调用关系:它服务于 is_dangerous_cmd 的拆词阶段。没有它,危险命令只要和前一个词黏在一起,就可能逃过分段检查。
调用图:外部调用 1 个(new)。
has_force_delete_cmdlet225–290 ↗
fn has_force_delete_cmdlet(tokens: &[String]) -> bool
作用:检查 PowerShell 里是否出现“删除命令”和“-Force 强制参数”在同一段命令里。它避免把别的命令里的 -Force 误算到删除命令上。
数据流:输入是已经小写化或规范化的词列表。它先按分号、管道、换行等硬分隔符切成命令段,再按括号、逗号等软标点拆成小词;只要某一段同时有 remove-item、rm、del 等删除名和 -Force 或 -Force:xxx,就返回 true。
调用关系:它由 is_dangerous_powershell_words 调用,专门负责 PowerShell 强制删除这一类危险。它相当于一个更谨慎的小筛子,保证 Get-ChildItem -Force; Remove-Item test 这种不同段的情况不会被误判。
调用图:被 1 处调用(is_dangerous_powershell_words);外部调用 3 个(new, new, vec!)。
has_force_flag_cmd293–295 ↗
fn has_force_flag_cmd(args: &[String]) -> bool
作用:检查 cmd 的 del 或 erase 参数里有没有 /f。/f 表示强制删除,是这里认为危险的信号之一。
数据流:输入是一段 cmd 命令的参数列表。它逐个看参数是否等于 /f,忽略大小写;找到就返回 true,找不到就返回 false。
调用关系:它用于 cmd 删除命令的规则判断,是 is_dangerous_cmd 检查 del /f、erase /f 时的一个小工具。
has_recursive_flag_cmd298–300 ↗
fn has_recursive_flag_cmd(args: &[String]) -> bool
作用:检查 cmd 的 rd 或 rmdir 参数里有没有 /s。/s 表示递归删除目录,也就是连子目录一起删。
数据流:输入是一段 cmd 命令的参数列表。它逐个查找 /s,忽略大小写;存在则返回 true,不存在则返回 false。
调用关系:它配合 has_quiet_flag_cmd 使用,用在 is_dangerous_cmd 对 rd /s /q 或 rmdir /s /q 的判断里。
has_quiet_flag_cmd303–305 ↗
fn has_quiet_flag_cmd(args: &[String]) -> bool
作用:检查 cmd 的 rd 或 rmdir 参数里有没有 /q。/q 表示安静模式,删除时少提示或不提示。
数据流:输入是一段 cmd 命令的参数列表。它查找是否有 /q,忽略大小写;有就返回 true,没有就返回 false。
调用关系:它和 has_recursive_flag_cmd 一起帮助 is_dangerous_cmd 判断静默递归删目录是否危险。
args_have_url307–309 ↗
fn args_have_url(args: &[String]) -> bool
作用:判断一组参数里是否至少有一个看起来像网页网址。这里主要关心 http 和 https。
数据流:输入是多个字符串参数。它逐个调用 looks_like_url 检查;只要有一个参数像网址,就返回 true;全部都不像就返回 false。
调用关系:它被 is_dangerous_powershell_words 和 is_direct_gui_launch 使用,用来把“启动程序”和“打开网页”这两个条件连起来。
调用图:被 2 处调用(is_dangerous_powershell_words, is_direct_gui_launch)。
looks_like_url311–334 ↗
fn looks_like_url(token: &str) -> bool
作用:判断单个字符串是不是 http 或 https 网址,即使它外面包着引号、括号或分号也尽量识别出来。
数据流:输入是一个字符串。它先尝试从里面找到 http:// 或 https:// 开头的位置,再用正则表达式(一种按模板找文字的工具)去掉常见包裹符号,最后用 URL 解析器确认协议是不是 http 或 https;符合就返回 true。
调用关系:它是 args_have_url 背后的实际网址识别器。它使用延迟初始化的正则表达式,意思是正则模板第一次用到时才创建,避免每次重复准备。
executable_basename336–341 ↗
fn executable_basename(exe: &str) -> Option<String>
作用:从一个程序路径里取出最后的文件名,并转成小写。这样 C:\Windows\System32\cmd.exe 和 cmd.exe 能按同一种方式比较。
数据流:输入是可执行文件路径字符串。它把路径交给系统路径工具取文件名,再转成普通字符串和小写;成功就返回文件名,失败就返回空值。
调用关系:它被 is_dangerous_cmd 和 is_direct_gui_launch 使用,是识别 cmd、explorer、浏览器等程序名的基础小工具。
调用图:被 2 处调用(is_dangerous_cmd, is_direct_gui_launch);外部调用 1 个(new)。
is_powershell_executable343–348 ↗
fn is_powershell_executable(exe: &str) -> bool
作用:判断给定程序名是不是 PowerShell。它同时支持老的 powershell 和新的 pwsh。
数据流:输入是程序路径或名称。它先取基础文件名,再和 powershell、powershell.exe、pwsh、pwsh.exe 这些名字比较;匹配则返回 true。
调用关系:它由 is_dangerous_powershell 调用,负责确认这条命令确实需要走 PowerShell 专项解析。
调用图:被 1 处调用(is_dangerous_powershell);外部调用 1 个(matches!)。
is_browser_executable350–362 ↗
fn is_browser_executable(name: &str) -> bool
作用:判断一个程序名是不是常见浏览器。这里包括 Chrome、Edge、Firefox 和旧的 Internet Explorer。
数据流:输入是已经规范化的程序名。它和内置的浏览器名称列表比较;匹配就返回 true,否则返回 false。
调用关系:它被 is_dangerous_powershell_words 和 is_direct_gui_launch 调用,用来识别“浏览器加网址”这种直接打开网页的危险模式。
调用图:被 2 处调用(is_dangerous_powershell_words, is_direct_gui_launch);外部调用 1 个(matches!)。
parse_powershell_invocation368–408 ↗
fn parse_powershell_invocation(args: &[String]) -> Option<ParsedPowershell>
作用:把 PowerShell 启动参数里的脚本内容提取出来,并拆成一组词。它不是完整的 PowerShell 解释器,只是做安全检查够用的粗解析。
数据流:输入是 PowerShell 程序名后面的参数。它跳过 -NoLogo、-NoProfile 等开关,识别 -Command、/Command、-c 或 -Command:脚本 这些写法;拿到脚本后用 shlex 拆词,或者把剩余参数当作词列表;无法可靠解析时返回空值。
调用关系:它由 is_dangerous_powershell 调用,是 PowerShell 检查进入规则扫描前的准备步骤。它把杂乱的启动参数变成 ParsedPowershell 里的 tokens。
调用图:被 1 处调用(is_dangerous_powershell);外部调用 1 个(split)。
tests::vec_str414–416 ↗
fn vec_str(items: &[&str]) -> Vec<String>
作用:这是测试用的小帮手,把一组字符串字面量快速变成 Vec<String>。这样测试用例写起来更短、更清楚。
数据流:输入是一组 &str,也就是借用的字符串切片。它逐个复制成 String,收集成向量并返回,不改动外部数据。
调用关系:它只在本文件测试里使用,帮助各个测试构造命令参数。测试随后把这些参数交给危险命令检查函数,并用断言确认结果。
tests::powershell_start_process_url_is_dangerous419–426 ↗
fn powershell_start_process_url_is_dangerous()
作用:验证 PowerShell 用 Start-Process 打开网址会被判危险。这是最典型的“脚本让系统打开网页”的场景。
数据流:测试构造 powershell -Command "Start-Process 'https://example.com'" 这样的参数,送入检查函数;期望结果是 true,并用断言确认。
调用关系:它属于测试集,通过 assert! 固定住 PowerShell 网址启动的安全规则,防止以后改代码时漏判。
调用图:外部调用 1 个(assert!)。
tests::powershell_start_process_url_with_trailing_semicolon_is_dangerous429–435 ↗
fn powershell_start_process_url_with_trailing_semicolon_is_dangerous()
作用:验证 PowerShell 命令末尾带分号时,网址启动仍然会被判危险。分号在脚本里很常见,不能让它干扰识别。
数据流:测试输入包含 Start-Process('https://example.com');。检查函数需要去掉或容忍尾部标点,最后返回 true。
调用关系:它用断言保护 looks_like_url 和 PowerShell 拆词规则,确保带括号、分号的写法也能被抓到。
调用图:外部调用 1 个(assert!)。
tests::powershell_start_process_local_is_not_flagged438–444 ↗
fn powershell_start_process_local_is_not_flagged()
作用:验证 PowerShell 启动本地程序 notepad.exe 不会被误判为危险网址启动。安全检查不能只看到 Start-Process 就报警。
数据流:测试输入是 Start-Process notepad.exe。检查函数看到没有 http 或 https 网址,应返回 false。
调用关系:它用断言约束 is_dangerous_powershell_words,保证规则同时需要“启动动作”和“网址”两个条件。
调用图:外部调用 1 个(assert!)。
tests::cmd_start_with_url_is_dangerous447–454 ↗
fn cmd_start_with_url_is_dangerous()
作用:验证 cmd /c start 加网址会被判危险。cmd 的 start 会把网址交给系统默认程序打开。
数据流:测试构造 cmd、/c、start、https://example.com 这组参数。检查函数进入 cmd 分支,发现 start 段里有网址,返回 true。
调用关系:它覆盖 is_dangerous_cmd 中对 start URL 的判断,并用 assert! 固定预期结果。
调用图:外部调用 1 个(assert!)。
tests::msedge_with_url_is_dangerous457–462 ↗
fn msedge_with_url_is_dangerous()
作用:验证直接启动 Edge 浏览器并传入网址会被判危险。即使不经过 PowerShell 或 cmd,也要能拦住。
数据流:测试输入是 msedge.exe 和一个 https 网址。检查函数最终走到直接图形启动检查,识别浏览器和网址后返回 true。
调用关系:它保护 is_direct_gui_launch 和 is_browser_executable 的配合逻辑。
调用图:外部调用 1 个(assert!)。
tests::explorer_with_directory_is_not_flagged465–470 ↗
fn explorer_with_directory_is_not_flagged()
作用:验证 explorer.exe 打开当前目录不会被误判。Explorer 打开文件夹是常见正常操作。
数据流:测试输入是 explorer.exe 和 .。检查函数识别 explorer,但没有发现网址,所以返回 false。
调用关系:它防止 is_direct_gui_launch 过度严格,确保 explorer 只有带网址时才被视为危险。
调用图:外部调用 1 个(assert!)。
tests::powershell_remove_item_force_is_dangerous475–481 ↗
fn powershell_remove_item_force_is_dangerous()
作用:验证 PowerShell 的 Remove-Item 配 -Force 会被判危险。-Force 代表强制删除,风险更高。
数据流:测试输入是 Remove-Item test -Force。检查函数在 PowerShell 词里发现删除命令和强制参数同段出现,返回 true。
调用关系:它覆盖 has_force_delete_cmdlet 的基本场景,并通过断言固定规则。
调用图:外部调用 1 个(assert!)。
tests::powershell_remove_item_recurse_force_is_dangerous484–490 ↗
fn powershell_remove_item_recurse_force_is_dangerous()
作用:验证 Remove-Item 同时带 -Recurse 和 -Force 会被判危险。递归加强制意味着可能删掉整棵目录。
数据流:测试构造 Remove-Item test -Recurse -Force。检查函数重点识别 Remove-Item 和 -Force,返回 true。
调用关系:它补充 PowerShell 删除规则的高风险场景,确保递归参数不会影响强制删除识别。
调用图:外部调用 1 个(assert!)。
tests::powershell_ri_alias_force_is_dangerous493–499 ↗
fn powershell_ri_alias_force_is_dangerous()
作用:验证 PowerShell 删除命令的别名 ri 加 -Force 也会被判危险。攻击或用户不一定写完整命令名。
数据流:测试输入是 pwsh -Command "ri test -Force"。检查函数识别 pwsh 是 PowerShell,也识别 ri 是删除别名,返回 true。
调用关系:它保护 is_powershell_executable 和 has_force_delete_cmdlet 对别名的支持。
调用图:外部调用 1 个(assert!)。
tests::powershell_remove_item_without_force_is_not_flagged502–508 ↗
fn powershell_remove_item_without_force_is_not_flagged()
作用:验证 Remove-Item 没有 -Force 时不会被这个规则判危险。这样可以减少对普通删除命令的误报。
数据流:测试输入是 Remove-Item test。检查函数发现删除命令,但没有强制参数,所以返回 false。
调用关系:它约束 has_force_delete_cmdlet 必须同时满足删除和强制两个条件。
调用图:外部调用 1 个(assert!)。
tests::cmd_del_force_is_dangerous512–516 ↗
fn cmd_del_force_is_dangerous()
作用:验证 cmd 里的 del /f 会被判危险。/f 是强制删除文件。
数据流:测试输入是 cmd /c del /f test.txt。检查函数进入 cmd 分支,识别 del 和 /f,返回 true。
调用关系:它覆盖 is_dangerous_cmd 与 has_force_flag_cmd 的基本配合。
调用图:外部调用 1 个(assert!)。
tests::cmd_erase_force_is_dangerous519–523 ↗
fn cmd_erase_force_is_dangerous()
作用:验证 erase /f 也会被判危险。erase 是 cmd 中删除文件的另一种名字。
数据流:测试输入是 cmd /c erase /f test.txt。检查函数识别 erase 和 /f 后返回 true。
调用关系:它确保 cmd 删除规则不只检查 del,也覆盖 erase。
调用图:外部调用 1 个(assert!)。
tests::cmd_del_without_force_is_not_flagged526–530 ↗
fn cmd_del_without_force_is_not_flagged()
作用:验证 cmd 的 del 没有 /f 时不会被这个规则判危险。这样避免把所有删除都一律拦下。
数据流:测试输入是 cmd /c del test.txt。检查函数发现 del,但没发现 /f,所以返回 false。
调用关系:它保护 has_force_flag_cmd 的边界,要求强制参数必须明确出现。
调用图:外部调用 1 个(assert!)。
tests::cmd_rd_recursive_is_dangerous533–537 ↗
fn cmd_rd_recursive_is_dangerous()
作用:验证 rd /s /q 会被判危险。/s 表示递归删目录,/q 表示安静删除,组合风险很高。
数据流:测试输入是 cmd /c rd /s /q test。检查函数识别 rd,同时发现 /s 和 /q,返回 true。
调用关系:它覆盖 is_dangerous_cmd 对递归静默删目录的判断。
调用图:外部调用 1 个(assert!)。
tests::cmd_rd_without_quiet_is_not_flagged540–544 ↗
fn cmd_rd_without_quiet_is_not_flagged()
作用:验证 rd /s 但没有 /q 时不会被这个规则判危险。这里的规则要求递归和安静两个参数都出现。
数据流:测试输入是 cmd /c rd /s test。检查函数发现 /s,但没有 /q,所以返回 false。
调用关系:它约束 has_recursive_flag_cmd 和 has_quiet_flag_cmd 必须一起满足。
调用图:外部调用 1 个(assert!)。
tests::cmd_rmdir_recursive_is_dangerous547–551 ↗
fn cmd_rmdir_recursive_is_dangerous()
作用:验证 rmdir /s /q 会被判危险。rmdir 和 rd 是同类删除目录命令。
数据流:测试输入是 cmd /c rmdir /s /q test。检查函数识别 rmdir、/s 和 /q,返回 true。
调用关系:它确保 cmd 目录删除规则覆盖 rmdir 这个完整命令名。
调用图:外部调用 1 个(assert!)。
tests::powershell_remove_item_path_recurse_force_is_dangerous555–561 ↗
fn powershell_remove_item_path_recurse_force_is_dangerous()
作用:验证带 -Path、-Recurse、-Force 的 Remove-Item 会被判危险。这对应一个曾经需要修复的实际问题场景。
数据流:测试输入是 Remove-Item -Path 'test' -Recurse -Force。检查函数应从这些参数里识别删除命令和 -Force,返回 true。
调用关系:它作为回归测试,防止以后改 PowerShell 拆词或删除规则时重新漏掉这个写法。
调用图:外部调用 1 个(assert!)。
tests::powershell_remove_item_force_with_semicolon_is_dangerous564–570 ↗
fn powershell_remove_item_force_with_semicolon_is_dangerous()
作用:验证删除命令后面接分号和另一个命令时,前一段的 -Force 仍会被识别。分号表示脚本里的命令分隔。
数据流:测试输入是 Remove-Item test -Force; Write-Host done。检查函数应在第一段发现删除加强制,返回 true。
调用关系:它覆盖 has_force_delete_cmdlet 对分号分段的处理。
调用图:外部调用 1 个(assert!)。
tests::powershell_remove_item_force_inside_block_is_dangerous573–579 ↗
fn powershell_remove_item_force_inside_block_is_dangerous()
作用:验证删除命令放在 if 代码块里也会被判危险。花括号不应该挡住识别。
数据流:测试输入是 if ($true) { Remove-Item test -Force}。检查函数拆掉或忽略括号类标点后,发现删除加 -Force,返回 true。
调用关系:它保护 has_force_delete_cmdlet 对代码块标点的宽容处理。
调用图:外部调用 1 个(assert!)。
tests::powershell_remove_item_force_inside_brackets_is_dangerous582–588 ↗
fn powershell_remove_item_force_inside_brackets_is_dangerous()
作用:验证删除命令被方括号或括号包住时也会被判危险。这类写法在 PowerShell 表达式中可能出现。
数据流:测试输入是 [void]( Remove-Item test -Force)]。检查函数拆分软标点后,发现 Remove-Item 和 -Force,返回 true。
调用关系:它覆盖 has_force_delete_cmdlet 对括号、方括号等软分隔符的处理。
调用图:外部调用 1 个(assert!)。
tests::cmd_del_path_containing_f_is_not_flagged591–598 ↗
fn cmd_del_path_containing_f_is_not_flagged()
作用:验证路径里含有字母 f 不会被误当成 /f 参数。安全规则必须看完整参数,而不是看某个字符。
数据流:测试输入是 cmd /c del C:/foo/bar.txt。检查函数没有看到独立的 /f 参数,所以返回 false。
调用关系:它保护 has_force_flag_cmd 的精确匹配行为。
调用图:外部调用 1 个(assert!)。
tests::cmd_rd_path_containing_s_is_not_flagged601–608 ↗
fn cmd_rd_path_containing_s_is_not_flagged()
作用:验证路径里含有 s 不会被误当成 /s 参数。否则很多正常路径都会误报。
数据流:测试输入是 cmd /c rd C:/source。检查函数没有看到独立的 /s 和 /q,所以返回 false。
调用关系:它约束 has_recursive_flag_cmd 必须匹配完整参数。
调用图:外部调用 1 个(assert!)。
tests::cmd_bypass_chained_del_is_dangerous611–615 ↗
fn cmd_bypass_chained_del_is_dangerous()
作用:验证用 & 把 echo 和 del /f 串起来时,危险删除仍能被发现。这是常见的绕过写法。
数据流:测试输入包含 echo hello & del /f file.txt。检查函数按 & 分段,在后一段发现 del /f,返回 true。
调用关系:它覆盖 is_dangerous_cmd 的命令分段逻辑。
调用图:外部调用 1 个(assert!)。
tests::powershell_chained_no_space_is_dangerous618–624 ↗
fn powershell_chained_no_space_is_dangerous()
作用:验证 PowerShell 里分号后面没有空格也能识别危险删除。脚本不一定写得规整。
数据流:测试输入是 Write-Host hi;Remove-Item -Force C:\tmp。检查函数按分号拆段,发现后一段删除加 -Force,返回 true。
调用关系:它保护 has_force_delete_cmdlet 对紧贴分隔符的处理。
调用图:外部调用 1 个(assert!)。
tests::powershell_comma_separated_is_dangerous627–633 ↗
fn powershell_comma_separated_is_dangerous()
作用:验证 PowerShell 中用逗号隔开的 del,-Force,C:\foo 也会被判危险。逗号是这里当作软分隔符处理的标点。
数据流:测试输入是 del,-Force,C:\foo。检查函数按逗号拆成小词,发现 del 和 -Force,返回 true。
调用关系:它覆盖 has_force_delete_cmdlet 对逗号分隔的处理。
调用图:外部调用 1 个(assert!)。
tests::cmd_echo_del_is_not_dangerous636–640 ↗
fn cmd_echo_del_is_not_dangerous()
作用:验证 echo del /f 只是把文字打印出来,不应被当成真正删除命令。检查要看命令段开头是什么。
数据流:测试输入是 cmd /c echo del /f。检查函数看到这一段的命令是 echo,不是 del,所以返回 false。
调用关系:它约束 is_dangerous_cmd 的分段判断,避免把普通输出文字误报成执行命令。
调用图:外部调用 1 个(assert!)。
tests::cmd_del_single_string_argument_is_dangerous643–649 ↗
fn cmd_del_single_string_argument_is_dangerous()
作用:验证 cmd /c 后面的整串命令是一个字符串时,del /f 也能被识别。很多调用方式会把命令正文合成一个参数。
数据流:测试输入是 cmd /c "del /f file.txt"。检查函数先把这一个字符串拆词,再发现 del /f,返回 true。
调用关系:它保护 is_dangerous_cmd 对单字符串命令正文的 shlex 拆分逻辑。
调用图:外部调用 1 个(assert!)。
tests::cmd_del_chained_single_string_argument_is_dangerous652–658 ↗
fn cmd_del_chained_single_string_argument_is_dangerous()
作用:验证单个字符串里包含 echo hello & del /f file.txt 时也会被判危险。危险命令可能藏在一整段文本里。
数据流:测试输入是 cmd /c "echo hello & del /f file.txt"。检查函数拆词并按 & 分段,在后一段发现 del /f,返回 true。
调用关系:它同时覆盖单字符串拆词和 cmd 命令链分段。
调用图:外部调用 1 个(assert!)。
tests::cmd_chained_no_space_del_is_dangerous661–667 ↗
fn cmd_chained_no_space_del_is_dangerous()
作用:验证 echo hi&del /f file.txt 这种 & 两边没空格的写法会被判危险。不能只依赖空格拆词。
数据流:测试输入是 cmd /c "echo hi&del /f file.txt"。检查函数把嵌在词里的 & 拆出来,然后发现 del /f,返回 true。
调用关系:它主要保护 split_embedded_cmd_operators 在 is_dangerous_cmd 中的作用。
调用图:外部调用 1 个(assert!)。
tests::cmd_chained_andand_no_space_del_is_dangerous670–676 ↗
fn cmd_chained_andand_no_space_del_is_dangerous()
作用:验证 echo hi&&del /f file.txt 也会被判危险。&& 表示前一条成功后执行后一条。
数据流:测试输入包含没有空格的 &&。检查函数拆出 && 并分段,在后一段发现 del /f,返回 true。
调用关系:它覆盖 split_embedded_cmd_operators 对双 & 操作符的处理。
调用图:外部调用 1 个(assert!)。
tests::cmd_chained_oror_no_space_del_is_dangerous679–685 ↗
fn cmd_chained_oror_no_space_del_is_dangerous()
作用:验证 echo hi||del /f file.txt 也会被判危险。|| 表示前一条失败后执行后一条。
数据流:测试输入包含没有空格的 ||。检查函数拆出 || 并分段,在后一段发现 del /f,返回 true。
调用关系:它覆盖 split_embedded_cmd_operators 对双 | 操作符的处理。
调用图:外部调用 1 个(assert!)。
tests::cmd_start_url_single_string_is_dangerous688–694 ↗
fn cmd_start_url_single_string_is_dangerous()
作用:验证 cmd /c 的命令正文是一整串 start https://... 时也会被判危险。
数据流:测试输入是 cmd /c "start https://example.com"。检查函数拆成 start 和网址,识别后返回 true。
调用关系:它保护 is_dangerous_cmd 对单字符串 start URL 的识别。
调用图:外部调用 1 个(assert!)。
tests::cmd_chained_no_space_rmdir_is_dangerous697–703 ↗
fn cmd_chained_no_space_rmdir_is_dangerous()
作用:验证 echo hi&rmdir /s /q testdir 这种没空格串联的删目录命令会被判危险。
数据流:测试输入包含 echo hi&rmdir /s /q testdir。检查函数拆出 &,在 rmdir 段发现 /s 和 /q,返回 true。
调用关系:它同时覆盖 split_embedded_cmd_operators 和 cmd 递归静默删目录规则。
调用图:外部调用 1 个(assert!)。
tests::cmd_del_force_uppercase_flag_is_dangerous706–710 ↗
fn cmd_del_force_uppercase_flag_is_dangerous()
作用:验证 DEL /F 的大写写法也会被判危险。Windows 命令和参数通常不区分大小写。
数据流:测试输入是 cmd /c DEL /F file.txt。检查函数忽略大小写识别 del 和 /f,返回 true。
调用关系:它保护 is_dangerous_cmd 及 has_force_flag_cmd 的大小写不敏感行为。
调用图:外部调用 1 个(assert!)。
tests::cmdexe_r_del_force_is_dangerous713–717 ↗
fn cmdexe_r_del_force_is_dangerous()
作用:验证 cmd.exe /r del /f file.txt 也会被判危险。这里 /r 和 /c 一样被当作进入命令正文的标志。
数据流:测试输入是 cmd.exe /r del /f file.txt。检查函数识别 cmd.exe 和 /r,随后发现 del /f,返回 true。
调用关系:它覆盖 is_dangerous_cmd 对 cmd.exe 名称和 /r 参数的支持。
调用图:外部调用 1 个(assert!)。
tests::cmd_start_quoted_url_single_string_is_dangerous720–726 ↗
fn cmd_start_quoted_url_single_string_is_dangerous()
作用:验证 start 后的网址带引号时也会被判危险。用户常会给网址加引号。
数据流:测试输入是 cmd /c "start \"https://example.com\""。检查函数拆词后识别被引号包住的网址,返回 true。
调用关系:它保护 looks_like_url 对引号包裹网址的识别,以及 cmd 单字符串拆词流程。
调用图:外部调用 1 个(assert!)。
tests::cmd_start_title_then_url_is_dangerous729–735 ↗
fn cmd_start_title_then_url_is_dangerous()
作用:验证 cmd start 的空标题参数后面跟网址时也会被判危险。cmd start 常用第一个引号参数当窗口标题。
数据流:测试输入是 cmd /c "start \"\" https://example.com"。检查函数在这一段参数中仍能找到网址,返回 true。
调用关系:它覆盖 is_dangerous_cmd 对 start 命令参数形态的宽容识别。
调用图:外部调用 1 个(assert!)。
tests::powershell_rm_alias_force_is_dangerous738–744 ↗
fn powershell_rm_alias_force_is_dangerous()
作用:验证 PowerShell 的 rm 别名加 -Force 会被判危险。rm 是 Remove-Item 的常见短写。
数据流:测试输入是 powershell -Command "rm test -Force"。检查函数识别 rm 是删除命令,且同段有 -Force,返回 true。
调用关系:它保护 has_force_delete_cmdlet 对 PowerShell 删除别名 rm 的支持。
调用图:外部调用 1 个(assert!)。
tests::powershell_benign_force_separate_command_is_not_dangerous747–753 ↗
fn powershell_benign_force_separate_command_is_not_dangerous()
作用:验证 -Force 出现在另一个命令里,而 Remove-Item 自己没有 -Force 时,不会误判危险。这能减少误报。
数据流:测试输入是 Get-ChildItem -Force; Remove-Item test。检查函数按分号分成两段,发现 -Force 和删除命令不在同一段,所以返回 false。
调用关系:它保护 has_force_delete_cmdlet 的分段逻辑,确保强制参数不会跨命令串到删除命令上。
调用图:外部调用 1 个(assert!)。
shell-command/src/command_safety/is_dangerous_command.rs源码 ↗
这个文件解决的是“用户给了一条命令,能不能安全地直接执行”的问题。它像门口保安,不真的运行命令,只看命令长什么样。最主要的入口是 command_might_be_dangerous:它先在 Windows 上调用专门的 Windows 危险命令规则,再检查通用规则,比如 rm -rf、rm -f 这种强制删除;如果命令是 bash -lc "一段脚本",它还会把脚本里的简单命令拆出来逐个检查,防止危险命令藏在 shell 脚本字符串里。文件里还放了几段和 git 有关的辅助逻辑,用来正确找到 git 的真正子命令,跳过 -C、-c、--git-dir 这类“全局选项”,避免把参数误当成子命令。最后的测试用例确认了强制删除会被拦住,也确认 PowerShell 的危险判断只在 Windows 上生效。
command_might_be_dangerous7–29 ↗
fn command_might_be_dangerous(command: &[String]) -> bool
作用:这是本文件最主要的危险命令判断入口。别人给它一条已经拆成单词的命令,它回答“这条命令看起来是不是可能很危险”。
数据流:进去的是一个字符串列表,比如 ["rm", "-rf", "/"]。它先按操作系统检查 Windows 特有危险命令,再检查通用危险规则;如果发现是 bash -lc 这种把脚本藏在字符串里的形式,还会把里面的命令拆出来继续查。出来的是 true 或 false,不改动原命令。
调用关系:它会被 render_decision_for_unmatched_command 在需要给未知命令做安全决策时调用。它自己把具体检查分给 is_dangerous_to_call_with_exec、Windows 专用检查器,以及 parse_shell_lc_plain_commands 这个 shell 命令拆解工具。
调用图:调用 3 个内部函数(parse_shell_lc_plain_commands, is_dangerous_to_call_with_exec, is_dangerous_command_windows);被 1 处调用(render_decision_for_unmatched_command)。
is_dangerous_powershell_words33–44 ↗
fn is_dangerous_powershell_words(command: &[String]) -> bool
作用:这个函数专门判断一串已经拆好的 PowerShell 命令词是不是危险。PowerShell 是 Windows 上常见的命令环境,所以这条规则主要给 Windows 用。
数据流:进去的是 PowerShell 命令的单词列表。Windows 上,它把这些词交给 Windows 专用的危险命令判断;非 Windows 上,它直接返回 false,因为这些规则在那里不适用。它不修改输入,只给出是否危险的结果。
调用关系:它也会被 render_decision_for_unmatched_command 调用,用在需要判断 PowerShell 风格命令的时候。真正复杂的 Windows 判断不写在这里,而是交给 windows_dangerous_commands 模块。
调用图:调用 1 个内部函数(is_dangerous_powershell_words);被 1 处调用(render_decision_for_unmatched_command)。
is_git_global_option_with_value46–57 ↗
fn is_git_global_option_with_value(arg: &str) -> bool
作用:这个小函数判断某个 git 参数是不是“后面还要跟一个值”的全局选项。比如 git -C 路径 status 里,-C 后面的路径不是子命令,应该跳过去。
数据流:进去的是一个参数字符串。它拿这个字符串和一组已知 git 全局选项对比,比如 -C、-c、--git-dir。出来的是 true 或 false,表示下一个参数是否应该被当作这个选项的值跳过。
调用关系:它只服务于 find_git_subcommand。find_git_subcommand 扫描 git 命令时,会靠它避免把选项后面的路径、配置值误认成 git 子命令。
调用图:被 1 处调用(find_git_subcommand);外部调用 1 个(matches!)。
is_git_global_option_with_inline_value59–69 ↗
fn is_git_global_option_with_inline_value(arg: &str) -> bool
作用:这个小函数判断 git 参数是不是“选项和值写在一起”的全局选项。比如 --git-dir=/tmp/repo 或 -C/tmp/repo,整段都不是子命令。
数据流:进去的是一个参数字符串。它检查这个字符串是否以 --git-dir=、--work-tree=、-C 加内容、-c 加内容等形式开头。出来的是 true 或 false,不改变任何数据。
调用关系:它被 find_git_subcommand 调用。find_git_subcommand 用它把这种连在一起写的 git 全局选项跳过去,继续寻找真正的 git 子命令。
调用图:被 1 处调用(find_git_subcommand);外部调用 1 个(matches!)。
executable_name_lookup_key71–95 ↗
fn executable_name_lookup_key(raw: &str) -> Option<String>
作用:这个函数把可执行文件路径整理成一个方便比较的名字。比如 /usr/bin/git 会变成 git;在 Windows 上,git.exe 也会被当成 git。
数据流:进去的是命令的第一个字符串,可能是 git,也可能是某个路径。它取出路径最后的文件名;Windows 上还会转成小写,并去掉 .exe、.cmd、.bat、.com 这些常见后缀。出来的是可比较的名字,或者在取不到合法名字时返回空结果。
调用关系:find_git_subcommand 用它确认这条命令是不是 git;is_safe_to_call_with_exec 也会用它做安全命令识别。它本身只做名字清洗,不判断安全与否。
调用图:被 2 处调用(find_git_subcommand, is_safe_to_call_with_exec);外部调用 1 个(new)。
find_git_subcommand101–143 ↗
fn find_git_subcommand(
command: &'a [String],
subcommands: &[&str],
) -> Option<(usize, &'a str)>
作用:这个函数负责在一条 git 命令里找到真正的 git 子命令,比如 status、diff、commit。它会跳过 git 的全局选项,避免被参数骗到。
数据流:进去的是整条命令列表,以及一组想找的子命令名字。它先确认第一个词是不是 git,然后从后面开始扫;遇到带值的全局选项就跳过选项和值,遇到写在一起的全局选项也跳过,遇到普通的第一个非选项词就把它当作 git 子命令。若这个子命令在目标列表里,就返回它的位置和名字;否则返回空。
调用关系:它被 is_safe_git_command 调用,用来判断某些 git 子命令是否属于允许范围。它内部依赖 executable_name_lookup_key 识别 git,也依赖两个 git 全局选项判断函数来正确跳过干扰项。
调用图:调用 3 个内部函数(executable_name_lookup_key, is_git_global_option_with_inline_value, is_git_global_option_with_value);被 1 处调用(is_safe_git_command)。
is_dangerous_to_call_with_exec145–157 ↗
fn is_dangerous_to_call_with_exec(command: &[String]) -> bool
作用:这个函数做最直接的通用危险命令检查。现在它主要盯住 rm -f 和 rm -rf 这类强制删除命令,也会看穿 sudo 后面包着的命令。
数据流:进去的是已经拆好的命令列表。它看第一个词:如果是 rm,就检查第二个词是不是 -f 或 -rf;如果是 sudo,就把 sudo 去掉,递归检查后面的真实命令;其他情况返回 false。出来的是是否危险的布尔值,不改动输入。
调用关系:command_might_be_dangerous 会调用它做核心的通用危险检查。它不负责 Windows 特例,也不负责拆 bash -lc 脚本,只判断已经摆在眼前的命令词。
调用图:被 1 处调用(command_might_be_dangerous);外部调用 1 个(matches!)。
tests::vec_str163–165 ↗
fn vec_str(items: &[&str]) -> Vec<String>
作用:这是测试里用的小帮手,把一组简短的字符串切片变成 Vec<String>。这样测试代码可以写得更短、更像真实命令参数。
数据流:进去的是一组字符串引用,比如 ["rm", "-rf", "/"]。它逐个复制成 String,最后出来一个 Vec<String>,供测试函数传给被测代码。
调用关系:它只在测试模块里使用,尤其被 PowerShell 测试调用。它不参与正式运行,只是让测试准备输入更方便。
tests::rm_rf_is_dangerous168–170 ↗
fn rm_rf_is_dangerous()
作用:这个测试确认 rm -rf / 会被认为是危险命令。它保护的是最典型、也最容易造成灾难的强制删除场景。
数据流:进去没有外部输入。测试内部构造 ["rm", "-rf", "/"],交给 command_might_be_dangerous,期望得到 true;如果不是 true,测试就失败。
调用关系:它通过 assert! 检查主入口 command_might_be_dangerous 的行为。它是安全规则的回归测试,防止以后改代码时不小心放过 rm -rf。
调用图:外部调用 1 个(assert!)。
tests::rm_f_is_dangerous173–175 ↗
fn rm_f_is_dangerous()
作用:这个测试确认 rm -f / 也会被认为是危险命令。虽然它没有 -r,但仍然是强制删除,所以也要被拦住。
数据流:进去没有外部输入。测试内部构造 ["rm", "-f", "/"],调用 command_might_be_dangerous,期望返回 true;如果返回 false,测试失败。
调用关系:它和 rm_rf_is_dangerous 一样,都是给 command_might_be_dangerous 的基础安全规则兜底。它用 assert! 表达“必须判危险”这个要求。
调用图:外部调用 1 个(assert!)。
tests::direct_powershell_words_reuse_windows_dangerous_detection178–186 ↗
fn direct_powershell_words_reuse_windows_dangerous_detection()
作用:这个测试确认 PowerShell 命令词会在 Windows 上走 Windows 专用危险判断,在非 Windows 上不会误报。它保证跨平台行为是有意设计的,而不是偶然结果。
数据流:进去没有外部输入。测试构造 ["Remove-Item", "test", "-Force"];如果当前平台是 Windows,就期望 is_dangerous_powershell_words 返回 true,否则期望返回 false。测试本身不改动任何全局状态。
调用关系:它调用 vec_str 准备输入,再用 cfg! 判断当前编译平台,用 assert! 检查结果。它主要覆盖 is_dangerous_powershell_words 这个平台分流函数。
shell-command/src/command_safety/windows_safe_commands.rs源码 ↗
在 Windows 上,同一句命令可能藏着很多危险写法,比如用 PowerShell 写文件、删文件,或让搜索工具偷偷启动别的程序。这个文件像门口保安:先确认命令是不是通过 powershell.exe 或 pwsh 启动;再把 PowerShell 脚本解析成一段段命令;最后逐段核对是否在“安全名单”里。安全名单主要是读文件、列目录、查找文本、查看 Git 信息这类只读动作。它也专门拦下 EncodedCommand、脚本文件执行、未知开关、重定向、动态变量、危险 cmdlet 等不透明写法。整体原则是:看不懂就不放行,宁可多问用户一次,也不冒险自动执行。
is_safe_command_windows8–17 ↗
fn is_safe_command_windows(command: &[String]) -> bool
作用:这是 Windows 命令安全检查的总入口。别人给它一串命令参数,它回答“这条命令能不能自动放行”。
数据流:输入是一组字符串,比如程序名和后面的参数。它先尝试把这组字符串当成 PowerShell 调用来解析;如果解析成功,就要求里面每一段命令都通过只读安全检查;如果不是 PowerShell 或解析失败,就直接返回不安全。
调用关系:上层的 is_known_safe_command 会在需要判断命令是否已知安全时调用它。它把第一步识别和拆分交给 try_parse_powershell_command_sequence,然后把拆出来的每段命令交给 PowerShell 白名单检查。
调用图:调用 1 个内部函数(try_parse_powershell_command_sequence);被 1 处调用(is_known_safe_command)。
try_parse_powershell_command_sequence21–28 ↗
fn try_parse_powershell_command_sequence(command: &[String]) -> Option<Vec<Vec<String>>>
作用:这个函数先确认命令是不是从 PowerShell 启动的。只有确认是 powershell 或 pwsh,它才继续拆里面真正要执行的命令。
数据流:输入是完整命令参数列表。它取第一个词当可执行文件名,检查是否是受支持的 PowerShell;是的话,把剩余参数交给解析函数;不是的话,返回空结果,表示不能按安全 PowerShell 命令处理。
调用关系:它被 is_safe_command_windows 调用,是第一道筛选门。它自己会用 is_powershell_executable 认程序名,再把 PowerShell 参数交给 parse_powershell_invocation。
调用图:调用 2 个内部函数(is_powershell_executable, parse_powershell_invocation);被 1 处调用(is_safe_command_windows)。
parse_powershell_invocation31–92 ↗
fn parse_powershell_invocation(executable: &str, args: &[String]) -> Option<Vec<Vec<String>>>
作用:这个函数看懂 PowerShell 的启动参数,找出真正的脚本内容。它会拒绝很多模糊或容易藏危险行为的启动方式。
数据流:输入是 PowerShell 程序名和参数。它允许一些无害开关,比如 NoLogo、NoProfile;遇到 -Command 或 -c 就取后面的脚本;遇到禁用的 -EncodedCommand、-File、未知开关等就失败;如果没有 -Command 但后面直接跟命令,就把这些参数重新拼成脚本再解析。输出是拆好的命令列表,或表示不安全的空结果。
调用关系:它由 try_parse_powershell_command_sequence 调用。它找到脚本后交给 parse_powershell_script;如果需要把零散参数拼回脚本,会用 join_arguments_as_script。
调用图:调用 2 个内部函数(join_arguments_as_script, parse_powershell_script);被 1 处调用(try_parse_powershell_command_sequence)。
parse_powershell_script96–104 ↗
fn parse_powershell_script(executable: &str, script: &str) -> Option<Vec<Vec<String>>>
作用:这个函数把一段 PowerShell 脚本文本交给真正的 PowerShell 语法解析器。它只接受解析器明确说“这是可拆分的命令列表”的结果。
数据流:输入是 PowerShell 可执行文件名和脚本文本。它调用 AST 解析器;AST 可以理解为“语法树”,就是把代码按语法结构拆开的结果。解析器返回命令列表时,它就把列表交出去;如果解析器认为脚本包含不支持或不安全的结构,它就返回失败。
调用关系:它被 parse_powershell_invocation 调用,是从启动参数进入脚本语法检查的桥。它把复杂语法判断交给 parse_with_powershell_ast。
调用图:调用 1 个内部函数(parse_with_powershell_ast);被 1 处调用(parse_powershell_invocation)。
is_powershell_executable107–118 ↗
fn is_powershell_executable(exe: &str) -> bool
作用:这个函数判断第一个命令词是不是 PowerShell 程序。它支持 powershell、powershell.exe、pwsh、pwsh.exe,也能处理带完整路径的情况。
数据流:输入是一个可执行文件字符串。它先从路径里取出文件名,转成小写,然后和允许的 PowerShell 名字比对;匹配就返回 true,否则返回 false。
调用关系:它被 try_parse_powershell_command_sequence 调用,决定后面的安全逻辑是否可以继续。内部用路径工具取文件名,用模式匹配做名字判断。
调用图:被 1 处调用(try_parse_powershell_command_sequence);外部调用 2 个(new, matches!)。
join_arguments_as_script120–129 ↗
fn join_arguments_as_script(args: &[String]) -> String
作用:这个函数把 PowerShell 后面直接跟着的多个参数拼回一段脚本文本。这样没有写 -Command 的调用也能走同一套解析流程。
数据流:输入是一组参数。第一个参数原样放进去,后面的参数如果有空格会被加引号保护,然后用空格连成一段脚本字符串。输出是拼好的脚本文本。
调用关系:它被 parse_powershell_invocation 在遇到“pwsh 命令 参数”这种形式时调用。它会把每个后续参数交给 quote_argument,确保拼回脚本时不会因为空格而变味。
调用图:调用 1 个内部函数(quote_argument);被 1 处调用(parse_powershell_invocation);外部调用 1 个(with_capacity)。
quote_argument131–141 ↗
fn quote_argument(arg: &str) -> String
作用:这个函数在需要时给单个参数加 PowerShell 风格的单引号。它的作用是让带空格的文件名之类不会被拆成多个词。
数据流:输入是一个参数字符串。空字符串会变成两个单引号;没有空白字符的参数原样返回;带空格的参数会包上单引号,并把里面已有的单引号转义。输出是适合拼进脚本的字符串。
调用关系:它只被 join_arguments_as_script 使用,是拼接脚本时的小工具。它使用格式化字符串来生成加引号后的结果。
调用图:被 1 处调用(join_arguments_as_script);外部调用 1 个(format!)。
is_safe_powershell_words145–207 ↗
fn is_safe_powershell_words(words: &[String]) -> bool
作用:这个函数检查一段已经解析好的 PowerShell 命令是不是只读安全命令。它是最后的白名单关卡。
数据流:输入是一段命令的词列表,比如命令名和参数。它先拒绝空命令,再扫描所有词,防止危险命令藏在括号或参数里;然后看第一个词是不是允许的读操作。普通读命令直接放行,git 和 rg 会做更细检查,写文件、删文件、启动进程等命令直接拒绝。
调用关系:它处在解析之后、最终放行之前。调用图把它归在逐条安全检查这一步;遇到 git 时交给 is_safe_git_command,遇到 rg 时交给 is_safe_ripgrep,其他命令按本文件里的白名单判断。
调用图:调用 2 个内部函数(is_safe_git_command, is_safe_ripgrep);被 1 处调用(is_safe_powershell_words);外部调用 1 个(matches!)。
is_safe_ripgrep210–222 ↗
fn is_safe_ripgrep(words: &[String]) -> bool
作用:这个函数专门检查 rg,也就是 ripgrep 文本搜索工具,是否用了危险选项。rg 本身常用于只读搜索,但某些选项可以启动外部程序,所以要额外拦住。
数据流:输入是 rg 命令的词列表。它跳过第一个 rg,检查后面的参数;如果发现 --pre、--hostname-bin、--search-zip、-z 等可能引入外部执行或复杂行为的选项,就返回不安全;否则返回安全。
调用关系:它由 is_safe_powershell_words 在识别到命令名是 rg 时调用。它是 PowerShell 白名单里对 rg 的细化补丁,避免“看起来是搜索,实际能跑程序”的情况。
调用图:被 1 处调用(is_safe_powershell_words)。
tests::vec_str232–234 ↗
fn vec_str(args: &[&str]) -> Vec<String>
作用:这是测试里用的小帮手,把一组字符串字面量变成真正拥有数据的 String 列表。这样测试用例写起来更短、更清楚。
数据流:输入是一组 &str,也就是借来的字符串片段。它逐个转换成 String,最后输出一个 Vec<String>,正好可以传给安全检查函数。
调用关系:它服务于本文件的多个测试。测试函数用它快速搭出命令参数列表,不参与正式运行时的安全判断。
tests::recognizes_safe_powershell_wrappers237–267 ↗
fn recognizes_safe_powershell_wrappers()
作用:这个测试确认常见 PowerShell 包装方式能被认作安全。它覆盖了 powershell.exe、pwsh、带 NoLogo 或 NoProfile 的情况。
数据流:测试输入是几组只读命令,比如列目录、git status、读文件。它把这些命令交给 is_safe_command_windows,期望结果都是 true;如果系统能找到 pwsh,也顺便测试 pwsh。
调用关系:它由测试框架运行。为了找到真实 pwsh,会调用 try_find_pwsh_executable_blocking;断言用来保证安全检查没有误杀这些正常只读命令。
调用图:调用 1 个内部函数(try_find_pwsh_executable_blocking);外部调用 1 个(assert!)。
tests::accepts_full_path_powershell_invocations270–290 ↗
fn accepts_full_path_powershell_invocations()
作用:这个测试确认即使 PowerShell 写成完整路径,也能被识别出来。现实里程序常常不会只写 powershell.exe,而是给出完整安装路径。
数据流:测试输入包括真实 pwsh 路径和 Windows 系统目录里的 powershell.exe 路径。它把这些路径作为命令第一个参数,再配上只读脚本,期望安全检查返回 true。
调用关系:它由测试框架运行,并在需要时用 try_find_pwsh_executable_blocking 找 pwsh。它也用 cfg! 判断平台,避免在非 Windows 的路径规则下误测。
调用图:调用 1 个内部函数(try_find_pwsh_executable_blocking);外部调用 2 个(assert!, cfg!)。
tests::allows_read_only_pipelines_and_git_usage293–333 ↗
fn allows_read_only_pipelines_and_git_usage()
作用:这个测试确认一些更复杂但仍然只读的管道命令能通过。管道可以理解为“前一个命令的结果交给后一个命令继续处理”。
数据流:测试输入包括 rg 搜索后计数、读文件后跳过前几行、git show 查看版本库内容、括号包住的读文件命令等。它们都只查看信息,不改东西,所以期望全部通过。
调用关系:它由测试框架运行。它先找 pwsh,然后通过断言验证 parse、白名单、git 检查和 rg 检查能配合放行这些只读组合。
调用图:调用 1 个内部函数(try_find_pwsh_executable_blocking);外部调用 1 个(assert!)。
tests::rejects_git_global_override_options336–368 ↗
fn rejects_git_global_override_options()
作用:这个测试确认 Git 的危险全局选项会被拦下。某些 Git 选项能改配置、换工作区或影响执行路径,虽然命令名还是 git,但风险已经变高。
数据流:测试输入是一批带 -c、--config-env、--git-dir、--work-tree、--exec-path 等选项的 git 命令。每条都交给 is_safe_command_windows,期望结果为 false。
调用关系:它由测试框架运行,并用 try_find_pwsh_executable_blocking 找 PowerShell。它主要验证 is_safe_powershell_words 转交给 is_safe_git_command 后,Git 的细粒度安全规则确实生效。
调用图:调用 1 个内部函数(try_find_pwsh_executable_blocking);外部调用 1 个(assert!)。
tests::rejects_git_subcommand_options_with_side_effects371–403 ↗
fn rejects_git_subcommand_options_with_side_effects()
作用:这个测试确认 Git 子命令里的危险选项也会被拒绝。问题不只在 git 的全局选项,diff、show、log 等子命令自己的参数也可能写文件或运行外部工具。
数据流:测试输入包括 git diff --output、git diff --ext-diff、git log --textconv、git show --output、git cat-file --filters 等。它收集每条命令的安全判断结果,再和预期的 false 列表比较。
调用关系:它由测试框架运行。它使用 assert_eq! 一次性比较所有结果,验证 Git 安全检查不会漏掉这些带副作用的子命令选项。
调用图:外部调用 1 个(assert_eq!)。
tests::rejects_stop_parsing_git_forms406–413 ↗
fn rejects_stop_parsing_git_forms()
作用:这个测试确认 PowerShell 的停止解析写法不会绕过 Git 安全检查。停止解析符号可能让后面的参数不按普通规则解释,容易藏危险参数。
数据流:输入是一条包含 git log --% 以及危险输出参数的命令。它交给 is_safe_command_windows,期望返回 false。
调用关系:它由测试框架运行。它用断言保证解析器和 Git 检查不会被特殊语法骗过去。
调用图:外部调用 1 个(assert!)。
tests::rejects_powershell_commands_with_side_effects416–512 ↗
fn rejects_powershell_commands_with_side_effects()
作用:这个测试集中验证各种会改东西、启动东西或语法太复杂的 PowerShell 命令都会被拒绝。
数据流:测试输入包括 Remove-Item、Set-Content、Out-File、重定向、调用运算符、先安全后危险的链式命令、括号里藏危险命令、数组展开、子表达式、空命令等。每条都送进安全检查,预期都是 false。
调用关系:它由测试框架运行。它覆盖本文件最重要的防线:解析器拒绝复杂结构,白名单拒绝写入和进程类命令,嵌套扫描拒绝把危险命令藏在参数里。
调用图:外部调用 1 个(assert!)。
tests::accepts_constant_expression_arguments515–527 ↗
fn accepts_constant_expression_arguments()
作用:这个测试确认普通的固定字符串参数可以通过。比如文件名里有空格,只要是明确写死的文本,就不应该被误判为危险。
数据流:测试输入是 Get-Content 后面跟单引号或双引号包住的文件名。它们交给安全检查后,期望返回 true。
调用关系:它由测试框架运行。它保证 PowerShell AST 解析和参数检查不会把安全的常量字符串误杀。
调用图:外部调用 1 个(assert!)。
tests::rejects_dynamic_arguments530–542 ↗
fn rejects_dynamic_arguments()
作用:这个测试确认动态参数会被拒绝。动态参数指运行时才展开的变量或插值字符串,安全检查无法提前知道它最终会变成什么。
数据流:测试输入包括 Get-Content $foo 和 Write-Output "foo $bar"。这些命令看起来可能只读,但参数内容不固定,所以期望安全检查返回 false。
调用关系:它由测试框架运行。它验证 parse_with_powershell_ast 和后续检查坚持“必须看得清楚才放行”的原则。
调用图:外部调用 1 个(assert!)。
tests::uses_invoked_powershell_variant_for_parsing545–572 ↗
fn uses_invoked_powershell_variant_for_parsing()
作用:这个测试确认解析时会尊重实际调用的是哪种 PowerShell。老的 Windows PowerShell 和新的 pwsh 支持的语法不完全一样,同一段脚本在两者里可能含义不同。
数据流:测试输入是包含 && 的命令链。它先用 powershell.exe 检查,预期不安全;如果能找到 pwsh,再用 pwsh 检查,预期可以按 pwsh 的语法判断为安全。
调用关系:它由测试框架运行。它调用 try_find_pwsh_executable_blocking 查找 pwsh,并用 cfg! 限定平台行为,验证 parse_powershell_script 把正确的可执行文件名传给 AST 解析器。
调用图:调用 1 个内部函数(try_find_pwsh_executable_blocking);外部调用 2 个(assert!, cfg!)。
shell-command/src/command_safety/is_safe_command.rs源码 ↗
程序在执行外部命令前,需要先分清哪些命令很安全,哪些必须让用户确认。这个文件做的就是这件事。它先把命令拆成一个个词来看,比如第一个词是 ls、git、find 还是 bash。对常见只读命令,比如 ls、cat、grep,会直接认为安全;但对容易“夹带私货”的命令会细查参数。比如 find 如果带 -delete 就不安全,base64 如果指定输出文件也不安全,git 只允许 status、log、diff、show 和很有限的 branch 查询。它还支持 bash -lc "..." 这种 shell 命令,但只接受简单命令组合,像 ls && pwd 可以,重定向、括号子 shell、rm 这类就不放行。Windows 上还会接入专门的 PowerShell 和 Windows 命令白名单。整体原则是宁可多拦一点,也不要误把会改系统的命令当安全。
is_known_safe_command12–50 ↗
fn is_known_safe_command(command: &[String]) -> bool
作用:这是这个文件最主要的入口,用来回答“这整条命令是不是已知安全”。调用者把已经切好的命令词传进来,它返回 true 或 false。
数据流:进去的是一串命令词,比如 ["bash", "-lc", "ls"]。它先把 zsh 当成 bash 来看,再在 Windows 上问 Windows 专用白名单,然后检查能不能直接执行为安全命令;如果是 bash -lc,它会把里面的脚本再拆成简单命令,逐条确认安全。出来的是一个布尔值:true 表示可以自动放行,false 表示不能放心放行。
调用关系:当上层的 render_decision_for_unmatched_command 遇到一条还没匹配到危险规则的命令时,会来问它。它自己把具体判断交给 is_safe_to_call_with_exec、parse_shell_lc_plain_commands,以及 Windows 平台上的 is_safe_command_windows。
调用图:调用 3 个内部函数(parse_shell_lc_plain_commands, is_safe_to_call_with_exec, is_safe_command_windows);被 1 处调用(render_decision_for_unmatched_command)。
is_safe_powershell_words54–65 ↗
fn is_safe_powershell_words(command: &[String]) -> bool
作用:这个函数专门判断一组已经拆好的 PowerShell 命令词是否足够只读、能自动通过。非 Windows 系统上它永远说不安全,避免误判。
数据流:进去的是 PowerShell 的词列表,比如 ["Get-Content", "Cargo.toml"]。在 Windows 上它转交给 Windows 专用白名单;在其他系统上它不尝试运行或探测任何东西,只返回 false。出来的是是否安全的判断,不改动输入。
调用关系:render_decision_for_unmatched_command 在处理 PowerShell 风格命令时会调用它。它的位置像一个平台分流口:Windows 交给真正的 Windows 安全规则,非 Windows 直接拒绝;调用图中也显示它与 git 安全判断路径有关,但实际核心用途是 PowerShell 白名单入口。
调用图:调用 1 个内部函数(is_safe_powershell_words);被 1 处调用(render_decision_for_unmatched_command)。
is_safe_to_call_with_exec67–173 ↗
fn is_safe_to_call_with_exec(command: &[String]) -> bool
作用:这个函数判断一条普通外部命令能不能直接执行而不用再问用户。它只认一小批明确只读的命令,并且会检查危险参数。
数据流:进去的是命令词列表,第一个词是程序名,后面是参数。它先把程序名规范化,比如处理路径里的 git.exe;然后按程序名套规则:普通只读命令直接通过,base64、find、rg、git、sed 会细看参数。出来的是 true 或 false,不执行命令,也不改文件。
调用关系:is_known_safe_command 会直接调用它,也会在解析 bash -lc 后对里面每条小命令调用它。它会进一步调用 executable_name_lookup_key 认出程序名,调用 is_safe_git_command 检查 git,调用 is_valid_sed_n_arg 检查 sed 的安全写法。
调用图:调用 3 个内部函数(executable_name_lookup_key, is_safe_git_command, is_valid_sed_n_arg);被 1 处调用(is_known_safe_command);外部调用 2 个(cfg!, matches!)。
is_safe_git_command175–200 ↗
fn is_safe_git_command(command: &[String]) -> bool
作用:这个函数专门判断 git 命令是不是只是在查看信息,而不是改仓库、换目录、调用外部工具。它让 git status、log、diff、show 和受限的 branch 查询可以自动通过。
数据流:进去的是完整 git 命令词。它先找到真正的 git 子命令,再把子命令前面的全局参数和子命令后面的参数分开检查:全局参数里有会改变环境或执行路径的选项就拒绝,子命令参数里有输出到文件、外部 diff、执行命令等选项也拒绝。出来的是是否安全的布尔值。
调用关系:is_safe_to_call_with_exec 认出第一个程序是 git 后会把判断交给它。它再调用 find_git_subcommand 找子命令,调用 git_has_unsafe_global_option 查全局危险选项,调用 git_subcommand_args_are_read_only 查子命令危险选项,遇到 git branch 还会调用 git_branch_is_read_only。
调用图:调用 4 个内部函数(find_git_subcommand, git_branch_is_read_only, git_has_unsafe_global_option, git_subcommand_args_are_read_only);被 2 处调用(is_safe_to_call_with_exec, is_safe_powershell_words);外部调用 1 个(debug_assert!)。
git_branch_is_read_only204–228 ↗
fn git_branch_is_read_only(branch_args: &[String]) -> bool
作用:这个函数判断 git branch 是不是只在“看分支”,而不是创建、删除或重命名分支。因为 git branch 同一个命令既能查看也能改东西,所以必须特别小心。
数据流:进去的是 git branch 后面的参数。没有参数时,它认为只是列出分支;有参数时,只允许 --list、--show-current、--all、--remotes、--verbose、--format=... 这类查询选项。只要看到其他参数或位置参数,就返回 false。
调用关系:is_safe_git_command 在确认子命令是 branch 后调用它。它和 git_subcommand_args_are_read_only 一起把关:一个防通用危险选项,一个防 branch 自己的改动用法。
调用图:被 1 处调用(is_safe_git_command)。
GitOptionPattern::matches268–276 ↗
fn matches(self, arg: &str) -> bool
作用:这个方法判断一个参数是否符合某种 git 危险选项的写法。它把“完全相等”“短选项后面直接跟值”“以某个前缀开头”这几种情况统一起来。
数据流:进去的是一个规则和一个命令参数字符串。它根据规则类型比较:Exact 要完全一样,ShortWithInlineValue 要像 -Cxxx 这样紧跟值,Prefix 要以指定前缀开头。出来的是 true 或 false。
调用关系:git_matches_option_pattern 会逐个拿规则调用它。它是 git 参数安全检查里的小齿轮,负责把不同写法的危险选项识别出来。
git_matches_option_pattern279–281 ↗
fn git_matches_option_pattern(arg: &str, patterns: &[GitOptionPattern]) -> bool
作用:这个函数检查某个 git 参数是否命中一组危险选项规则。它让调用者不用关心每条规则具体怎么匹配。
数据流:进去的是一个参数字符串和一组 GitOptionPattern 规则。它逐个规则调用 GitOptionPattern::matches,只要有一个命中就返回 true;全都没命中就返回 false。
调用关系:git_has_unsafe_global_option 和 git_subcommand_args_are_read_only 都依赖它。它位于 git 安全检查的中间层,把“遍历规则”的重复活儿集中到一处。
调用图:外部调用 1 个(iter)。
git_has_unsafe_global_option283–288 ↗
fn git_has_unsafe_global_option(global_args: &[String]) -> bool
作用:这个函数检查 git 子命令前面的全局参数里有没有危险选项。所谓全局参数,就是像 git -C dir status 里 -C dir 这种会影响整个 git 运行方式的参数。
数据流:进去的是 git 程序名之后、子命令之前的参数列表。它把每个参数拿去和 UNSAFE_GIT_GLOBAL_OPTIONS 里的规则比对;只要发现会换目录、换 git 目录、改配置、开分页器等选项,就返回 true。否则返回 false。
调用关系:is_safe_git_command 找到子命令后会先调用它。它把具体匹配工作交给 git_matches_option_pattern,帮助主流程尽早拒绝环境可被篡改的 git 命令。
调用图:被 1 处调用(is_safe_git_command)。
git_subcommand_args_are_read_only290–295 ↗
fn git_subcommand_args_are_read_only(args: &[String]) -> bool
作用:这个函数检查 git 子命令后面的参数有没有明显会写文件或调用外部工具的选项。它用于保护看似只读的 git log、diff、show 等命令。
数据流:进去的是子命令之后的参数。它逐个查是否命中 --output、--ext-diff、--textconv、--exec 等危险规则;发现一个就返回 false,全都没有就返回 true。
调用关系:is_safe_git_command 在处理 status、log、diff、show、branch 时都会用它。它调用 git_matches_option_pattern 做具体匹配,是 git 子命令参数的通用安全过滤器。
调用图:被 1 处调用(is_safe_git_command)。
is_valid_sed_n_arg304–334 ↗
fn is_valid_sed_n_arg(arg: Option<&str>) -> bool
作用:这个函数判断 sed -n 后面的打印范围是不是非常简单安全的格式,比如 10p 或 1,5p。它只允许打印指定行,不允许复杂 sed 脚本。
数据流:进去的是一个可能存在的字符串参数。它要求字符串必须以 p 结尾,前面必须是一个数字或两个用逗号隔开的数字;符合就返回 true,否则返回 false。
调用关系:is_safe_to_call_with_exec 在遇到 sed 时会调用它。只有 sed -n 加这种简单打印表达式,才会被认为是只读安全命令。
调用图:被 1 处调用(is_safe_to_call_with_exec)。
tests::vec_str341–343 ↗
fn vec_str(args: &[&str]) -> Vec<String>
作用:这是测试里用的小工具,把 [&str] 这种字符串片段数组变成 Vec<String>。这样测试用例写起来短一些。
数据流:进去的是字符串切片数组,比如 ["git", "status"]。它逐个转成拥有所有权的 String,出来的是 Vec<String>,方便传给被测函数。
调用关系:很多测试函数都会用它来准备输入数据。它不参与正式运行,只服务于测试代码。
tests::known_safe_examples346–377 ↗
tests::git_branch_mutating_flags_are_not_safe380–389 ↗
fn git_branch_mutating_flags_are_not_safe()
作用:这个测试确认会修改分支的 git branch 用法不会被自动放行。比如删除分支或创建新分支都应该被拦住。
数据流:测试把 git branch -d feature 和 git branch new-branch 传给 is_known_safe_command。期望结果都是 false。
调用关系:它主要保护 git_branch_is_read_only 这条规则,确保 git branch 只在查询分支时才算安全。
调用图:外部调用 1 个(assert!)。
tests::git_branch_global_options_respect_safety_rules392–406 ↗
fn git_branch_global_options_respect_safety_rules()
作用:这个测试检查 git branch 的安全判断是否同时遵守分支规则和 bash -lc 包装后的规则。它防止命令藏在 shell 字符串里绕过检查。
数据流:测试输入安全的 git branch --show-current,也输入不安全的删除分支命令,以及 bash -lc "git branch -d feature"。它断言前者通过,后两者拒绝。
调用关系:它覆盖 is_known_safe_command 到 bash 解析再到 git 判断的组合路径。
调用图:外部调用 1 个(assert!)。
tests::git_first_positional_is_the_subcommand409–415 ↗
fn git_first_positional_is_the_subcommand()
作用:这个测试确认 git 的第一个非选项词才是子命令,后面的词不能被误认成安全子命令。比如 git checkout status 不应该因为出现 status 就通过。
数据流:测试把 ["git", "checkout", "status"] 传入 is_known_safe_command。正确结果是 false。
调用关系:它保护 find_git_subcommand 和 is_safe_git_command 的配合逻辑,避免从后面的参数里误抓安全子命令。
调用图:外部调用 1 个(assert!)。
tests::git_output_flags_are_not_safe418–438 ↗
fn git_output_flags_are_not_safe()
作用:这个测试确认 git log、diff、show 如果带输出到文件的参数,就不能自动放行。因为写文件已经不是纯查看了。
数据流:测试构造带 --output=... 或 --output 后接路径的 git 命令。每条都送进 is_known_safe_command,并断言结果是 false。
调用关系:它验证 git_subcommand_args_are_read_only 对 --output 规则的拦截效果。
调用图:外部调用 1 个(assert!)。
tests::git_global_pagination_flags_are_not_safe441–461 ↗
fn git_global_pagination_flags_are_not_safe()
作用:这个测试确认 git 的全局分页器选项不会被当成安全。分页器可能触发外部程序,所以这里选择保守拒绝。
数据流:测试输入 git --paginate log、git -p log,以及它们放进 bash -lc 后的版本。所有结果都应为 false。
调用关系:它覆盖 git_has_unsafe_global_option,同时也检查 bash -lc 解析后的安全判断没有绕过 git 全局参数规则。
调用图:外部调用 1 个(assert!)。
tests::git_subcommand_patch_flags_remain_safe464–475 ↗
fn git_subcommand_patch_flags_remain_safe()
作用:这个测试确认 git log -p、git diff -p、git show -p 这种子命令里的 -p 仍然安全。这里的 -p 是显示补丁,不是全局分页器。
数据流:测试把带 -p 的 log、diff、show 命令,以及 bash -lc 包装版本传入 is_known_safe_command。期望全部返回 true。
调用关系:它防止规则过度扩大,把子命令里的普通显示选项误判成 git 全局危险选项。
调用图:外部调用 1 个(assert!)。
tests::git_global_override_flags_are_not_safe478–527 ↗
fn git_global_override_flags_are_not_safe()
作用:这个测试覆盖大量 git 全局危险选项,比如换目录、改配置、换 git-dir、换 work-tree、改执行路径等。它确保这些能改变 git 行为环境的参数都必须被拦住。
数据流:测试逐条构造带 -C、-c、--config-env、--git-dir、--work-tree、--exec-path、--namespace、--super-prefix 等参数的 git 命令,并断言 is_known_safe_command 返回 false。它也检查 bash -lc 里的同类命令。
调用关系:它主要验证 UNSAFE_GIT_GLOBAL_OPTIONS、GitOptionPattern::matches、git_has_unsafe_global_option 和 is_safe_git_command 这一串逻辑。
tests::cargo_check_is_not_safe530–532 ↗
fn cargo_check_is_not_safe()
作用:这个测试确认 cargo check 不在自动安全白名单里。即使它看起来常见,也可能运行构建脚本或触发复杂行为,所以不能默认放行。
数据流:测试把 ["cargo", "check"] 交给 is_known_safe_command。期望结果是 false。
调用关系:它保护白名单的保守原则:没有明确列入并证明只读的命令,就不自动批准。
调用图:外部调用 1 个(assert!)。
tests::zsh_lc_safe_command_sequence535–537 ↗
fn zsh_lc_safe_command_sequence()
作用:这个测试确认 zsh -lc 的简单安全命令会按 bash -lc 类似方式处理。文件里会把 zsh 当成 bash 来识别。
数据流:测试输入 ["zsh", "-lc", "ls"],调用 is_known_safe_command。期望返回 true。
调用关系:它验证 is_known_safe_command 开头把 zsh 替换成 bash 的兼容处理。
调用图:外部调用 1 个(assert!)。
tests::unknown_or_partial540–566 ↗
tests::base64_output_options_are_unsafe569–581 ↗
tests::ripgrep_rules584–615 ↗
tests::windows_powershell_full_path_is_safe618–636 ↗
fn windows_powershell_full_path_is_safe()
作用:这个测试只在 Windows 上检查:即使用完整路径调用 PowerShell,安全白名单也能识别。比如 C 盘里找到的 powershell 或 pwsh 路径。
数据流:测试先在 Windows 上尝试找到 PowerShell 可执行文件路径,然后构造完整路径加 -Command Get-Location 的命令,断言 is_known_safe_command 返回 true。非 Windows 会直接跳过。
调用关系:它验证 Windows 专用安全规则和可执行文件名识别能处理完整路径。它会调用 try_find_pwsh_executable_blocking 或 try_find_powershell_executable_blocking 来找 PowerShell。
调用图:调用 1 个内部函数(try_find_pwsh_executable_blocking);外部调用 2 个(assert!, cfg!)。
tests::windows_git_full_path_is_safe639–648 ↗
fn windows_git_full_path_is_safe()
作用:这个测试只在 Windows 上确认完整路径的 git.exe 也能被识别为 git。否则安全的 git status 可能因为路径太长而被误拦。
数据流:测试构造 C:\Program Files\Git\cmd\git.exe status,并在 Windows 上断言 is_known_safe_command 返回 true。非 Windows 直接跳过。
调用关系:它验证 executable_name_lookup_key 和 git 安全判断在 Windows 路径上的配合。
tests::bash_lc_safe_examples651–680 ↗
fn bash_lc_safe_examples()
作用:这个测试确认 bash -lc 里放简单只读命令时,可以自动放行。它覆盖 ls、git status、grep、sed 和安全的 find。
数据流:测试把多条 ["bash", "-lc", "..."] 命令传给 is_known_safe_command。里面的脚本都是简单命令,没有重定向、括号或危险参数,所以期望都返回 true。
调用关系:它验证 parse_shell_lc_plain_commands 和 is_safe_to_call_with_exec 的组合:先拆 shell 字符串,再逐条套安全白名单。
调用图:外部调用 1 个(assert!)。
tests::bash_lc_safe_examples_with_operators683–704 ↗
fn bash_lc_safe_examples_with_operators()
作用:这个测试确认 bash -lc 里使用有限的安全连接符也可以通过。比如 &&、||、分号和管道,只要每个小命令本身安全。
数据流:测试输入 grep ... || true、ls && pwd、echo 'hi' ; ls、ls | wc -l。解析后每个片段都是安全命令,所以断言整体安全。
调用关系:它保护 bash 解析器与 is_known_safe_command 的组合规则:连接符本身不算危险,但连接的每条命令都必须安全。
调用图:外部调用 1 个(assert!)。
tests::bash_lc_unsafe_examples707–743 ↗
fn bash_lc_unsafe_examples()
作用:这个测试确认 bash -lc 中稍微复杂或危险的写法会被拒绝。比如参数数量不对、把整条命令额外加引号、find -delete、rm、括号子 shell、重定向。
数据流:测试构造多种 shell 字符串并送进 is_known_safe_command。只要解析不出简单安全命令,或者包含危险命令和危险语法,就断言返回 false。
调用关系:它验证 parse_shell_lc_plain_commands 的保守边界,以及 is_known_safe_command 不会因为外面是 bash -lc 就盲目放行。
调用图:外部调用 1 个(assert!)。
tests::direct_powershell_words_use_windows_safelist746–754 ↗
fn direct_powershell_words_use_windows_safelist()
作用:这个测试确认直接给 PowerShell 词列表时,会使用 Windows 白名单;非 Windows 则不会误判为安全。
数据流:测试构造 ["Get-Content", "Cargo.toml"]。如果运行在 Windows,期望 is_safe_powershell_words 返回 true;否则期望 false。
调用关系:它直接测试 is_safe_powershell_words 的平台分流行为,确保 PowerShell 安全规则只在合适系统上启用。
tests::non_windows_safe_classification_does_not_spawn_repo_powershell_path758–801 ↗
fn non_windows_safe_classification_does_not_spawn_repo_powershell_path()
作用:这个 Unix 测试确认:在非 Windows 上做安全判断时,不会真的启动一个看起来像 PowerShell 的程序。安全检查必须只是检查,不能有副作用。
数据流:测试创建一个临时假的 pwsh 可执行文件,如果它被运行就会写 marker 文件。然后把这个假 pwsh 命令传给 is_known_safe_command,期望返回 false,并确认 marker 文件不存在。最后清理临时目录。
调用关系:它保护非 Windows 分支的安全边界,确保 is_known_safe_command 和 is_safe_powershell_words 不会为了识别命令而执行外部程序。它用文件系统操作搭建和清理测试现场。
调用图:外部调用 10 个(now, assert!, format!, create, create_dir, metadata, remove_dir_all, set_permissions, temp_dir, writeln!)。