路径、文件系统、环境与沙箱支持工具
这一层像后台工具箱,不是主流程,却支撑启动命令、读写文件、沙箱运行和终端界面。路径工具先把各种 Windows、Linux、WSL、URI 写法统一成可靠地址;文件系统工具负责安全打开文件、监听变化、判断新旧。环境工具给子进程和沙箱整理变量,找可执行程序,避免泄密或找错。沙箱和平台专用代码再处理 macOS、Linux、Windows 的权限、链接、SSH 配置、剪贴板和终端颜色,让上层代码少操心系统差异。
路径标识和转换
这些文件定义核心路径抽象和转换层,用于跨平台表示绝对路径、规范文件 URI 和面向 API 的路径字符串。
utils/path-uri/src/api_path_string.rs源码 ↗
这个文件像一个“路径翻译员”。外部 API 还在用普通 UTF-8 字符串传路径,比如 POSIX 风格的“/home/a”或 Windows 风格的“C盘路径、网络共享路径”。但项目内部正在迁到 PathUri,也就是用 file:// 这类统一格式表达路径。LegacyAppPathString 就是老接口边界上的包装盒:它能保留外部传来的原始字符串,但项目内部不能随便从 String 造它,避免绕过检查。转换时必须明确选择 PathConvention,也就是路径语法:Posix 或 Windows。文件还处理一些麻烦情况,比如 Windows 盘符、UNC 网络路径、不能正常解释的“兜底二进制路径”、百分号编码、非 UTF-8 内容。简单说,它保证“接口看到的是老样子的路径字符串,内部拿到的是规范路径”,中间尽量不丢信息,遇到不兼容就明确报错。
LegacyAppPathString::from_abs_path37–39 ↗
fn from_abs_path(path: &AbsolutePathBuf) -> Self
作用:把一个已经确认是绝对路径的本机路径,变成老 API 要的普通字符串路径。有人需要把内部路径交给旧接口时会用它。
数据流:进去的是 AbsolutePathBuf,也就是“已经确认不是相对路径”的路径对象;函数把它按当前机器的路径写法转成字符串,遇到非 UTF-8 内容会用替代字符尽量保住可显示文本;出来的是 LegacyAppPathString。
调用关系:它是最直接的出口之一。From<AbsolutePathBuf> 的实现会转交给它,权限相关和审批请求相关流程也会用它把内部路径填回老接口字段。
调用图:调用 1 个内部函数(to_string_lossy);被 4 处调用(from, additional_file_system_permissions_populates_entries_for_legacy_roots, app_server_exec_approval_request_preserves_permissions_context, app_server_request_permissions_preserves_file_system_permissions)。
LegacyAppPathString::from_path_uri48–60 ↗
fn from_path_uri(
path: &PathUri,
convention: PathConvention,
) -> Result<Self, LegacyAppPathStringError>
作用:把内部统一的 PathUri 翻译成老 API 需要的本地路径字符串,并且可以指定要翻译成 POSIX 写法还是 Windows 写法。
数据流:进去的是 PathUri 和 PathConvention;它先看 PathUri 有没有“兜底二进制路径”,有就走兜底渲染;没有就按选择的语法分别渲染成 POSIX 或 Windows 字符串;出来要么是 LegacyAppPathString,要么是说明不兼容的错误。
调用关系:它是 PathUri 输出到老 API 的主要通道。调用方如 remote_cwd 和相关测试会用它;它把具体工作分给 render_opaque_fallback、render_posix_path 或 render_windows_path。
调用图:调用 4 个内部函数(opaque_fallback_bytes, render_opaque_fallback, render_posix_path, render_windows_path);被 3 处调用(remote_cwd, renders_native_paths_from_shared_cases, serializes_and_deserializes_as_a_string)。
LegacyAppPathString::to_path_uri64–76 ↗
fn to_path_uri(
&self,
convention: PathConvention,
) -> Result<PathUri, LegacyAppPathStringError>
作用:把老 API 传来的路径字符串,按指定的 POSIX 或 Windows 规则解析成内部统一的 PathUri。
数据流:进去的是 LegacyAppPathString 自己保存的字符串和一个路径语法;它按语法调用对应解析器;如果能看出这是绝对路径,就产出 PathUri;如果是相对路径或格式不对,就返回 InvalidNativePath 错误。
调用关系:它是老 API 输入进入内部世界的主要入口。它不自己拆所有细节,而是把 POSIX 字符串交给 parse_posix_path,把 Windows 字符串交给 parse_windows_path。
调用图:调用 2 个内部函数(parse_posix_path, parse_windows_path)。
LegacyAppPathString::infer_absolute_path_convention83–97 ↗
fn infer_absolute_path_convention(&self) -> Option<PathConvention>
作用:根据路径字符串长什么样,猜它是 POSIX 绝对路径还是 Windows 绝对路径。它用于没有明确说明路径语法时做一个保守判断。
数据流:进去的是内部那段路径文本;它检查是否像 Windows 盘符根路径或 Windows 网络路径,再检查是否以斜杠开头像 POSIX 路径;出来是 Some(Posix)、Some(Windows),或者无法判断时的 None。
调用关系:它不做真正转换,只做“看外形猜分类”。后续如果要正式变成 PathUri,还需要调用 to_path_uri 并指定语法。
调用图:外部调用 1 个(matches!)。
LegacyAppPathString::as_str99–101 ↗
fn as_str(&self) -> &str
作用:借用里面的路径字符串,不拿走所有权。调用者只是想看看或临时使用文本时会用它。
数据流:进去的是 LegacyAppPathString;函数直接返回内部 String 的字符串视图;对象本身不变,也不会分配新内存。
调用关系:这是一个轻量读取口。它让外部代码能读到 API 字符串,但不能绕过这个类型直接乱改内部内容。
LegacyAppPathString::into_string103–105 ↗
fn into_string(self) -> String
作用:把 LegacyAppPathString 拆开,拿出里面真正的 String。调用者确定不再需要这个包装类型时会用它。
数据流:进去的是整个 LegacyAppPathString;函数消耗掉这个对象,把内部字符串原样交出去;之后原对象不能再用。
调用关系:这是离开包装盒的出口。它不做校验、不做转换,只用于已经接受这个字符串形态的地方。
LegacyAppPathString::from109–111 ↗
fn from(path: AbsolutePathBuf) -> Self
作用:让 AbsolutePathBuf 可以用标准的 from 转换方式变成 LegacyAppPathString。它是为了写代码更顺手。
数据流:进去的是 AbsolutePathBuf;它借用这条路径并交给 from_abs_path;出来的是老 API 路径字符串包装。
调用关系:它只是标准转换接口的一层薄包装,真正的转换由 LegacyAppPathString::from_abs_path 完成。
调用图:外部调用 1 个(from_abs_path)。
parse_posix_path114–122 ↗
fn parse_posix_path(path: &str) -> Option<PathUri>
作用:按 POSIX 规则解析路径字符串,也就是 Linux/macOS 常见的以斜杠开头的绝对路径。
数据流:进去的是普通字符串;它先要求字符串必须以“/”开头,说明是绝对路径;如果含有空字符,就用不透明兜底方式保存原始字节;否则按斜杠拆成一段段路径并组成 PathUri;解析失败就返回 None。
调用关系:它由 LegacyAppPathString::to_path_uri 在选择 Posix 时调用。它把正常路径继续交给 path_uri_from_segments,把特殊路径交给 PathUri 的兜底构造。
调用图:调用 2 个内部函数(from_opaque_path_bytes, path_uri_from_segments);被 1 处调用(to_path_uri);外部调用 1 个(format!)。
parse_windows_path124–160 ↗
fn parse_windows_path(path: &str) -> Option<PathUri>
作用:按 Windows 规则解析路径字符串,包括盘符路径、网络共享路径,以及一些特殊命名空间路径。
数据流:进去的是普通字符串;它先识别 Windows 特殊前缀或空字符,这类内容用不透明方式保存;再识别盘符根路径;再识别双分隔符开头的 UNC 网络路径;能解析就产出 PathUri,不能确认是绝对 Windows 路径就返回 None。
调用关系:它由 LegacyAppPathString::to_path_uri 在选择 Windows 时调用。正常可拆的路径交给 path_uri_from_segments,特殊或复杂路径交给 windows_opaque_path_uri。
调用图:调用 2 个内部函数(path_uri_from_segments, windows_opaque_path_uri);被 1 处调用(to_path_uri);外部调用 2 个(matches!, once)。
path_uri_from_segments162–178 ↗
fn path_uri_from_segments(
host: Option<&str>,
segments: impl Iterator<Item = &'a str>,
) -> Option<PathUri>
作用:把已经拆好的路径片段拼成规范的 file:// 形式 PathUri。它像是把一串地址零件装进统一信封里。
数据流:进去的是可选主机名和一串路径片段;它先创建 file:/// URL,必要时设置主机名,然后把每个片段放进 URL 路径;最后尝试转成 PathUri;成功返回 Some(PathUri),失败返回 None。
调用关系:POSIX 和 Windows 解析器都会用它处理“能正常拆成路径段”的情况。它把字符串级别的解析结果推进到内部 PathUri 表示。
调用图:调用 1 个内部函数(try_from);被 2 处调用(parse_posix_path, parse_windows_path);外部调用 1 个(parse)。
windows_opaque_path_uri180–186 ↗
fn windows_opaque_path_uri(path: &str) -> PathUri
作用:把不能安全拆解的 Windows 路径,按原始 Windows 字符编码方式塞进 PathUri 的兜底区域。
数据流:进去的是 Windows 路径字符串;它把字符串编码成 UTF-16 小端字节,也就是 Windows 常用的宽字符字节形式;出来的是带不透明字节的 PathUri。
调用关系:parse_windows_path 在遇到命名空间路径、空字符或 UNC 正常解析失败时会用它,目的是不强行解释、尽量保留原始内容。
调用图:调用 1 个内部函数(from_opaque_path_bytes);被 1 处调用(parse_windows_path)。
is_windows_separator_char188–190 ↗
fn is_windows_separator_char(character: char) -> bool
作用:判断一个字符是不是 Windows 路径分隔符。Windows 里反斜杠和斜杠都可能被当作分隔符。
数据流:进去的是一个 char;它检查是否为反斜杠或斜杠;出来是 true 或 false。
调用关系:它是 parse_windows_path 拆分路径时的小工具,帮助同一套逻辑同时接受两种 Windows 分隔符。
调用图:外部调用 1 个(matches!)。
is_windows_separator_byte192–194 ↗
fn is_windows_separator_byte(character: u8) -> bool
作用:判断一个字节是不是 Windows 路径分隔符。它用于还没按字符处理、只看原始字节形状的场景。
数据流:进去的是一个 u8 字节;它检查是否等于反斜杠或斜杠的字节值;出来是 true 或 false。
调用关系:它被 Windows 路径识别逻辑使用,比如判断盘符后面是不是根分隔符,或判断路径是不是双分隔符开头。
调用图:外部调用 1 个(matches!)。
render_opaque_fallback196–211 ↗
fn render_opaque_fallback(
path: &PathUri,
path_bytes: &[u8],
convention: PathConvention,
) -> Result<String, LegacyAppPathStringError>
作用:把 PathUri 里保存的“不透明兜底路径字节”尽量还原成本地路径字符串。还原不了时明确报错。
数据流:进去的是 PathUri、它的兜底字节,以及目标路径语法;如果目标是 POSIX 且字节以斜杠开头,就按 UTF-8 损耗式转字符串;如果目标是 Windows,就交给 Windows 兜底渲染;否则返回 OpaqueFallback 错误。
调用关系:LegacyAppPathString::from_path_uri 发现 PathUri 带兜底字节时会先调用它。它再把 Windows 情况交给 render_windows_opaque_fallback。
调用图:调用 1 个内部函数(render_windows_opaque_fallback);被 1 处调用(from_path_uri);外部调用 1 个(from_utf8_lossy)。
render_windows_opaque_fallback213–238 ↗
fn render_windows_opaque_fallback(path_bytes: &[u8]) -> Option<String>
作用:把 Windows 兜底字节还原为 Windows 路径字符串,并确认它看起来像绝对路径。
数据流:进去的是一段字节;它要求字节数必须是偶数,再按 UTF-16 小端还原成字符;然后检查是否有盘符根路径或双分隔符开头的网络/命名空间根;符合就返回字符串,不符合就返回 None。
调用关系:它只服务于 render_opaque_fallback。这样可以把 Windows 特有的编码和绝对路径判断集中在一个地方。
调用图:被 1 处调用(render_opaque_fallback);外部调用 1 个(matches!)。
is_windows_separator240–242 ↗
fn is_windows_separator(character: u16) -> bool
作用:判断一个 UTF-16 字符值是不是 Windows 路径分隔符。它用于处理 Windows 宽字符路径。
数据流:进去的是一个 u16,也就是 UTF-16 单元;它比较是否等于反斜杠或斜杠;出来是 true 或 false。
调用关系:render_windows_opaque_fallback 在检查盘符根路径和 UNC 根路径时会用它。
调用图:外部调用 1 个(from)。
LegacyAppPathString::fmt245–247 ↗
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
作用:让 LegacyAppPathString 能像普通文本一样被打印出来。比如日志或错误信息里需要显示路径时会用到。
数据流:进去的是格式化器和这个路径对象;它把内部字符串写入格式化器;出来是格式化是否成功的结果。
调用关系:这是 Rust 的 Display 显示接口实现。它不参与转换,只提供友好的文字输出。
调用图:外部调用 1 个(write_str)。
LegacyAppPathString::serialize251–256 ↗
fn serialize(&self, serializer: S) -> Result<S::Ok, S::Error>
作用:把 LegacyAppPathString 序列化成 JSON 等格式里的普通字符串。这样老 API 看到的仍然是一个字符串,而不是复杂对象。
数据流:进去的是序列化器和路径对象;它把内部字符串交给 serializer.serialize_str;出来是序列化框架需要的结果。
调用关系:当接口响应、配置或消息需要写出这个类型时会用它。它配合 Deserialize 的透明包装,让这个类型在外部表现得像 string。
调用图:外部调用 1 个(serialize_str)。
LegacyAppPathString::schema_name260–262 ↗
fn schema_name() -> String
作用:告诉 JSON Schema 生成器这个类型叫什么名字。JSON Schema 可以理解成给 JSON 数据写的说明书。
数据流:进去没有业务数据;函数返回固定字符串“LegacyAppPathString”;不改动任何状态。
调用关系:它是 JsonSchema 接口的一部分,供自动生成 API 文档或类型说明时使用。
LegacyAppPathString::json_schema264–266 ↗
fn json_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema
作用:告诉文档生成工具:这个类型在 JSON 里其实就是一个字符串。
数据流:进去的是 schema 生成器;它复用 String 的 schema 生成逻辑;出来的是字符串类型的 JSON Schema。
调用关系:它和 serialize 的思想一致:内部是受控包装,外部文档和网络格式里仍然显示为 string。
调用图:外部调用 1 个(json_schema)。
render_posix_path269–285 ↗
fn render_posix_path(path: &PathUri) -> Result<String, LegacyAppPathStringError>
作用:把 PathUri 渲染成 POSIX 路径字符串,也就是用斜杠分隔、以根目录开头的形式。
数据流:进去的是 PathUri;它先转成 URL,拒绝带主机名的 file URL,因为 POSIX 路径无法安全表达 UNC 服务器身份;然后逐段解码路径片段并用斜杠拼回去;出来是字符串或不兼容错误。
调用关系:LegacyAppPathString::from_path_uri 在目标语法是 Posix 时会调用它。它依赖 path_segments 取路径段,依赖 decode_native_segment 解码每一段,遇到不合适就用 incompatible_convention 造错误。
调用图:调用 4 个内部函数(to_url, decode_native_segment, incompatible_convention, path_segments);被 1 处调用(from_path_uri);外部调用 1 个(new)。
render_windows_path287–334 ↗
fn render_windows_path(path: &PathUri) -> Result<String, LegacyAppPathStringError>
作用:把 PathUri 渲染成 Windows 路径字符串,包括盘符路径和 UNC 网络共享路径。
数据流:进去的是 PathUri;它转成 URL 后,如果有主机名,就生成 UNC 网络路径并要求有共享名;如果没有主机名,就要求第一段是像“C:”这样的盘符;之后把剩余路径段解码并用 Windows 分隔符连接;出来是字符串或不兼容错误。
调用关系:LegacyAppPathString::from_path_uri 在目标语法是 Windows 时会调用它。它把取片段交给 path_segments,把百分号解码交给 decode_native_segment,把错误构造交给 incompatible_convention。
调用图:调用 4 个内部函数(to_url, decode_native_segment, incompatible_convention, path_segments);被 1 处调用(from_path_uri);外部调用 1 个(new)。
path_segments336–339 ↗
fn path_segments(url: &url::Url) -> std::str::Split<'_, char>
作用:从一个已经验证过的 file URL 里取出路径片段迭代器。可以把它理解成拿到“路径一段一段的清单”。
数据流:进去的是 URL;它调用 URL 库的 path_segments,并假定合法 file URL 一定能取到路径段;出来是按斜杠拆开的片段迭代器。
调用关系:render_posix_path 和 render_windows_path 都用它统一读取 URI 路径段,避免两边重复写取片段的代码。
调用图:被 2 处调用(render_posix_path, render_windows_path);外部调用 1 个(path_segments)。
decode_native_segment341–346 ↗
fn decode_native_segment(segment: &str) -> String
作用:把 URL 路径片段里的百分号编码解开一次,再转成普通路径文字。比如空格常会写成百分号编码。
数据流:进去的是一个 URL 路径片段;它先按 URL 编码规则解成字节,再用 UTF-8 损耗式转成字符串;出来是本地路径里要显示的片段文本。
调用关系:render_posix_path 和 render_windows_path 在拼回本地路径前都会调用它。它故意只解码一次,避免把本来想表示文字的编码再次误解成路径分隔符。
调用图:被 2 处调用(render_posix_path, render_windows_path);外部调用 2 个(from_utf8_lossy, decode_binary)。
incompatible_convention348–353 ↗
fn incompatible_convention(path: &PathUri, convention: PathConvention) -> LegacyAppPathStringError
作用:生成“这个 PathUri 不能按某种路径语法表示”的错误。它让报错信息统一、清楚。
数据流:进去的是 PathUri 和目标 PathConvention;它把 PathUri 转成字符串放进错误里;出来是 LegacyAppPathStringError::IncompatibleConvention。
调用关系:render_posix_path 和 render_windows_path 遇到无法安全表示的路径时会调用它,比如把 Windows 形状硬渲染成 POSIX 或反过来。
调用图:被 2 处调用(render_posix_path, render_windows_path);外部调用 1 个(to_string)。
PathConvention::native392–394 ↗
fn native() -> Self
作用:返回当前程序运行机器默认使用的路径语法。Windows 上是 Windows,Unix 系统上是 Posix。
数据流:进去没有参数;编译时根据目标系统选择实现;出来是 PathConvention::Windows 或 PathConvention::Posix。
调用关系:需要“按本机习惯处理路径”的地方会调用它,比如路径转换和路径语法选择逻辑。它避免调用方自己到处判断操作系统。
调用图:被 2 处调用(try_from, path_convention)。
utils/path-uri/src/lib.rs源码 ↗
文件路径看起来简单,但一旦要在不同系统之间传递,就很容易乱:Windows 有盘符和共享路径,Unix 有字节路径,URI 里还可能有用户名、端口、问号参数、井号片段等对文件路径没意义的东西。这个文件把这些复杂性包进 PathUri 里。它只接受 file: 开头的 URI,并在创建时检查掉危险或无意义的内容。它还能从本机绝对路径生成 URI;如果某个路径没法正常写成 URI,就放进一个特殊的“坏路径保险箱”:用 base64(一种把任意字节变成安全文本的编码)保存原始路径,之后还能尽量还原。它还提供取文件名、取父目录、拼接相对路径、推断路径风格、转回本机绝对路径等常用操作。整体像一个“文件地址海关”:所有路径进出都先验明身份,保证后面的代码拿到的是规整、可解释的地址。
PathUri::parse62–64 ↗
fn parse(uri: &str) -> Result<Self, PathUriParseError>
作用:把一段文字解析成 PathUri,并确认它真的是合法的 file: 文件 URI。别人用它来避免随便一串字符串混进文件路径系统。
数据流:输入是一段 URI 字符串 → 它先交给通用 URL 解析器拆开,再转成 PathUri 并做 file: 专属检查 → 成功时得到一个不可随便篡改的 PathUri,失败时得到明确的解析错误。
调用关系:这是最直接的入口之一。远程工作目录、测试协议、非本机路径处理等地方都会先调用它,把外部传来的文字变成受控的 PathUri。它后续把具体校验交给 PathUri::try_from。
调用图:被 30 处调用(remote_cwd, helper_protocol_uses_path_uris, non_native_cwd, non_native_uri, start_process_rejects_non_native_cwd_before_launch, non_native_cwd, non_native_uri, renders_native_paths_from_shared_cases, serializes_and_deserializes_as_a_string, bad_path_uris_are_opaque_to_lexical_operations (+15 more));外部调用 1 个(parse)。
PathUri::from_abs_path76–98 ↗
fn from_abs_path(path: &AbsolutePathBuf) -> Self
作用:把当前机器上的绝对路径变成 file: URI。它特别重要,因为真实文件路径可能包含普通 URI 表达不了的内容。
数据流:输入是 AbsolutePathBuf,也就是已经确认是绝对路径的路径对象 → 它先尝试按标准方式转成 file: URL,再校验成 PathUri;如果失败,就把原始路径字节编码进特殊 fallback URI → 输出总是一个 PathUri。
调用关系:文件读写、建目录、删文件、打补丁等很多功能都会用它把本机路径统一变成 PathUri。遇到棘手路径时,它会把活交给 PathUri::from_opaque_path_bytes 做兜底保存。
调用图:调用 1 个内部函数(as_path);被 100 处调用(copy, create_directory, get_metadata, read_directory, read_file, remove, write_file, apply_hunks_to_files, derive_new_contents_from_chunks, ensure_not_directory (+15 more));外部调用 3 个(from_opaque_path_bytes, try_from, from_file_path)。
PathUri::from_opaque_path_bytes100–106 ↗
fn from_opaque_path_bytes(path_bytes: &[u8]) -> Self
作用:把无法正常表示的路径原始字节藏进一个特殊 file: URI 里。可以把它理解成“路径急救箱”。
数据流:输入是一串原始路径字节 → 它用 URL 安全的 base64 编码成文本,再拼到固定的坏路径前缀后面并解析成 PathUri → 输出一个看起来是 file: URI、但内容是封装字节的特殊 PathUri。
调用关系:主要由 PathUri::from_abs_path 在普通转换失败时使用,测试里也会专门构造这种情况。它依赖 PathUri::parse 保证生成结果仍然符合 PathUri 的规则。
调用图:被 2 处调用(parse_posix_path, windows_opaque_path_uri);外部调用 3 个(parse, format!, unreachable!)。
PathUri::from_path113–117 ↗
fn from_path(path: impl AsRef<Path>) -> io::Result<Self>
作用:接收一个普通路径,并要求它必须是绝对路径,然后转成 PathUri。它给调用者一个更方便但更严格的入口。
数据流:输入是任何像路径的值 → 它先检查是不是绝对路径;如果是,就包装成 AbsolutePathBuf,再调用 PathUri::from_abs_path;如果不是,就返回输入无效的错误 → 输出是 PathUri 或 io 错误。
调用关系:很多上层功能在写文件、读文件、创建目录、加载项目配置时会用它。它负责入口把关,真正的 URI 生成交给 PathUri::from_abs_path。
调用图:调用 1 个内部函数(from_absolute_path_checked);被 89 处调用(create_dir_all, read_file_text, write_file, fresh_thread_composes_global_before_project_and_reports_sources, multi_environment_thread_loads_every_project_and_keeps_creation_snapshot, apply_patch_turn_diff_tracks_local_and_remote_environment_paths, apply_patch_approvals_are_remembered_per_environment, apply_patch_freeform_routes_to_selected_remote_environment, apply_patch_intercepted_exec_command_routes_to_selected_remote_environment, exec_command_routes_to_selected_remote_environment (+15 more));外部调用 1 个(from_abs_path)。
PathUri::encoded_path123–125 ↗
fn encoded_path(&self) -> &str
作用:取出 URI 里的路径部分,而且保持百分号编码后的原样。它适合需要看 URI 字面路径、但不想碰主机名的场景。
数据流:输入是已有的 PathUri → 它读取内部 Url 的 path 字段 → 输出类似 /share/file.rs 这样的路径字符串,不包含 file://server 里的 server。
调用关系:PathUri::parent 会用它判断当前 URI 是否已经是根目录。它是一个很小的读取工具,不改动 PathUri。
调用图:被 1 处调用(parent)。
PathUri::opaque_fallback_bytes127–129 ↗
fn opaque_fallback_bytes(&self) -> Option<Vec<u8>>
作用:判断这个 PathUri 是不是“坏路径保险箱”,如果是,就取出里面保存的原始路径字节。
数据流:输入是 PathUri → 它检查内部 URL 是否符合特殊坏路径前缀,并尝试 base64 解码 → 如果匹配就输出原始字节,否则输出 None。
调用关系:PathUri::infer_path_convention 和其他路径渲染逻辑会用它识别 fallback URI。实际解码工作交给 decode_bad_path_uri。
调用图:调用 1 个内部函数(decode_bad_path_uri);被 2 处调用(infer_path_convention, from_path_uri)。
PathUri::infer_path_convention147–165 ↗
fn infer_path_convention(&self) -> Option<PathConvention>
作用:猜这个 URI 更像 Unix/POSIX 路径,还是 Windows 路径。这里的 POSIX 可以简单理解成 Linux/macOS 那类用斜杠开头的路径规则。
数据流:输入是 PathUri → 如果是 fallback URI,就查看原始字节;否则看 URI 有没有主机名、有没有 Windows 盘符形状的路径段 → 输出 PathConvention::Windows、PathConvention::Posix,或无法判断的 None。
调用关系:它在需要展示或解释跨平台路径时使用。它先问 PathUri::opaque_fallback_bytes 有没有原始字节,再把特殊情况交给 infer_opaque_path_convention。
调用图:调用 2 个内部函数(opaque_fallback_bytes, infer_opaque_path_convention)。
PathUri::basename172–181 ↗
fn basename(&self) -> Option<String>
作用:取这个文件 URI 最后一级名字,比如从一个文件路径里拿到 file.rs。对根目录或特殊 fallback URI,它会返回没有结果。
数据流:输入是 PathUri → 它先排除 fallback URI,然后从路径段末尾往前找第一个非空段,并尽量把百分号编码解成人能读的文字 → 输出文件名字符串或 None。
调用关系:这是给上层显示文件名、判断最后一级路径用的便捷方法。它会先用 decode_bad_path_uri 避免对特殊封装路径做错误的普通路径拆分。
调用图:调用 1 个内部函数(decode_bad_path_uri)。
PathUri::parent185–199 ↗
fn parent(&self) -> Option<Self>
作用:取当前 URI 的父目录。比如 file:///a/b/c 会变成 file:///a/b。
数据流:输入是 PathUri → 它先判断是不是根目录或 fallback URI;不是的话,复制内部 URL,删除最后一个路径段 → 输出父目录 PathUri,或在没有父级时输出 None。
调用关系:需要向上找目录的代码会用它。它通过 PathUri::encoded_path 判断根路径,并用 decode_bad_path_uri 排除不能安全拆分的特殊 URI。
调用图:调用 2 个内部函数(encoded_path, decode_bad_path_uri);外部调用 1 个(unreachable!)。
PathUri::join209–246 ↗
fn join(&self, path: &str) -> Result<Self, PathUriParseError>
作用:把一个相对路径拼到当前 URI 后面。它像在文件夹里继续往下走,但不允许传入绝对路径来“跳出起点”。
数据流:输入是当前 PathUri 和一段相对路径文字 → 它拒绝以斜杠开头的绝对路径、拒绝空字符;空路径直接返回原 URI;普通路径会忽略空段和点号,遇到双点号就向上退一级,但不会越过根 → 输出新的 PathUri 或错误。
调用关系:上层需要基于目录定位子文件时会用它。它先用 decode_bad_path_uri 排除特殊 fallback URI,拼完后再交给 PathUri::try_from 重新校验结果。
调用图:调用 1 个内部函数(decode_bad_path_uri);外部调用 3 个(try_from, unreachable!, JoinPathMustBeRelative)。
PathUri::to_abs_path258–309 ↗
fn to_abs_path(&self) -> io::Result<AbsolutePathBuf>
作用:把 PathUri 转回当前机器能使用的绝对路径。它只适合 URI 确实指向当前机器路径的情况。
数据流:输入是 PathUri → 如果是 fallback URI,就按当前系统规则解出原始路径字节并再次验证;如果是普通 file: URI,就用 URL 库转成本机路径,再检查它是绝对路径 → 输出 AbsolutePathBuf,失败时输出输入无效的 io 错误。
调用关系:读文件、查元数据、规范化路径、沙箱工作目录等真正碰本机文件系统的地方会用它。它会调用 decode_bad_path_uri、AbsolutePathBuf 检查,并用 PathUri::from_abs_path 做一次回环确认,避免解错。
调用图:调用 2 个内部函数(from_absolute_path_checked, decode_bad_path_uri);被 22 处调用(canonicalize, read_file, get_metadata, read_file, get_metadata, read_file, native_sandbox_cwd, canonicalize, copy, create_directory (+12 more));外部调用 4 个(from_abs_path, new, from_vec, from)。
PathUri::to_url312–314 ↗
fn to_url(&self) -> Url
作用:拿到内部标准 Url 的副本。适合那些确实需要 URL 对象能力的代码。
数据流:输入是 PathUri → 它复制内部 Url → 输出一个新的 Url,原 PathUri 不变。
调用关系:渲染 POSIX 路径和 Windows 路径的代码会用它。它不做校验,因为 PathUri 创建时已经校验过。
调用图:被 2 处调用(render_posix_path, render_windows_path)。
PathUri::try_from335–337 ↗
fn try_from(uri: String) -> Result<Self, Self::Error>
作用:把通用 Url 转成受限制的 PathUri。它是从“普通 URL”进入“安全文件 URI”的关口。
数据流:输入是 Url → 它检查 scheme 是否等于 file;然后检查用户名、密码、端口、参数、片段、空字符等规则;最后把 localhost 这种本地主机别名去掉 → 输出 PathUri 或具体错误。
调用关系:PathUri::parse、PathUri::join、反序列化等入口都会间接走到这里。它把细节校验交给 validate_file_url,并让 without_localhost_authority 做规范化。
调用图:调用 2 个内部函数(validate_file_url, without_localhost_authority);被 1 处调用(path_uri_from_segments);外部调用 3 个(parse, scheme, UnsupportedScheme)。
PathUri::deserialize341–370 ↗
fn deserialize(deserializer: D) -> Result<Self, D::Error>
作用:让 PathUri 可以从 JSON 或其他 serde 数据格式里读出来。serde 是 Rust 常用的序列化框架,用来把数据结构和文本互相转换。
数据流:输入是反序列化器提供的字符串 → 它先尝试按 URI 解析;如果是合法 file: URI 就返回 PathUri;如果看起来不像 URI,就再尝试当成本机绝对路径兼容旧数据 → 输出 PathUri 或 serde 错误。
调用关系:配置、协议消息、保存的数据里读 PathUri 时会自动调用它。它会调用 Url::parse、PathUri::try_from、AbsolutePathBuf 检查和 PathUri::from_abs_path。
调用图:调用 1 个内部函数(from_absolute_path_checked);外部调用 6 个(from_abs_path, try_from, deserialize, parse, custom, InvalidUri)。
PathUri::from_str376–378 ↗
fn from_str(uri: &str) -> Result<Self, Self::Err>
作用:支持用 Rust 的标准字符串解析方式生成 PathUri,比如调用 "...".parse()。这是给常见写法准备的接口。
数据流:输入是一段字符串 → 它直接调用 PathUri::parse → 输出 PathUri 或 PathUriParseError。
调用关系:它是标准 FromStr trait 的实现,方便泛型代码或测试代码统一解析。真正工作都交给 PathUri::parse。
调用图:外部调用 1 个(parse)。
PathUri::fmt382–384 ↗
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
作用:定义 PathUri 被打印成字符串时长什么样。结果就是它的规范 URI 文本。
数据流:输入是 PathUri 和格式化输出器 → 它把内部 Url 写进去 → 输出格式化结果,不改变 PathUri。
调用关系:当代码使用 to_string、日志打印、错误消息展示 PathUri 时会自动用到它。它让 PathUri 的显示形式和内部 Url 保持一致。
PathUri::serialize388–393 ↗
fn serialize(&self, serializer: S) -> Result<S::Ok, S::Error>
作用:让 PathUri 可以写进 JSON 或其他 serde 数据格式里。它把复杂对象保存成一条规范 URI 字符串。
数据流:输入是 PathUri 和序列化器 → 它读取内部 Url 的字符串形式并写出 → 输出序列化框架需要的结果。
调用关系:协议消息、配置保存、测试快照等需要输出 PathUri 时会自动调用它。它只负责写字符串,不重新解析。
调用图:外部调用 1 个(serialize_str)。
PathUri::schema_name397–399 ↗
fn schema_name() -> String
作用:给 JSON Schema 里的这个类型起名。JSON Schema 可以理解成“这份 JSON 应该长什么样”的说明书。
数据流:没有业务输入 → 它返回固定名字 PathUri → 输出给 schema 生成器使用。
调用关系:生成 API 或配置 schema 时会用到它。它和 PathUri::json_schema 一起告诉外部工具:PathUri 在数据里其实按字符串看待。
PathUri::json_schema401–403 ↗
fn json_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema
作用:声明 PathUri 在 JSON Schema 里按字符串处理。这样外部系统知道它不是一个复杂对象。
数据流:输入是 schema 生成器 → 它复用 String 的 schema 规则 → 输出字符串类型的 schema。
调用关系:生成接口文档或类型说明时会调用它。它配合 PathUri::schema_name,让 PathUri 对外表现为一条 URI 字符串。
调用图:外部调用 1 个(json_schema)。
without_localhost_authority407–414 ↗
fn without_localhost_authority(mut url: Url) -> Url
作用:把 file://localhost/... 规范化成 file:///...。localhost 表示本机,去掉它能减少同一路径的两种写法。
数据流:输入是 Url → 它检查主机名是不是 localhost;如果是,就清掉主机名;如果不是,就保留 → 输出规范化后的 Url。
调用关系:PathUri::try_from 在完成校验后会调用它。它只去掉本机别名,不会删除真正的网络共享主机名。
调用图:被 1 处调用(try_from);外部调用 3 个(host_str, set_host, unreachable!)。
decode_uri_path421–425 ↗
fn decode_uri_path(path: &str) -> String
作用:把 URI 路径里的百分号编码尽量还原成人能读的文字。比如 %20 会变成空格。
数据流:输入是一段 URI 路径文本 → 它尝试按 UTF-8 解码;如果成功就返回解码后的文字,如果遇到非 UTF-8 字节就保留原样 → 输出字符串。
调用关系:主要服务于取文件名这类展示用途。它的策略很保守:能读就读,不能读就不硬猜,避免丢失信息。
调用图:外部调用 1 个(decode)。
decode_bad_path_uri428–439 ↗
fn decode_bad_path_uri(url: &Url) -> Option<Vec<u8>>
作用:识别并解开特殊的坏路径 fallback URI。它负责确认这真是本文件定义的“保险箱”,不是普通路径碰巧长得像。
数据流:输入是 Url → 它检查是否以固定坏路径前缀开头,确认后面只有一段 base64 文本,再解码并重新编码核对 → 成功输出原始字节,否则输出 None。
调用关系:basename、join、parent、to_abs_path、validate_file_url、opaque_fallback_bytes 都会用它区分普通 URI 和特殊封装 URI。它是 fallback 机制的核心识别器。
调用图:被 6 处调用(basename, join, opaque_fallback_bytes, parent, to_abs_path, validate_file_url);外部调用 1 个(as_str)。
is_windows_drive_uri_segment441–443 ↗
fn is_windows_drive_uri_segment(segment: &str) -> bool
作用:判断某个 URI 路径段是不是 Windows 盘符形状,比如 C:。这是推断路径风格的小工具。
数据流:输入是一段路径段文字 → 它检查是否正好是一个英文字母加冒号 → 输出 true 或 false。
调用关系:PathUri::infer_path_convention 会用它判断 file:///C:/src 这类写法更像 Windows 路径。它只看字面形状,不访问文件系统。
调用图:外部调用 1 个(matches!)。
infer_opaque_path_convention445–462 ↗
fn infer_opaque_path_convention(path_bytes: &[u8]) -> Option<PathConvention>
作用:从 fallback URI 里保存的原始字节猜路径属于 POSIX 还是 Windows。因为这种 URI 没有普通路径结构,只能看字节特征。
数据流:输入是原始路径字节 → 如果以斜杠字节开头,就认为是 POSIX;否则尝试按 Windows UTF-16LE 字节看前两个字符,识别盘符或双反斜杠共享路径 → 输出路径约定或 None。
调用关系:PathUri::infer_path_convention 在遇到 opaque fallback URI 时会调用它。它不保证百分百知道来源,只提供安全的启发式判断。
调用图:被 1 处调用(infer_path_convention);外部调用 2 个(from, try_from)。
validate_common_known_uri465–479 ↗
fn validate_common_known_uri(url: &Url) -> Result<(), PathUriParseError>
作用:拒绝那些对文件 URI 没有明确定义的附加信息,比如用户名、密码、端口、查询参数、片段。这样可以避免同一个文件地址被塞进奇怪含义。
数据流:输入是 Url → 它逐项检查 username、password、port、query、fragment → 如果都没有就返回成功,否则返回对应错误。
调用关系:validate_file_url 会先调用它做通用 URI 卫生检查。它是所有 file: URI 校验的第一层过滤网。
调用图:被 1 处调用(validate_file_url);外部调用 5 个(fragment, password, port, query, username)。
validate_file_url482–494 ↗
fn validate_file_url(url: &Url) -> Result<(), PathUriParseError>
作用:对 file: URL 做最终安全检查。它确保这个 URL 不带无意义元数据,也不包含普通本机路径无法安全表达的空字节。
数据流:输入是 Url → 它先调用 validate_common_known_uri;再解码路径字节检查是否有空字符,除非它是本文件认可的特殊 fallback URI → 成功返回空结果,失败返回 PathUriParseError。
调用关系:PathUri::try_from 会调用它作为创建 PathUri 的核心校验步骤。它还会借助 decode_bad_path_uri,避免把合法 fallback URI 错误拒绝掉。
调用图:调用 2 个内部函数(decode_bad_path_uri, validate_common_known_uri);被 1 处调用(try_from);外部调用 3 个(path, to_string, decode_binary)。
utils/absolute-path/src/lib.rs源码 ↗
项目里很多地方都要处理文件路径。普通路径可能是“./file”、可能是“~/code”,也可能带有 Windows 的特殊前缀;如果每个调用方都自己猜,很容易出错。这个文件把这些麻烦收进一个小工具里:AbsolutePathBuf 表示“已经变成绝对路径的 PathBuf”。创建它时,会先展开家目录的 ~,再按平台整理路径,然后把相对路径接到一个明确的基准目录上。反序列化(把配置里的文字读成程序里的值)时,还用 AbsolutePathBufGuard 临时放一个“基准目录”,这样配置里的相对路径知道该从哪里算起。文件还特别处理符号链接(类似快捷方式):有时真正路径会被改写到目标位置,但这里提供了保留用户看到的逻辑路径的函数,避免界面和运行时路径不一致。
AbsolutePathBuf::maybe_expand_home_directory27–43 ↗
fn maybe_expand_home_directory(path: &Path) -> PathBuf
作用:把路径开头的“~”换成用户的家目录,比如把“~/code”变成“/home/you/code”。这样配置里常见的写法可以直接用。
数据流:输入一个路径 → 它先把路径尽量看成文字,再查系统家目录;如果路径正好是“~”或以“~/”开头,就替换成真实家目录,否则原样复制 → 输出一个新的 PathBuf,不改原路径。
调用关系:这是创建绝对路径前的第一道清理工序。AbsolutePathBuf::resolve_path_against_base、AbsolutePathBuf::from_absolute_path 和 AbsolutePathBuf::from_absolute_path_checked 都会先让它处理“~”。
调用图:外部调用 4 个(to_path_buf, to_str, cfg!, home_dir)。
AbsolutePathBuf::resolve_path_against_base45–56 ↗
fn resolve_path_against_base(
path: P,
base_path: B,
) -> Self
作用:把任意路径变成绝对路径;如果传进来的是相对路径,就按给定的基准目录来算。比如基准是“/app”,路径是“logs/a.txt”,结果就是“/app/logs/a.txt”。
数据流:输入一个待处理路径和一个基准目录 → 先展开“~”,再整理 Windows 特殊写法,然后把路径相对基准目录转成规范的绝对路径 → 输出 AbsolutePathBuf。
调用关系:这是很多配置和文件操作共同使用的入口。配置写入、补丁应用、测试缓存、反序列化等场景都会调用它,因为这些地方经常允许用户写相对路径。
调用图:调用 2 个内部函数(absolutize_from, normalize_path_for_platform);被 53 处调用(config_batch_write_applies_multiple_edits, config_value_write_replaces_value, apply_hunks_to_files, resolve_path, create_test_cache, new, create_test_cache, home_relative_path_fields_are_allowed_and_resolved, relative_absolute_path_fields_resolve_against_base_dir, load_config_layers_state (+15 more));外部调用 3 个(as_ref, as_ref, maybe_expand_home_directory)。
AbsolutePathBuf::from_absolute_path58–62 ↗
fn from_absolute_path(path: P) -> std::io::Result<Self>
作用:把一个路径转成 AbsolutePathBuf,并尽量把里面的“.”、“..”这类绕路写法整理掉。它接受已经绝对的路径,也能处理某些需要补全的情况。
数据流:输入一个路径 → 展开“~”,按平台整理,再交给底层 absolutize 规则变成绝对且简化的路径 → 成功时输出 AbsolutePathBuf,失败时返回系统 I/O 错误。
调用关系:这是从外部路径进入这个安全类型的常用门口。服务器 socket 路径、权限请求、测试路径和序列化相关代码都会用它。
调用图:调用 2 个内部函数(absolutize, normalize_path_for_platform);被 271 处调用(remote_unix_socket_typed_request_roundtrip_works, app_server_control_socket_path, app_server_startup_lock_path, absolute_path, test_socket_path, test_startup_lock_path, request_permissions_response_accepts_explicit_child_grant_for_requested_cwd_scope, request_permissions_response_ignores_broader_cwd_grant_for_requested_child_path, request_permissions_response_rejects_child_grant_outside_requested_cwd_scope, apply_edits (+15 more));外部调用 2 个(as_ref, maybe_expand_home_directory)。
AbsolutePathBuf::from_absolute_path_checked64–78 ↗
fn from_absolute_path_checked(path: P) -> std::io::Result<Self>
作用:更严格地创建 AbsolutePathBuf:传进来的路径必须本来就是绝对路径。它适合那些不想偷偷接受相对路径的场景。
数据流:输入一个路径 → 展开“~”并整理平台写法 → 检查它是不是绝对路径;不是就返回 InvalidInput 错误,是就清理后包装成 AbsolutePathBuf → 输出成功值或错误。
调用关系:插件、配置加载、测试辅助等地方会用它来守住边界:这里要求调用方明确给绝对路径,不允许含糊。
调用图:调用 2 个内部函数(absolutize_from, normalize_path_for_platform);被 36 处调用(model_provider_auth_from_proto, loader_translates_sources_to_config_layers, host_and_executor_sources_parse_the_same_manifest, selected_plugin_root, malformed_preferred_manifest_does_not_fall_through_to_alternate, plugin_root_resolution_uses_supplied_executor_file_system, try_new, load_default_with_cli_overrides_for_codex_home, with_models_provider_home_and_state_for_tests, malformed_declared_config_is_an_error (+15 more));外部调用 5 个(as_ref, new, maybe_expand_home_directory, new, format!)。
AbsolutePathBuf::current_dir80–82 ↗
fn current_dir() -> std::io::Result<Self>
作用:取得当前进程所在目录,并包装成 AbsolutePathBuf。这样后续代码不用再担心当前目录是不是绝对路径。
数据流:不需要业务输入 → 读取系统当前工作目录 → 用 AbsolutePathBuf::from_absolute_path 整理包装 → 输出当前目录或系统错误。
调用关系:启动、命令执行、工作区判断等流程会用它拿“现在从哪里运行”。它把系统返回的普通 PathBuf 交给本文件的绝对路径规则处理。
调用图:被 67 处调用(cancellation_expiration_keeps_process_alive_until_terminated, timeout_or_cancellation_reports_cancellation_without_timeout_exit_code, windows_sandbox_exec_request, run_main, arg0_dispatch, workspace_dir, build_inner, default_thread_environment_selections_empty_when_default_disabled, default_thread_environment_selections_use_manager_default_id, latest_environment_update_wins_while_previous_resolution_is_pending (+15 more));外部调用 2 个(from_absolute_path, current_dir)。
AbsolutePathBuf::relative_to_current_dir86–91 ↗
fn relative_to_current_dir(path: P) -> std::io::Result<Self>
作用:把一个路径按当前工作目录来解释。用户输入“file.txt”时,它能变成“当前目录/file.txt”。
数据流:输入一个路径 → 读取系统当前目录 → 调用 AbsolutePathBuf::resolve_path_against_base,把输入路径接到当前目录上 → 输出 AbsolutePathBuf 或读取当前目录失败的错误。
调用关系:监听地址、工作目录配置、线程列表过滤、socket 路径解析等地方会用它,因为这些输入常常允许写相对路径。
调用图:被 21 处调用(from_listen_url, resolve_cwd_config, normalize_thread_list_cwd_filters, thread_from_stored_thread, normalize_thread_list_cwd_filter_resolves_relative_paths_against_server_cwd, summary_to_thread, parse_allow_unix_socket_path, parse_socket_path, collect_explicit_skill_mentions, build_inner (+11 more));外部调用 2 个(resolve_path_against_base, current_dir)。
AbsolutePathBuf::join93–95 ↗
fn join(&self, path: P) -> Self
作用:在已有绝对路径下面拼一个子路径,并保证结果仍然是 AbsolutePathBuf。它比普通拼接更安全,因为会顺手处理“..”等路径片段。
数据流:输入当前 AbsolutePathBuf 和要拼上的路径 → 把当前路径当作基准目录,调用 resolve_path_against_base → 输出新的 AbsolutePathBuf。
调用关系:加载 codex home、插件 hook、manifest 文件、默认 skill 目录等地方都会用它。它是“从一个确定目录继续往下找文件”的常用零件。
调用图:被 39 处调用(from_core_with_cwd, new, load_from_codex_home, project_ignored_config_keys_warning, default_skill_roots, load_plugin_hooks, write_hook_file, write_manifest, resolve_plugin_root, update_personal_marketplace (+15 more));外部调用 1 个(resolve_path_against_base)。
AbsolutePathBuf::canonicalize97–99 ↗
fn canonicalize(&self) -> std::io::Result<Self>
作用:把这个路径交给操作系统做真实化处理,确认它存在,并解析符号链接等。适合必须访问真实文件的地方。
数据流:输入当前 AbsolutePathBuf → 调用 dunce::canonicalize 读取文件系统信息 → 成功输出新的 AbsolutePathBuf,路径不存在或不可访问就返回错误。
调用关系:canonicalize_if_exists 会调用它。它和后面的 preserving_symlinks 系列不同,这个函数按系统标准真实化,不特意保留逻辑路径。
调用图:被 1 处调用(canonicalize_if_exists);外部调用 1 个(canonicalize)。
AbsolutePathBuf::parent101–109 ↗
fn parent(&self) -> Option<Self>
作用:取得当前路径的上一级目录,并保持“绝对路径”的保证。比如“/a/b.txt”的父目录是“/a”。
数据流:输入当前 AbsolutePathBuf → 向标准路径对象询问父目录 → 如果有父目录,就包装成 AbsolutePathBuf;如果已经没有父目录,就返回 None。
调用关系:保存文件、查找 git 根目录、读取 skill 元数据等地方会用它向上找目录。它只做一层上移,不负责循环查找。
调用图:被 12 处调用(new_add_for_test, write_file_with_missing_parent_retry, save, find_git_checkout_root, load_requirements_toml, default_skill_name, load_skill_metadata, read_resolved_agent_role_file, write_shell_snapshot, default_output_csv_path (+2 more))。
AbsolutePathBuf::ancestors111–119 ↗
fn ancestors(&self) -> impl Iterator<Item = Self> + '_
作用:从当前路径开始,一路列出它自己、父目录、祖父目录,直到根目录。适合“往上找项目根目录”这类任务。
数据流:输入当前 AbsolutePathBuf → 使用标准路径的 ancestors 迭代器逐级向上 → 每一级都包装成 AbsolutePathBuf 输出给调用方逐个使用。
调用关系:项目根目录查找、配置层加载、git 入口查找和插件命名空间判断会用它。它提供的是一串候选目录,真正选哪个由调用方决定。
调用图:被 6 处调用(find_project_root, load_project_layers, dirs_between_project_root_and_cwd, find_project_root, find_ancestor_git_entry_with_fs, plugin_namespace_for_skill_path)。
AbsolutePathBuf::as_path121–123 ↗
fn as_path(&self) -> &Path
作用:把 AbsolutePathBuf 暂时当作普通 Path 借出去用,不转移所有权。很多系统 API 只认识 Path,所以需要这个出口。
数据流:输入当前 AbsolutePathBuf → 返回里面 PathBuf 的只读 Path 引用 → 不复制数据,也不改变路径。
调用关系:网络 socket、锁文件、命令运行、文件内容生成等很多地方都会用它和标准库或系统接口对接。它是安全类型通向普通路径 API 的桥。
调用图:被 58 处调用(connect_unix_socket_endpoint, drop, acquire_app_server_startup_lock, start_control_socket_acceptor, create_empty_user_layer, derive_new_contents_from_chunks, run_command_under_windows_session, wait_for_foreground_remote_control_ready, cloud_config_layers_from_fragments_impl, validate_fragment_strictly (+15 more))。
AbsolutePathBuf::into_path_buf125–127 ↗
fn into_path_buf(self) -> PathBuf
作用:把 AbsolutePathBuf 拆回普通 PathBuf,并把所有权交出去。适合调用方后面不再需要“绝对路径类型”的包装时使用。
数据流:输入一个拥有所有权的 AbsolutePathBuf → 取出内部 PathBuf → 输出 PathBuf,原 AbsolutePathBuf 被消耗掉。
调用关系:PathBuf::from 这个转换实现会调用它。它是从安全包装回到标准路径类型的正式出口。
调用图:被 1 处调用(from)。
AbsolutePathBuf::to_path_buf129–131 ↗
fn to_path_buf(&self) -> PathBuf
作用:复制一份普通 PathBuf 出来,但保留原来的 AbsolutePathBuf。适合调用方需要自己持有一份路径副本。
数据流:输入当前 AbsolutePathBuf 的引用 → 克隆内部 PathBuf → 输出一份新的 PathBuf,原值不变。
调用关系:插件路径规范化、codex home、测试添加路径等地方会用它,因为这些流程需要把路径保存到别的结构里。
调用图:被 18 处调用(new_add_for_test, invalid_marketplace_layout_error, normalize_git_plugin_source_url, normalize_relative_git_plugin_source_url, normalize_remote_plugin_subdir, resolve_local_plugin_source_path, personal_marketplace_relative_plugin_path, codex_home, rebuild_preserving_session_layers, to_mcp_config_with_plugin_registrations (+8 more))。
AbsolutePathBuf::to_string_lossy133–135 ↗
fn to_string_lossy(&self) -> std::borrow::Cow<'_, str>
作用:把路径转成能显示的字符串;如果路径里有不标准的字符,也尽量用替代字符显示出来,不直接失败。
数据流:输入当前 AbsolutePathBuf → 调用底层路径的宽容字符串转换 → 输出 Cow 字符串;可能是借来的,也可能是新建的。
调用关系:提示词、命令展示、策略说明、元数据名字等地方会用它把路径拿来给人看或拼成文字。
调用图:被 9 处调用(from, normalized_path, prompt, commands_for_intercepted_exec_policy, join_program_and_argv, seatbelt_protected_metadata_name_regex, prepare_escalated_exec, prepare_escalated_exec, from_abs_path)。
AbsolutePathBuf::display137–139 ↗
fn display(&self) -> Display<'_>
作用:得到一个专门用于打印路径的显示对象。它不会立刻生成字符串,但可以安全地放进格式化输出里。
数据流:输入当前 AbsolutePathBuf → 返回内部路径的 Display 包装 → 调用方之后在日志或错误信息中格式化它。
调用关系:codex home 展示、快照校验、hook 来源说明等地方会用它输出人类可读的路径。
调用图:被 5 处调用(codex_home, validate_snapshot, write_shell_snapshot, hook_handler_source, unmanaged_hook_handler_source)。
normalize_path_for_platform142–151 ↗
fn normalize_path_for_platform(path: &Path) -> Cow<'_, Path>
作用:按操作系统整理路径写法,主要是清理 Windows 上的特殊设备路径前缀。这样同一路径不会因为写法奇怪而在项目里变成两种样子。
数据流:输入一个 Path → 如果是 Windows 且能转成文字,就尝试 normalize_windows_device_path;能整理就返回新的 PathBuf,否则借用原路径 → 输出 Cow,也就是“可能借用原值,也可能拥有新值”的包装。
调用关系:from_absolute_path、from_absolute_path_checked 和 resolve_path_against_base 在真正绝对化前都会调用它。它把平台差异挡在核心路径逻辑外面。
调用图:调用 1 个内部函数(normalize_windows_device_path);被 3 处调用(from_absolute_path, from_absolute_path_checked, resolve_path_against_base);外部调用 5 个(Borrowed, Owned, to_str, from, cfg!)。
normalize_windows_device_path153–171 ↗
fn normalize_windows_device_path(path: &str) -> Option<String>
作用:识别并去掉 Windows 常见的“\\?\”或“\\.\”这类特殊前缀,只保留普通人熟悉的路径写法。
数据流:输入路径字符串 → 检查它是不是 UNC 网络路径或盘符绝对路径的特殊形式 → 能识别就返回普通写法字符串,不能识别就返回 None。
调用关系:normalize_path_for_platform 只在 Windows 场景下会借助它。它还会调用 is_windows_drive_absolute_path 来确认盘符路径是不是完整绝对路径。
调用图:调用 1 个内部函数(is_windows_drive_absolute_path);被 1 处调用(normalize_path_for_platform);外部调用 1 个(format!)。
is_windows_drive_absolute_path173–179 ↗
fn is_windows_drive_absolute_path(path: &str) -> bool
作用:判断一个字符串是不是 Windows 的盘符绝对路径,比如“C:\foo”或“D:/foo”。
数据流:输入字符串 → 看前三个字节是否符合“字母 + 冒号 + 斜杠”的模式 → 输出 true 或 false。
调用关系:normalize_windows_device_path 用它避免误删不该删的 Windows 特殊前缀。它是一个很小但重要的安全检查。
调用图:被 1 处调用(normalize_windows_device_path);外部调用 1 个(matches!)。
canonicalize_preserving_symlinks189–197 ↗
fn canonicalize_preserving_symlinks(path: &Path) -> std::io::Result<PathBuf>
作用:尽量真实化路径,但如果真实化会把用户写的符号链接路径改成别的路径,就尽量保留用户看到的逻辑路径。它适合“不一定要求文件存在”的场景。
数据流:输入一个 Path → 先转成逻辑上的绝对路径 → 判断是否应该保留符号链接写法 → 再尝试系统 canonicalize;成功且需要保留时返回逻辑路径,成功且不需保留时返回真实路径,失败时也返回逻辑路径。
调用关系:相关测试会验证它保留符号链接、处理缺失子路径、避免 Windows verbatim 前缀。它内部依赖 AbsolutePathBuf::from_absolute_path 和 should_preserve_logical_path。
调用图:调用 2 个内部函数(from_absolute_path, should_preserve_logical_path);被 3 处调用(canonicalize_preserving_symlinks_avoids_verbatim_prefixes, canonicalize_preserving_symlinks_keeps_logical_missing_child_under_symlink, canonicalize_preserving_symlinks_keeps_logical_symlink_path);外部调用 1 个(canonicalize)。
canonicalize_existing_preserving_symlinks204–212 ↗
fn canonicalize_existing_preserving_symlinks(path: &Path) -> std::io::Result<PathBuf>
作用:和 canonicalize_preserving_symlinks 类似,也尽量保留符号链接写法;但它要求路径必须真实存在,不存在就报错。
数据流:输入一个 Path → 转成逻辑绝对路径 → 调用系统 canonicalize,失败就把错误传出去 → 如果发现应保留逻辑路径且真实路径不同,就输出逻辑路径,否则输出真实路径。
调用关系:测试会用它确认缺失路径会失败、符号链接路径会被保留。它适合启动时提前拒绝无效工作目录这类严格场景。
调用图:调用 2 个内部函数(from_absolute_path, should_preserve_logical_path);被 2 处调用(canonicalize_existing_preserving_symlinks_errors_for_missing_path, canonicalize_existing_preserving_symlinks_keeps_logical_symlink_path);外部调用 1 个(canonicalize)。
should_preserve_logical_path214–221 ↗
fn should_preserve_logical_path(logical: &Path) -> bool
作用:判断一个逻辑路径里是否包含“值得保留”的符号链接。这里的符号链接可以理解成文件系统里的快捷方式。
数据流:输入一个逻辑路径 → 从它自己一路检查到祖先目录 → 对每一级读取符号链接元数据;如果发现不是顶层别名、而是更深层的符号链接,就返回 true,否则 false。
调用关系:canonicalize_preserving_symlinks 和 canonicalize_existing_preserving_symlinks 都靠它决定是否保留用户写的路径。它把“什么时候该保留”的判断集中在一处。
调用图:被 2 处调用(canonicalize_existing_preserving_symlinks, canonicalize_preserving_symlinks);外部调用 1 个(ancestors)。
AbsolutePathBuf::as_ref224–226 ↗
fn as_ref(&self) -> &Path
作用:让 AbsolutePathBuf 可以自动当作 Path 引用使用。这样调用很多标准库函数时不用手动取内部路径。
数据流:输入当前 AbsolutePathBuf → 返回内部 Path 的只读引用 → 不复制也不修改任何数据。
调用关系:这是 Rust 的 AsRef<Path> 转换接口实现。很多泛型函数看到 AbsolutePathBuf 时会通过它拿到普通 Path。
AbsolutePathBuf::deref232–234 ↗
fn deref(&self) -> &Self::Target
作用:让 AbsolutePathBuf 像 Path 一样被直接使用。它让代码写起来更自然,比如可以调用 Path 上的方法。
数据流:输入当前 AbsolutePathBuf → 返回内部 Path 的只读引用 → 不改变路径。
调用关系:这是 Rust 的 Deref(自动解引用)实现。它不是业务流程的一步,而是让这个类型更容易接入现有路径 API。
PathBuf::from238–240 ↗
fn from(path: AbsolutePathBuf) -> Self
作用:定义如何把 AbsolutePathBuf 转成普通 PathBuf。调用方需要标准 PathBuf 时可以直接转换。
数据流:输入 AbsolutePathBuf → 调用 into_path_buf 取出内部路径 → 输出 PathBuf。
调用关系:这是 From<AbsolutePathBuf> for PathBuf 的实现。它把转换工作交给 AbsolutePathBuf::into_path_buf,避免重复写取出逻辑。
调用图:调用 1 个内部函数(into_path_buf)。
test_support::test_path_buf252–265 ↗
test_support::Path::abs275–278 ↗
fn abs(&self) -> AbsolutePathBuf
作用:测试里给 Path 增加一个快捷方法,把已经是绝对路径的 Path 变成 AbsolutePathBuf。写测试时更省事。
数据流:输入一个 Path 引用 → 调用 from_absolute_path_checked 严格检查并包装 → 成功输出 AbsolutePathBuf;失败时测试直接 panic,因为测试假设路径应当合法。
调用关系:这是测试扩展 trait 的实现。它把实际检查交给生产代码的 from_absolute_path_checked,保证测试辅助和真实规则一致。
调用图:调用 1 个内部函数(from_absolute_path_checked)。
test_support::PathBuf::abs288–290 ↗
fn abs(&self) -> AbsolutePathBuf
作用:测试里给 PathBuf 增加一个快捷方法,把路径缓冲区转成 AbsolutePathBuf。它只是让测试代码更顺手。
数据流:输入一个 PathBuf 引用 → 先转成 Path 引用 → 调用 Path 的 abs 方法 → 输出 AbsolutePathBuf。
调用关系:这是 PathBufExt 的实现,依赖 test_support::Path::abs 做真正转换。它服务测试代码里的路径构造。
AbsolutePathBuf::try_from321–323 ↗
fn try_from(value: String) -> Result<Self, Self::Error>
作用:提供“尝试转换”为 AbsolutePathBuf 的标准入口。字符串、Path、PathBuf 等外部值可以通过这个接口进入安全路径类型。
数据流:输入某种路径形式 → 调用 AbsolutePathBuf::from_absolute_path 做展开、整理和绝对化 → 输出 AbsolutePathBuf,或者输出 I/O 错误。
调用关系:很多序列化、插件接口和命令参数解析会通过 TryFrom 使用它。它是 Rust 标准转换习惯和本文件路径规则之间的适配层。
调用图:被 245 处调用(marketplace_remove_response_serializes_nullable_installed_root, marketplace_upgrade_response_serializes_camel_case_fields, plugin_install_params_serialization_omits_force_remote_sync, plugin_interface_serializes_local_paths_and_remote_urls_separately, plugin_read_params_serialization_uses_install_source_fields, plugin_share_params_and_response_serialization_use_camel_case_fields, plugin_source_serializes_local_git_and_remote_variants, absolute_path_arg, read, read_includes_origins_and_layers (+15 more));外部调用 1 个(from_absolute_path)。
AbsolutePathBufGuard::new337–342 ↗
fn new(base_path: &Path) -> Self
作用:在当前线程临时设置一个“相对路径从哪里开始算”的基准目录。读取配置里的 AbsolutePathBuf 时,没有它就不知道相对路径该接到哪里。
数据流:输入一个基准 Path → 把它复制进线程本地存储,也就是只属于当前线程的小格子 → 输出一个 guard 对象;只要这个对象还活着,反序列化就能看到这个基准目录。
调用关系:配置校验、配置片段解析、项目信任上下文等流程会在反序列化前创建它。之后 AbsolutePathBuf::deserialize 会读取这个基准。
调用图:被 23 处调用(validate_fragment_strictly, deserialize_filesystem_deny_read_glob_requirements, first_layer_config_error_for_entries, validate_config_toml_strictly, validate_managed_config_toml_strictly_if_requested, project_trust_context, resolve_relative_paths_in_config_toml, validate_cli_overrides_strictly, validate_config_toml_strictly, load_skill_metadata (+13 more))。
AbsolutePathBufGuard::drop346–350 ↗
fn drop(&mut self)
作用:在 guard 生命周期结束时清掉临时基准目录,避免影响后面的反序列化。就像用完临时钥匙后立刻还回去。
数据流:输入即将销毁的 guard → 访问当前线程的小格子 → 把里面的基准路径设回 None → 没有返回值,但改变了线程本地状态。
调用关系:这是 Rust 的 Drop 自动清理函数。调用方通常不手动调用它;当 AbsolutePathBufGuard 离开作用域时,系统自动执行。
AbsolutePathBuf::deserialize354–368 ↗
fn deserialize(deserializer: D) -> Result<Self, D::Error>
作用:把配置或 JSON 里的路径文字读成 AbsolutePathBuf。它保证读出来后不是随便的字符串,而是已经按规则变成绝对路径。
数据流:输入反序列化器 → 先读出 PathBuf → 查看当前线程是否有 AbsolutePathBufGuard 设置的基准目录;有就按基准解析,没有但路径本身是绝对路径就直接转换,没有且路径相对就返回错误 → 输出 AbsolutePathBuf 或反序列化错误。
调用关系:deserialize_absolute_path、default_provider_auth_cwd 等测试或配置读取会触发它。它和 AbsolutePathBufGuard 配合,解决配置文件相对路径的上下文问题。
调用图:被 2 处调用(deserialize_absolute_path, default_provider_auth_cwd);外部调用 1 个(deserialize)。
tests::create_with_absolute_path_ignores_base_path382–390 ↗
fn create_with_absolute_path_ignores_base_path()
作用:验证如果输入本来就是绝对路径,基准目录不会把它改掉。这样用户写绝对路径时不会被错误地接到别的目录下面。
数据流:测试创建两个临时目录 → 用一个绝对路径和另一个基准目录调用 resolve_path_against_base → 检查结果仍是原来的绝对路径。
调用关系:这是 resolve_path_against_base 的行为测试。测试框架运行它,确认绝对路径优先于基准目录。
调用图:调用 1 个内部函数(resolve_path_against_base);外部调用 2 个(assert_eq!, tempdir)。
tests::from_absolute_path_does_not_read_current_dir_when_path_is_absolute394–403 ↗
fn from_absolute_path_does_not_read_current_dir_when_path_is_absolute()
作用:验证处理绝对路径时不依赖当前工作目录。即使当前目录坏了,绝对路径也应该还能正常整理。
数据流:测试启动当前测试程序的子进程,并设置一个环境变量 → 子进程运行专门的被忽略测试 → 父进程检查子进程成功退出。
调用关系:它配合 tests::from_absolute_path_with_removed_current_dir_child 使用。父测试负责制造隔离进程,子测试负责真正破坏当前目录并验证行为。
调用图:外部调用 3 个(assert!, new, current_exe)。
tests::from_absolute_path_with_removed_current_dir_child408–430 ↗
fn from_absolute_path_with_removed_current_dir_child()
作用:在子进程里模拟“当前目录已被删除”的极端情况,确认 from_absolute_path 处理绝对路径时不会因此失败。
数据流:测试先确认环境变量存在 → 记住原当前目录,进入临时目录后删除它 → 确认 current_dir 已经不可用 → 调用 from_absolute_path 处理一个绝对路径 → 恢复原目录并断言路径整理结果正确。
调用关系:它被上一个父测试通过子进程方式触发。这样破坏当前目录不会污染整个测试进程。
调用图:调用 1 个内部函数(from_absolute_path);外部调用 7 个(assert_eq!, current_dir, set_current_dir, var_os, remove_dir, tempdir, test_path_buf)。
tests::from_absolute_path_checked_rejects_relative_path433–438 ↗
fn from_absolute_path_checked_rejects_relative_path()
作用:验证严格创建函数会拒绝相对路径。这样调用方不能不小心把“relative/path”当成已经安全的绝对路径。
数据流:输入一个明显的相对路径 → 调用 from_absolute_path_checked → 期待得到错误,并检查错误类型是 InvalidInput。
调用关系:这是 AbsolutePathBuf::from_absolute_path_checked 的边界测试。它保证严格入口不会悄悄放宽规则。
调用图:调用 1 个内部函数(from_absolute_path_checked);外部调用 1 个(assert_eq!)。
tests::normalize_windows_device_path_strips_supported_verbatim_prefixes441–462 ↗
fn normalize_windows_device_path_strips_supported_verbatim_prefixes()
作用:验证 Windows 特殊前缀能被正确去掉,而不支持的特殊路径不会乱处理。
数据流:测试传入多种 Windows 字符串 → 调用 normalize_windows_device_path → 对每个结果和预期普通路径或 None 做比较。
调用关系:这是 normalize_windows_device_path 的单元测试。它覆盖盘符路径、UNC 网络路径和不支持的 GLOBALROOT 路径。
调用图:外部调用 1 个(assert_eq!)。
tests::from_absolute_path_strips_windows_verbatim_prefix466–475 ↗
fn from_absolute_path_strips_windows_verbatim_prefix()
作用:在 Windows 上验证严格路径创建会去掉“\\?\”这类 verbatim 前缀。这样项目内部保存的是普通路径形式。
数据流:输入带特殊前缀的 Windows 盘符路径 → 调用 from_absolute_path_checked → 检查结果等于没有特殊前缀的普通 Path。
调用关系:这是 Windows 专用测试。它从更高层验证 normalize_path_for_platform 和 from_absolute_path_checked 的配合。
调用图:调用 1 个内部函数(from_absolute_path_checked);外部调用 1 个(assert_eq!)。
tests::relative_path_is_resolved_against_base_path478–483 ↗
fn relative_path_is_resolved_against_base_path()
作用:验证相对路径会接到基准目录下面。比如“file.txt”应该变成“基准目录/file.txt”。
数据流:测试创建临时基准目录 → 调用 resolve_path_against_base 处理相对文件名 → 检查输出路径等于基准目录拼上文件名。
调用关系:这是 resolve_path_against_base 的核心行为测试。配置相对路径能工作,靠的就是这个规则。
调用图:调用 1 个内部函数(resolve_path_against_base);外部调用 2 个(assert_eq!, tempdir)。
tests::relative_path_dots_are_normalized_against_base_path486–492 ↗
fn relative_path_dots_are_normalized_against_base_path()
作用:验证相对路径里的“.”和“..”会被整理掉。这样“./nested/../file.txt”不会在内部保留绕路写法。
数据流:测试创建临时基准目录 → 用带点号片段的相对路径调用 resolve_path_against_base → 检查结果是基准目录下的 file.txt。
调用关系:这是路径规范化测试。它确保 resolve_path_against_base 不只是拼字符串,还会整理路径结构。
调用图:调用 1 个内部函数(resolve_path_against_base);外部调用 2 个(assert_eq!, tempdir)。
tests::canonicalize_returns_absolute_path_buf495–512 ↗
fn canonicalize_returns_absolute_path_buf()
作用:验证 AbsolutePathBuf::canonicalize 能返回真实存在文件的规范绝对路径。
数据流:测试创建目录和文件 → 用包含“one/../two/./file.txt”的路径创建 AbsolutePathBuf → 调用 canonicalize → 和系统 canonicalize 的结果比较。
调用关系:这是 AbsolutePathBuf::canonicalize 的成功路径测试。它确认包装类型不会妨碍标准文件系统真实化。
调用图:调用 1 个内部函数(from_absolute_path);外部调用 4 个(assert_eq!, create_dir, write, tempdir)。
tests::canonicalize_returns_error_for_missing_path515–521 ↗
fn canonicalize_returns_error_for_missing_path()
作用:验证 canonicalize 遇到不存在的路径会报错。这样调用方可以用它判断文件是否真的存在。
数据流:测试创建临时目录但不创建目标文件 → 用缺失文件路径创建 AbsolutePathBuf → 调用 canonicalize → 断言结果是错误。
调用关系:这是 AbsolutePathBuf::canonicalize 的失败路径测试。它和 preserving_symlinks 的“失败时可返回逻辑路径”形成对比。
调用图:调用 1 个内部函数(from_absolute_path);外部调用 2 个(assert!, tempdir)。
tests::ancestors_returns_absolute_path_bufs524–542 ↗
fn ancestors_returns_absolute_path_bufs()
作用:验证 ancestors 会从完整路径一路列到根目录,而且每一项都是预期的绝对路径。
数据流:测试构造“/tmp/one/two”这样的绝对路径 → 调用 ancestors 并收集成列表 → 和预期的路径列表比较。
调用关系:这是 AbsolutePathBuf::ancestors 的行为测试。项目根查找等功能依赖它能按顺序向上枚举目录。
调用图:调用 1 个内部函数(from_absolute_path_checked);外部调用 3 个(assert_eq!, test_path_buf, vec!)。
tests::relative_to_current_dir_resolves_relative_path545–553 ↗
fn relative_to_current_dir_resolves_relative_path() -> std::io::Result<()>
作用:验证 relative_to_current_dir 会用当前工作目录解析相对路径。
数据流:测试读取当前目录 → 调用 relative_to_current_dir("file.txt") → 检查结果等于当前目录拼上 file.txt。
调用关系:这是 AbsolutePathBuf::relative_to_current_dir 的直接测试。它确认该函数和系统 current_dir 的配合正确。
调用图:调用 1 个内部函数(relative_to_current_dir);外部调用 2 个(assert_eq!, current_dir)。
tests::guard_used_in_deserialization556–569 ↗
fn guard_used_in_deserialization()
作用:验证反序列化相对路径时,AbsolutePathBufGuard 设置的基准目录会生效。
数据流:测试创建临时基准目录 → 在 guard 存活期间从 JSON 字符串读入相对路径 → 检查结果等于基准目录加相对路径。
调用关系:这是 AbsolutePathBufGuard::new 和 AbsolutePathBuf::deserialize 的配合测试。它证明配置里的相对路径不会无上下文地解析。
调用图:调用 1 个内部函数(new);外部调用 3 个(assert_eq!, format!, tempdir)。
tests::home_directory_root_is_expanded_in_deserialization572–582 ↗
fn home_directory_root_is_expanded_in_deserialization()
作用:验证配置里单独写“~”时,会被解析成用户家目录。
数据流:测试先获取系统家目录;如果没有家目录就跳过 → 在 guard 环境下反序列化 JSON 字符串“~” → 检查结果等于家目录。
调用关系:这是反序列化时家目录展开的测试。它间接覆盖 maybe_expand_home_directory。
调用图:调用 1 个内部函数(new);外部调用 3 个(assert_eq!, home_dir, tempdir)。
tests::home_directory_subpath_is_expanded_in_deserialization585–595 ↗
fn home_directory_subpath_is_expanded_in_deserialization()
作用:验证配置里写“~/code”时,会被解析成家目录下面的 code。
数据流:测试获取家目录并创建临时基准 → 在 guard 环境下反序列化“~/code” → 检查结果等于 home/code。
调用关系:这是家目录子路径展开测试。它确认“~”展开优先于相对路径基准解析。
调用图:调用 1 个内部函数(new);外部调用 3 个(assert_eq!, home_dir, tempdir)。
tests::home_directory_double_slash_is_expanded_in_deserialization598–608 ↗
fn home_directory_double_slash_is_expanded_in_deserialization()
作用:验证“~//code”这种多一个斜杠的写法也能整理成家目录下的 code。
数据流:测试获取家目录 → 在 guard 环境下反序列化“~//code” → 检查输出是 home/code,而不是带双斜杠的奇怪路径。
调用关系:这是 maybe_expand_home_directory 的容错测试,通过反序列化路径触发。
调用图:调用 1 个内部函数(new);外部调用 3 个(assert_eq!, home_dir, tempdir)。
tests::canonicalize_preserving_symlinks_keeps_logical_symlink_path612–623 ↗
fn canonicalize_preserving_symlinks_keeps_logical_symlink_path()
作用:验证保留符号链接版本的 canonicalize 不会把链接路径改成真实目录路径。
数据流:测试创建真实目录和指向它的符号链接 → 调用 canonicalize_preserving_symlinks 处理链接 → 检查结果仍然是链接路径。
调用关系:这是 canonicalize_preserving_symlinks 的 Unix 测试。它确认 should_preserve_logical_path 的判断会影响最终结果。
调用图:调用 1 个内部函数(canonicalize_preserving_symlinks);外部调用 4 个(assert_eq!, create_dir_all, symlink, tempdir)。
tests::canonicalize_preserving_symlinks_keeps_logical_missing_child_under_symlink627–639 ↗
fn canonicalize_preserving_symlinks_keeps_logical_missing_child_under_symlink()
作用:验证符号链接下面的缺失子路径也能保留逻辑路径,而不是因为文件不存在就失败。
数据流:测试创建真实目录和符号链接,再构造链接下面不存在的文件路径 → 调用 canonicalize_preserving_symlinks → 检查返回的就是这个逻辑缺失路径。
调用关系:这是 canonicalize_preserving_symlinks 的容错测试。它覆盖该函数在系统 canonicalize 失败时返回逻辑绝对路径的行为。
调用图:调用 1 个内部函数(canonicalize_preserving_symlinks);外部调用 4 个(assert_eq!, create_dir_all, symlink, tempdir)。
tests::canonicalize_existing_preserving_symlinks_errors_for_missing_path642–650 ↗
fn canonicalize_existing_preserving_symlinks_errors_for_missing_path()
作用:验证要求路径存在的版本遇到缺失路径会报 NotFound 错误。
数据流:测试创建临时目录但不创建目标路径 → 调用 canonicalize_existing_preserving_symlinks → 期待错误,并检查错误类型是 NotFound。
调用关系:这是 canonicalize_existing_preserving_symlinks 和宽松版本的关键区别测试。它保证严格场景不会吞掉缺失路径错误。
调用图:调用 1 个内部函数(canonicalize_existing_preserving_symlinks);外部调用 2 个(assert_eq!, tempdir)。
tests::canonicalize_existing_preserving_symlinks_keeps_logical_symlink_path654–665 ↗
fn canonicalize_existing_preserving_symlinks_keeps_logical_symlink_path()
作用:验证严格版本在路径存在时,也会保留符号链接的逻辑路径。
数据流:测试创建真实目录和符号链接 → 调用 canonicalize_existing_preserving_symlinks 处理链接 → 检查结果仍是链接路径。
调用关系:这是 canonicalize_existing_preserving_symlinks 的 Unix 成功路径测试。它同时验证“必须存在”和“保留逻辑路径”两个要求可以共存。
调用图:调用 1 个内部函数(canonicalize_existing_preserving_symlinks);外部调用 4 个(assert_eq!, create_dir_all, symlink, tempdir)。
tests::home_directory_backslash_subpath_is_expanded_in_deserialization669–681 ↗
fn home_directory_backslash_subpath_is_expanded_in_deserialization()
作用:在 Windows 上验证“~\code”这种反斜杠写法也会展开到家目录下的 code。
数据流:测试获取家目录和临时基准目录 → 把“~\code”序列化成 JSON 字符串再反序列化为 AbsolutePathBuf → 检查结果等于 home/code。
调用关系:这是 Windows 专用的家目录展开测试。它补充验证 maybe_expand_home_directory 对 Windows 分隔符的支持。
调用图:调用 1 个内部函数(new);外部调用 4 个(assert_eq!, home_dir, to_string, tempdir)。
tests::canonicalize_preserving_symlinks_avoids_verbatim_prefixes685–699 ↗
fn canonicalize_preserving_symlinks_avoids_verbatim_prefixes()
作用:在 Windows 上验证 canonicalize_preserving_symlinks 返回的路径不会带“\\?\”特殊前缀。
数据流:测试创建临时目录 → 调用 canonicalize_preserving_symlinks → 和 dunce::canonicalize 的结果比较,并检查字符串不是以特殊前缀开头。
调用关系:这是 Windows 专用测试。它确认该文件选择 dunce::canonicalize 和平台整理规则后,输出更适合项目内部使用。
调用图:调用 1 个内部函数(canonicalize_preserving_symlinks);外部调用 3 个(assert!, assert_eq!, tempdir)。
utils/absolute-path/src/absolutize.rs源码 ↗
文件路径看起来简单,其实很容易出错。比如 ./a/../b、../x、Windows 里的 D:file,人能看懂,程序如果不先统一口径,就可能找错文件。这个文件做的事就像给地址做“标准化”:先看路径是不是已经从根开始;如果不是,就把它接到一个基准目录后面;然后把 .(当前目录)删掉,把 ..(上一级目录)按规则往回退。这里特意把“给定基准目录时的整理”做成不会失败,因为它只是在内存里拼路径;只有需要读取当前工作目录时,才可能因为系统原因失败。它还区分 Unix 和 Windows,因为 Windows 的盘符、根路径规则更复杂。后面的测试用一组常见路径例子,保证这些边角情况不会被改坏。
absolutize14–20 ↗
fn absolutize(path: &Path) -> std::io::Result<PathBuf>
作用:把一个路径变成绝对且整理过的路径。如果传进来的路径本来就是绝对路径,它只清理里面的 . 和 ..;如果是相对路径,它会先拿当前工作目录当起点。
数据流:输入是一个路径。它先检查这个路径是不是绝对路径;是的话,直接交给清理步骤得到新路径;不是的话,先向操作系统询问当前工作目录,再把原路径接到这个目录后面并清理。输出是整理后的路径;如果读取当前工作目录失败,就返回错误。
调用关系:这是需要“按当前所在目录理解路径”时的入口。外层的 from_absolute_path 会调用它;它自己根据情况把活儿交给 normalize_path 或 absolutize_from。
调用图:调用 2 个内部函数(absolutize_from, normalize_path);被 1 处调用(from_absolute_path);外部调用 2 个(is_absolute, current_dir)。
absolutize_from22–24 ↗
fn absolutize_from(path: &Path, base_path: &Path) -> PathBuf
作用:把一个路径按指定的基准目录来解释,并整理成规范路径。它适合在代码已经知道“相对路径应该从哪里开始算”时使用。
数据流:输入是待处理路径和一个基准路径。它先用 path_with_base 决定两者该怎么拼在一起,再用 normalize_path 去掉多余的当前目录和上级目录标记。输出是新的 PathBuf 路径;它不需要访问操作系统,所以不会因为当前目录读取失败而出错。
调用关系:这是本文件里最稳定的核心接口之一。from_absolute_path_checked、resolve_path_against_base 和 absolutize 都会用它;它把“拼接规则”交给 path_with_base,把“清理规则”交给 normalize_path。
调用图:调用 2 个内部函数(normalize_path, path_with_base);被 3 处调用(from_absolute_path_checked, resolve_path_against_base, absolutize)。
normalize_path26–45 ↗
fn normalize_path(path: &Path) -> PathBuf
作用:清理路径里的特殊片段,让路径更像最终地址。它会忽略 .,遇到 .. 就往上退一级,普通目录名照常保留。
数据流:输入是一个路径。它把路径拆成一个个组成部分,从左到右检查:当前目录标记被丢掉,上级目录标记会让已经收集的路径退回一层,根目录、盘符和普通名字会被加入结果。输出是清理后的路径;如果最后什么都不剩,就返回 . 表示当前目录。
调用关系:这是最终“扫地整理”的步骤。absolutize 在处理已有绝对路径时直接用它,absolutize_from 在拼好基准路径后也会用它。
调用图:被 2 处调用(absolutize, absolutize_from);外部调用 3 个(components, from, new)。
path_with_base57–84 ↗
fn path_with_base(path: &Path, base_path: &Path) -> PathBuf
作用:决定一个路径应该怎样和基准目录合在一起。它专门处理 Unix 和 Windows 的差异,尤其是 Windows 盘符和从根开始但不带盘符的路径。
数据流:输入是待处理路径和基准路径。它先判断待处理路径是否已经足够独立:在 Unix 上,绝对路径直接原样使用,相对路径接到基准后面;在 Windows 上,还要额外考虑根路径、盘符相对路径等情况。输出是拼好的路径,后面还需要再清理。
调用关系:它只被 absolutize_from 调用,是“先怎么拼”的规则层。拼好之后,absolutize_from 会继续交给 normalize_path 做最终整理。
调用图:被 1 处调用(absolutize_from);外部调用 9 个(components, has_root, is_absolute, join, push, to_path_buf, new, matches!, from)。
tests::absolute_path_without_dots_is_unchanged93–98 ↗
fn absolute_path_without_dots_is_unchanged()
作用:验证 Unix 上一个已经干净的绝对路径不会被乱改。这个测试防止标准化过程把本来正确的路径处理坏。
数据流:输入是 /path/to/123/456 和一个不会实际用到的基准路径 /base。测试调用路径整理函数,然后比较结果是否仍然是原来的绝对路径。输出不是业务数据,而是测试通过或失败。
调用关系:这是测试模块里的一个安全网。它直接检查 absolutize_from 的行为,确认绝对路径优先于基准目录。
调用图:外部调用 1 个(assert_eq!)。
tests::absolute_path_dots_are_removed102–107 ↗
fn absolute_path_dots_are_removed()
作用:验证 Unix 上绝对路径里的 . 和 .. 会被正确清理。它保证看起来绕路的路径会变成直接路径。
数据流:输入是 /path/to/./123/../456 和基准路径 /base。整理后,. 被忽略,123/.. 抵消,结果应为 /path/to/456。测试用断言确认这一点。
调用关系:它测试的是 absolutize_from 背后的清理能力,也就是 normalize_path 的关键效果。
调用图:外部调用 1 个(assert_eq!)。
tests::relative_path_without_dot_uses_base111–116 ↗
fn relative_path_without_dot_uses_base()
作用:验证 Unix 上普通相对路径会接到基准目录后面。这样调用者可以放心:path/to/file 会按指定起点来理解。
数据流:输入是相对路径 path/to/123/456 和基准 /base。函数把它们拼成 /base/path/to/123/456,测试检查结果是否一致。
调用关系:它覆盖 absolutize_from 中“相对路径加基准目录”的主流程,间接确认 path_with_base 的 Unix 规则。
调用图:外部调用 1 个(assert_eq!)。
tests::relative_path_with_current_dir_uses_base120–125 ↗
fn relative_path_with_current_dir_uses_base()
作用:验证以 ./ 开头的相对路径也会从基准目录开始算,并且 ./ 会被去掉。./ 只是“当前这里”的意思,不应该留在最终结果里。
数据流:输入是 ./path/to/123/456 和 /base。函数先按基准拼接,再清理当前目录标记,最后得到 /base/path/to/123/456。测试用断言确认结果。
调用关系:它把 path_with_base 的拼接和 normalize_path 的清理放在一起验证,确保两步配合正确。
调用图:外部调用 1 个(assert_eq!)。
tests::relative_path_with_parent_dir_uses_base_parent129–134 ↗
fn relative_path_with_parent_dir_uses_base_parent()
作用:验证相对路径开头的 .. 会让结果从基准目录往上退一级。比如从 /base/cwd 出发,../x 应该落到 /base/x。
数据流:输入是 ../path/to/123/456 和基准 /base/cwd。函数先拼成类似 /base/cwd/../path...,再把 cwd/.. 抵消,输出 /base/path/to/123/456。测试检查这个变化。
调用关系:它主要检验 normalize_path 对上级目录标记的处理,也确认 absolutize_from 会先以基准目录为出发点。
调用图:外部调用 1 个(assert_eq!)。
tests::parent_dir_above_root_stays_at_root138–143 ↗
fn parent_dir_above_root_stays_at_root()
作用:验证 Unix 上路径想退到根目录以上时,不会产生奇怪的地址。根目录已经是最顶层,再往上退也只能停在根附近。
数据流:输入是 ../../path/to/123/456 和基准 /。整理时,多余的向上退级不会把路径变成无效结构,最后得到 /path/to/123/456。测试确认这个结果。
调用关系:它覆盖一个容易出错的边界情况,保证 normalize_path 在根目录附近处理 .. 时不会乱掉。
调用图:外部调用 1 个(assert_eq!)。
tests::empty_path_uses_base147–152 ↗
fn empty_path_uses_base()
作用:验证空路径会被理解成基准目录本身。也就是说,如果用户没写额外路径,就默认指向当前给定的位置。
数据流:输入是空路径和基准 /base/cwd。函数把空路径接到基准上,清理后仍然是 /base/cwd。测试检查输出是否等于基准路径。
调用关系:它检查 absolutize_from 对空输入的实用行为,防止空字符串被错误变成别的目录。
调用图:外部调用 1 个(assert_eq!)。
tests::windows_root_relative_path_uses_base_prefix156–161 ↗
fn windows_root_relative_path_uses_base_prefix()
作用:验证 Windows 上以反斜杠开头、但没有盘符的路径,会沿用基准路径的盘符。比如 \path 配上 C:\base,应该落在 C 盘根下。
数据流:输入是 \path\to\file 和基准 C:\base\cwd。函数把基准里的 C: 盘符补上,再得到 C:\path\to\file。测试用断言确认这个 Windows 特有规则。
调用关系:它只在 Windows 测试中运行,主要验证 path_with_base 的 Windows 根路径处理。
调用图:外部调用 1 个(assert_eq!)。
tests::windows_drive_relative_path_uses_path_prefix_and_base_tail165–170 ↗
fn windows_drive_relative_path_uses_path_prefix_and_base_tail()
作用:验证 Windows 上带盘符但不带根的路径,比如 D:path,会使用自己的盘符,同时借用基准路径的目录尾巴。这个规则很少见,但 Windows 路径语义确实如此复杂。
数据流:输入是 D:path\to\file 和基准 C:\base\cwd。函数保留输入路径里的 D:,再接上基准的 \base\cwd 和后续相对部分,最后得到 D:\base\cwd\path\to\file。测试检查结果是否符合预期。
调用关系:它也是 Windows 专用测试,专门盯住 path_with_base 中最容易被误解的盘符相对路径规则。
调用图:外部调用 1 个(assert_eq!)。
app-server-client/src/path.rs源码 ↗
这份文件解决的是“路径到底该怎么拼、怎么判断”的问题。应用服务器可能跑在 Linux、macOS,也可能跑在 Windows;这些系统的路径写法不一样,比如 Unix 常用“/home/a”,Windows 可能是“C:\a”或网络路径“\\server”。AppServerPath 就像给路径套了一个标签:这是服务器那边的路径,不是本机随便解释的普通字符串。它可以从已有字符串创建路径,也可以检查一个字符串是不是绝对路径,还能把路径拆成一段一段,或者在后面接上新的文件夹/文件名。内部最关键的小工具是 is_windows_absolute_path,用来认出 Windows 风格的绝对路径。这样其他代码在请求文件、拼接目录时,不用到处重复写这些容易漏掉的判断。
AppServerPath::from_app_server9–11 ↗
fn from_app_server(path: impl Into<String>) -> Self
作用:把应用服务器传来的路径字符串包成 AppServerPath。别人会用它来明确表示:这不是普通文字,而是一条服务器端路径。
数据流:进去的是一个可以变成字符串的路径值 → 函数把它转换成 String,并直接放进 AppServerPath 里 → 出来的是一个新的 AppServerPath;原输入不再需要被调用者手工区分。
调用关系:它常在已经确信路径来自服务器时使用,比如 codex_home 会用它包装服务器上的主目录路径;测试里也会用它构造路径样例。它不再继续调用本文件的路径判断逻辑,只做最简单的包装。
调用图:被 2 处调用(codex_home, set_thread_goal_draft_materializes_long_objective_and_confirms_before_paste);外部调用 1 个(into)。
AppServerPath::from_absolute_str13–15 ↗
fn from_absolute_str(raw: &str) -> Option<Self>
作用:只在输入看起来是“绝对路径”时才创建 AppServerPath。绝对路径就是从根位置开始写的完整路径,不依赖当前目录。
数据流:进去的是一段原始路径文字 → 它先看是否以“/”开头,或者是否符合 Windows 绝对路径写法 → 如果是,就返回 Some(AppServerPath);如果不是,就返回 None,表示这条路径不够完整、不该被当作服务器绝对路径使用。
调用关系:objective_file_path 会用它来把用户或系统给出的目标文件路径变成安全的服务器路径。它把 Windows 绝对路径识别这件事交给 is_windows_absolute_path,避免自己重复判断。
调用图:调用 1 个内部函数(is_windows_absolute_path);被 1 处调用(objective_file_path)。
AppServerPath::as_str17–19 ↗
fn as_str(&self) -> &str
作用:取出里面保存的原始路径文字。需要把路径交给别的接口或请求时,会用它拿到字符串形式。
数据流:进去的是一个 AppServerPath 本身 → 函数不复制、不修改,只借出里面那段字符串 → 出来的是一个字符串引用,调用者可以读取但不能通过它改路径。
调用关系:request_fs_path 会用它把封装好的服务器路径拿出来,放进文件系统请求里。它处在“内部安全表示”到“外部请求参数”的转换位置。
调用图:被 1 处调用(request_fs_path)。
AppServerPath::components21–31 ↗
fn components(&self) -> Vec<&str>
作用:把一条路径拆成一段一段的名字,比如目录名和文件名。这样其他代码可以逐级查看路径内容,而不用自己处理斜杠。
数据流:进去的是 AppServerPath 里保存的路径字符串 → 它先判断是不是 Windows 绝对路径;如果是,就同时把“/”和“\”都当分隔符,否则只把“/”当分隔符 → 它丢掉空白段,返回一个字符串片段列表。
调用关系:它依赖 is_windows_absolute_path 来决定按哪种系统规则拆路径。它适合被需要检查路径层级、展示路径结构或逐段处理路径的代码调用。
调用图:调用 1 个内部函数(is_windows_absolute_path)。
AppServerPath::join33–41 ↗
fn join(&self, segment: impl AsRef<str>) -> Self
作用:在现有服务器路径后面接上一段新名字,得到一条更长的路径。它会根据原路径风格选择正确的分隔符。
数据流:进去的是一个已有 AppServerPath 和一个要追加的片段 → 它判断原路径是不是 Windows 风格;如果是,就去掉末尾多余的“/”或“\”,再用“\”连接;否则去掉末尾多余的“/”,再用“/”连接 → 出来的是新的 AppServerPath,原路径不被修改。
调用关系:它把“该用哪种斜杠”的判断交给 is_windows_absolute_path,然后用格式化字符串拼出新路径。它通常会在代码需要在服务器目录下定位某个子文件或子目录时使用。
调用图:调用 1 个内部函数(is_windows_absolute_path);外部调用 1 个(format!)。
AppServerPath::fmt45–47 ↗
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
作用:让 AppServerPath 可以像普通文字一样被打印或放进格式化字符串里。这样日志、错误信息、调试输出都能直接显示路径。
数据流:进去的是 AppServerPath 和一个格式化输出器 → 它把内部保存的字符串交给输出器写出去 → 出来的是格式化是否成功的结果;路径本身不会改变。
调用关系:这是 Rust 的 Display 显示接口实现,会在代码写类似打印、日志、format 字符串时自动被用到。它不参与路径判断,只负责把路径展示出来。
is_windows_absolute_path50–58 ↗
fn is_windows_absolute_path(path: &str) -> bool
作用:判断一段路径文字是不是 Windows 风格的绝对路径。它是本文件里识别 Windows 路径规则的共同小助手。
数据流:进去的是一段路径字符串 → 它检查几种情况:是否像“C:\”或“C:/”这样以盘符开头,或者是否像“\\”和“//”这样表示网络路径 → 出来的是 true 或 false,告诉调用者该不该按 Windows 路径处理。
调用关系:from_absolute_str 用它判断字符串能不能算绝对路径;components 用它决定该用哪些分隔符拆路径;join 用它决定拼接时该用“\”还是“/”。它把容易写错的 Windows 路径识别集中在一个地方。
调用图:被 3 处调用(components, from_absolute_str, join);外部调用 1 个(matches!)。
文件系统路径操作
这些辅助工具基于核心路径类型,在本地应用代码中规范化、检查并安全操作文件系统路径。
utils/path-utils/src/lib.rs源码 ↗
文件路径看起来简单,其实很容易出错:同一个文件可能有相对路径、真实路径、符号链接路径;Windows 路径大小写不敏感,Linux 通常大小写敏感,而 WSL(Windows 上的 Linux 环境)又夹在中间。这个文件就是给 Codex 其他部分共用的“路径整理工具箱”。它先把路径变成系统认可的真实路径,再按运行环境做额外处理,比如在 WSL 的 /mnt/c 这类 Windows 磁盘路径下,把字母统一转小写,方便比较。它还能顺着符号链接找到最终指向的文件,同时在出错或遇到循环链接时保留一个安全的写入位置。最后,它提供“原子写入”:先写到临时文件,再一次性替换目标文件,像先把信写好再投进信箱,避免读者看到半封信。
normalize_for_path_comparison13–16 ↗
fn normalize_for_path_comparison(path: impl AsRef<Path>) -> std::io::Result<PathBuf>
作用:把一个路径整理成适合比较的标准样子。别人想判断两个路径是不是指向同一个地方时,会先用它消除相对路径、符号链接和 WSL 大小写差异带来的干扰。
数据流:进去的是一个路径 → 它先让操作系统把路径转成真实路径,也就是展开相对路径和符号链接 → 再按 WSL 规则做必要的大小写统一 → 出来的是一个整理后的 PathBuf;如果系统无法确认这个路径,就返回错误。
调用关系:它是路径比较流程的第一步。paths_match_after_normalization 会分别拿左右两个路径调用它;它自己会把 WSL 相关的细节交给 normalize_for_wsl。
调用图:调用 1 个内部函数(normalize_for_wsl);被 1 处调用(paths_match_after_normalization);外部调用 1 个(as_ref)。
paths_match_after_normalization21–29 ↗
fn paths_match_after_normalization(left: impl AsRef<Path>, right: impl AsRef<Path>) -> bool
作用:判断两个路径在经过 Codex 的路径整理规则后是不是同一个路径。它适合用在“不想被路径写法差异骗到”的地方。
数据流:进去的是左右两个路径 → 它尝试分别标准化它们 → 如果都成功,就比较标准化后的结果;如果有任意一个失败,就退回到最朴素的路径文本比较 → 出来的是 true 或 false,不修改文件系统。
调用关系:它站在调用者和底层路径细节之间。调用者只问“这俩是不是同一个”,它会把具体整理工作交给 normalize_for_path_comparison,并在整理失败时提供兜底判断。
调用图:调用 1 个内部函数(normalize_for_path_comparison);外部调用 1 个(as_ref)。
normalize_for_native_workdir31–33 ↗
fn normalize_for_native_workdir(path: impl AsRef<Path>) -> PathBuf
作用:把本机工作目录路径整理成当前操作系统更自然、更稳定的写法。尤其在 Windows 上,它会简化一些容易造成混淆的路径形式。
数据流:进去的是一个路径 → 它检查编译出来的程序是不是跑在 Windows 规则下 → 然后把路径和这个判断交给 normalize_for_native_workdir_with_flag → 出来的是整理后的路径。
调用关系:这是给外部代码使用的简洁入口。真正根据“是不是 Windows”做选择的逻辑放在 normalize_for_native_workdir_with_flag 里,这样测试时也更容易控制条件。
调用图:调用 1 个内部函数(normalize_for_native_workdir_with_flag);外部调用 2 个(as_ref, cfg!)。
resolve_symlink_write_paths46–119 ↗
fn resolve_symlink_write_paths(path: &Path) -> io::Result<SymlinkWritePaths>
作用:顺着符号链接找到最终要读的真实位置,同时给出一个安全的写入位置。符号链接可以理解成“快捷方式”,这个函数就是沿着快捷方式一路找源文件,但遇到危险情况会及时停下。
数据流:进去的是一个路径 → 它先尽量确认这个路径是绝对路径,然后从这里开始检查文件信息 → 如果当前路径是符号链接,就读取它指向哪里;相对指向会按当前文件所在目录补全 → 它会记录走过的路径,防止链接 A 指向 B、B 又指回 A 这种死循环 → 出来的是 SymlinkWritePaths:read_path 表示可安全读取的最终路径,write_path 表示应该写入的位置;如果解析失败或发现循环,read_path 会是 None,write_path 会退回原始路径。
调用关系:它服务于需要读写文件但又要尊重符号链接的流程。它自己会调用系统的文件元数据读取和链接读取能力,也会借助 AbsolutePathBuf 来确认或拼出绝对路径;如果一路顺利,它给后续读写步骤一个明确目标,如果不顺利,它用保守结果避免写错地方。
调用图:调用 2 个内部函数(from_absolute_path, resolve_path_against_base);外部调用 4 个(new, into_path_buf, read_link, symlink_metadata)。
write_atomically121–133 ↗
fn write_atomically(write_path: &Path, contents: &str) -> io::Result<()>
作用:安全地把文本写进文件,尽量避免目标文件只写了一半。它适合保存配置、状态或任何不希望被半截内容破坏的文件。
数据流:进去的是目标文件路径和要写入的字符串 → 它先找到父目录,没有父目录就报错 → 确保父目录存在 → 在同一个目录里创建临时文件 → 把内容写进临时文件 → 最后把临时文件一次性替换成目标文件 → 成功时目标文件完整更新,失败时尽量不留下半成品目标文件。
调用关系:它通常会出现在最终落盘的步骤。前面的代码决定要写什么、写到哪里;它负责把“写入”这件事做得更稳,内部把临时文件创建交给 NamedTempFile,把目录创建和写文件交给标准文件系统接口。
调用图:外部调用 4 个(new_in, parent, create_dir_all, write)。
normalize_for_wsl135–137 ↗
fn normalize_for_wsl(path: PathBuf) -> PathBuf
作用:按当前环境判断是否需要做 WSL 路径整理。WSL 是 Windows Subsystem for Linux,也就是 Windows 里的 Linux 环境;这里主要处理 Windows 磁盘路径大小写不敏感的问题。
数据流:进去的是一个路径 → 它询问当前是不是 WSL 环境 → 把路径和这个判断交给 normalize_for_wsl_with_flag → 出来的是可能被转成小写的路径,也可能原样返回。
调用关系:它是 normalize_for_path_comparison 的一部分。上层不需要知道怎么判断 WSL,它在这里调用 is_wsl,然后把具体转换规则交给 normalize_for_wsl_with_flag。
调用图:调用 2 个内部函数(is_wsl, normalize_for_wsl_with_flag);被 1 处调用(normalize_for_path_comparison)。
normalize_for_native_workdir_with_flag139–145 ↗
fn normalize_for_native_workdir_with_flag(path: PathBuf, is_windows: bool) -> PathBuf
作用:根据一个明确传入的“是不是 Windows”标记来整理工作目录路径。它把系统判断和路径处理分开,方便代码测试,也让规则更清楚。
数据流:进去的是一个路径和布尔值 is_windows → 如果是 Windows,就用 dunce 的 simplified 功能把路径简化成更普通的形式;如果不是 Windows,就不动它 → 出来的是整理后的路径。
调用关系:它被 normalize_for_native_workdir 调用。normalize_for_native_workdir 负责拿到真实环境标记,它负责按标记做具体处理。
调用图:被 1 处调用(normalize_for_native_workdir);外部调用 1 个(simplified)。
normalize_for_wsl_with_flag147–157 ↗
fn normalize_for_wsl_with_flag(path: PathBuf, is_wsl: bool) -> PathBuf
作用:根据“是不是 WSL”这个明确标记,决定要不要把某些路径统一成小写。它只处理 WSL 下挂载的 Windows 盘路径,不会乱改普通 Linux 路径。
数据流:进去的是一个路径和 is_wsl 标记 → 如果不是 WSL,直接原样返回 → 如果是 WSL,再判断路径是不是 /mnt/盘符 这种 Windows 磁盘路径 → 是的话把 ASCII 字母转小写,不是的话原样返回 → 出来的是用于稳定比较的路径。
调用关系:它被 normalize_for_wsl 调用,是 WSL 路径规范化的核心判断点。它先请 is_wsl_case_insensitive_path 判断路径是否属于大小写不敏感区域,再在需要时交给 lower_ascii_path 做实际转小写。
调用图:调用 2 个内部函数(is_wsl_case_insensitive_path, lower_ascii_path);被 1 处调用(normalize_for_wsl)。
is_wsl_case_insensitive_path159–186 ↗
fn is_wsl_case_insensitive_path(path: &Path) -> bool
作用:判断一个路径是不是 WSL 里挂载的 Windows 磁盘路径,比如 /mnt/c/Users。因为这类路径在底层通常大小写不敏感,所以比较前需要特殊处理。
数据流:进去的是一个路径 → 在 Linux 编译目标上,它拆开路径的各段,检查是否以根目录开头、第二段是不是 mnt、第三段是不是单个英文字母盘符 → 如果符合就返回 true,否则返回 false;在非 Linux 目标上直接返回 false。
调用关系:它被 normalize_for_wsl_with_flag 用来决定是否需要转小写。检查 mnt 时,它会用 ascii_eq_ignore_case 做不区分大小写的 ASCII 比较,避免 /MNT/c 这类写法漏掉。
调用图:调用 1 个内部函数(ascii_eq_ignore_case);被 1 处调用(normalize_for_wsl_with_flag);外部调用 1 个(components)。
ascii_eq_ignore_case189–195 ↗
fn ascii_eq_ignore_case(left: &[u8], right: &[u8]) -> bool
作用:比较两段 ASCII 字节内容是否相同,但不在乎英文字母大小写。这里的 ASCII 可以理解成最基础的英文字符编码。
数据流:进去的是两段字节数组 → 它先检查长度是否一样 → 再逐个字符转成小写后比较 → 出来的是 true 或 false,不改动输入。
调用关系:它是 is_wsl_case_insensitive_path 的小帮手,专门用来判断路径里的 mnt 这一段是不是等于 mnt,即使用户写成 MNT 或 MnT 也能认出来。
调用图:被 1 处调用(is_wsl_case_insensitive_path)。
lower_ascii_path213–215 ↗
fn lower_ascii_path(path: PathBuf) -> PathBuf
作用:把路径里的 ASCII 字母统一转成小写,主要用于 WSL 下的 Windows 磁盘路径比较。这样 /mnt/c/Users 和 /mnt/c/users 不会被误认为两个不同地方。
数据流:进去的是一个 PathBuf → 在 Linux 目标上,它把路径按原始字节取出,逐个字节做 ASCII 小写转换,再组装回路径 → 在非 Linux 目标上,它不做转换,直接返回原路径 → 出来的是转换后的或原样的路径。
调用关系:它被 normalize_for_wsl_with_flag 在确认路径需要大小写统一时调用。前面的函数负责判断“该不该改”,它只负责“怎么改”。
调用图:被 1 处调用(normalize_for_wsl_with_flag);外部调用 4 个(from_vec, as_os_str, from, with_capacity)。
utils/path-utils/src/env.rs源码 ↗
这个文件很小,但解决的是一个容易踩坑的问题:程序看起来运行在 Linux 上,可实际上可能是在 Windows Subsystem for Linux(简称 WSL,也就是 Windows 提供的 Linux 子系统)里。WSL 里的文件路径、Windows 路径之间常常要互相转换;如果程序不知道自己身处 WSL,就可能按普通 Linux 的方式处理路径,结果路径打不开或转换错。这里的做法像看“身份证”:先查环境变量里有没有 WSL 的标记;如果没有,再读 Linux 系统文件 /proc/version,看看里面是否提到 Microsoft。不是 Linux 系统时,它直接认为不是 WSL。
is_wsl4–19 ↗
fn is_wsl() -> bool
作用:判断当前进程是否运行在 WSL 环境中。需要根据运行环境决定路径怎么转换时,会用到它。
数据流:它不接收参数,而是读取当前进程的环境变量 WSL_DISTRO_NAME,再在 Linux 上尝试读取 /proc/version 这个系统信息文件。只要发现明确的 WSL 痕迹,就返回 true;如果没有发现、读取失败,或者根本不是 Linux 系统,就返回 false。它只做检测,不修改任何文件或环境设置。
调用关系:它被 normalize_for_wsl 使用:在路径需要为 WSL 做特殊整理时,normalize_for_wsl 会先问它“现在是不是 WSL”。它自己只把检查工作交给标准库函数:一个用来读取环境变量,一个用来读取系统文件。
调用图:被 1 处调用(normalize_for_wsl);外部调用 2 个(var_os, read_to_string)。
core/src/utils/path_utils.rs源码 ↗
这个文件本身几乎不写逻辑,只有一句“重新导出”。可以把它理解成商场里的指示牌:真正的店不在这里,但从这里能直接找到。它把 codex_utils_path 里的所有公开内容都暴露出来,其他代码只要通过当前项目的 utils::path_utils 来用路径相关工具就行。这样做的好处是,如果以后路径工具换了来源,很多使用方不一定要跟着改。路径处理通常包括拼接路径、规范化路径、判断路径关系等基础能力;这些小事如果各处自己写,很容易出现不一致甚至安全问题。
ext/memories/src/local/path.rs源码 ↗
这份文件像是本地文件系统入口处的“门卫和整理员”。它会帮别的代码读取一个目录,并把里面的路径排好序,这样每次列出来的顺序都稳定;如果目录不存在,它不把这当成大错误,而是当作“里面什么都没有”。它还会拒绝符号链接。符号链接可以理解成一个“快捷方式”,可能指到根目录外面去,所以这里明确不允许,避免用户借它访问不该访问的位置。文件还提供了判断隐藏文件的方法:名字以点号开头,比如“.cache”,就算隐藏。最后,它能把完整路径变成相对于根目录的好读路径,并统一用“/”连接,方便展示给用户或用于搜索结果。
read_sorted_dir_paths7–21 ↗
async fn read_sorted_dir_paths(
dir_path: &Path,
) -> Result<Vec<PathBuf>, MemoriesBackendError>
作用:读取某个目录里的所有条目,并按路径排序后返回。这样列表和搜索结果不会因为系统返回顺序不同而忽上忽下。
数据流:进去的是一个目录路径。它用异步方式读取目录;如果目录不存在,就直接返回空列表;如果能读到,就把每个条目的完整路径收集起来,再排序。出来的是排好序的一组路径,或者一个文件系统错误。
调用关系:当 list 需要列出目录内容、search_entries 需要遍历搜索范围时,会来找它拿一份稳定的目录清单。它自己把读目录的活交给 tokio 的 read_dir,也就是异步文件读取工具。
调用图:被 2 处调用(list, search_entries);外部调用 2 个(new, read_dir)。
reject_symlink23–34 ↗
fn reject_symlink(
path: &str,
metadata: &std::fs::Metadata,
) -> Result<(), MemoriesBackendError>
作用:检查一个路径是不是符号链接,如果是就拒绝。这样可以防止一个看起来在允许范围内的路径,实际跳到别的地方去。
数据流:进去的是路径字符串和这条路径的元数据,元数据可以理解成文件的身份证信息。它查看文件类型;如果发现是符号链接,就生成一个“无效路径”的错误;如果不是,就什么也不改,返回成功。
调用关系:resolve_scoped_path、ensure_directory、list、read、search 在真正使用路径前都会调用它,相当于先过安全检查。发现问题时,它会通过 MemoriesBackendError::invalid_path 把原因包装成后端统一能理解的错误。
调用图:调用 1 个内部函数(invalid_path);被 5 处调用(resolve_scoped_path, ensure_directory, list, read, search);外部调用 1 个(file_type)。
display_relative_path48–56 ↗
fn display_relative_path(root: &Path, path: &Path) -> String
作用:把一个磁盘上的完整路径,变成相对于某个根目录的显示路径。这样用户看到的是“notes/a.txt”,而不是一长串机器内部路径。
数据流:进去的是根目录路径和目标路径。它先尝试把根目录前缀去掉;如果去不掉,就保留原路径。然后把路径拆成一段段文字,去掉空段,再用“/”拼起来。出来的是适合展示或记录的字符串。
调用关系:resolve_scoped_path、list、build_search_match、search 在需要把内部路径变成用户可理解的相对路径时会调用它。它依赖系统的 strip_prefix 做“去掉根目录”这一步,之后自己统一格式。
调用图:被 4 处调用(resolve_scoped_path, list, build_search_match, search);外部调用 1 个(strip_prefix)。
memories/read/src/lib.rs源码 ↗
这个文件属于 Codex 的“记忆读取”部分。这里的“记忆”可以理解成程序保存下来、以后读取使用的一些资料;而这个文件只管读这一侧,不碰写入流程。它对外声明了几个子模块:citations 用来处理记忆引用,usage 用来处理使用相关能力,metrics 用来做读取访问的分类统计但不公开给外部。文件里真正的函数 memory_root 很简单:给它一个 Codex 的主目录,它就返回主目录下面的 memories 文件夹路径。这样做的好处是,所有地方都用同一套规则找记忆目录,就像大家约定档案柜永远放在办公室里的“memories”抽屉里,不会有人把它找错地方。
memory_root13–15 ↗
fn memory_root(codex_home: &AbsolutePathBuf) -> AbsolutePathBuf
作用:这个函数用来算出“记忆文件夹”的完整路径。别人只要告诉它 Codex 的家目录在哪里,它就会给出里面 memories 子文件夹的位置。
数据流:进去的是一个已经确认过的 Codex 主目录路径 codex_home。函数把固定名字 "memories" 接到这个路径后面,生成一个新的完整路径并返回;它不读取文件,也不创建文件夹,只是算路径。
调用关系:它是读取记忆相关代码寻找记忆目录时会用到的小工具。它把具体拼路径的工作交给路径对象的 join 方法完成,也就是安全地把 codex_home 和 "memories" 合成一个路径。
调用图:调用 1 个内部函数(join)。
git-utils/src/platform.rs源码 ↗
符号链接可以理解成一个“快捷方式”:它看起来像一个文件或目录,但实际指向别的地方。这个文件的作用,就是在不同系统上正确创建这种快捷方式。Unix 系统比较简单,创建符号链接时不太需要区分目标是文件还是目录,所以直接用系统提供的 symlink 就行。Windows 麻烦一点,它创建“指向文件的链接”和“指向目录的链接”用的是两套接口,所以这里会先读取原始路径的元信息,判断它是不是目录链接,再选择 symlink_dir 或 symlink_file。这样做的好处是:项目其他地方不用写一堆“如果是 Windows 就怎样、如果是 Unix 就怎样”的代码。对于不支持的平台,这个文件会直接让编译失败,避免程序运行到一半才发现根本不会创建链接。
create_symlink18–34 ↗
fn create_symlink(
source: &Path,
link_target: &Path,
destination: &Path,
) -> Result<(), GitToolingError>
作用:创建一个符号链接,也就是类似“快捷方式”的东西,让 destination 指向 link_target。有人会用它来复制或重建 Git 工作区里需要保留的链接,而不用真的复制目标文件内容。
数据流:进去的是三个路径:source 是用来判断原链接类型的参考路径,link_target 是新链接要指向哪里,destination 是新链接要放在哪里。在 Windows 上,它会先读取 source 的文件元信息,看看它代表的是目录链接还是文件链接,然后用对应的系统接口创建链接;在 Unix 上,它直接把 link_target 链到 destination。出来的结果是成功或一个 GitToolingError 错误;成功时磁盘上会多出一个新的符号链接。
调用关系:它是这个平台适配文件对外提供的统一入口。上层代码需要创建符号链接时只找它,不直接碰各操作系统自己的接口。在 Windows 分支里,它会把判断类型这件事交给系统的 symlink_metadata,然后再选择创建目录链接或文件链接;这样上层流程可以保持简单。
调用图:外部调用 1 个(symlink_metadata)。
state/src/paths.rs源码 ↗
程序经常需要知道一个文件是不是变过,比如配置文件有没有更新、缓存文件是不是过期。这个文件就做这件很小但很关键的事:给它一个文件路径,它会去系统里查询这个文件的“元信息”(也就是文件大小、修改时间这类属性),拿到最后修改时间后,把它转换成 UTC 时间。UTC 可以理解成全球统一标准时间,避免因为不同机器、不同地区的时区不同而算错。这里还有一个重要行为:如果文件不存在、读不到信息,或者系统没有给出修改时间,它不会让程序崩掉,而是返回“没有结果”。这就像去查一本书的最后借阅时间,查到了就告诉你标准时间,查不到就安静地说“没有记录”。
file_modified_time_utc5–9 ↗
async fn file_modified_time_utc(path: &Path) -> Option<DateTime<Utc>>
作用:这个函数用来查询一个文件的最后修改时间,并把结果变成 UTC 时间。别人想判断文件有没有变化、是不是比某个时间更新时,会用到它。
数据流:输入是一个文件路径。函数先异步读取这个路径对应文件的元信息;如果读取失败,就直接返回空结果。读到后再取出文件的修改时间;如果这一步也失败,同样返回空结果。成功时,它把系统时间转换成 UTC 时间,然后返回这个时间,不修改任何文件内容。
调用关系:它是一个底层辅助函数,通常会被更上层的状态检查、缓存判断或文件同步流程调用。它自己只把“查文件元信息”这件事交给 tokio::fs::metadata 这样的异步文件系统接口来做,然后把得到的时间整理成调用者更容易使用的格式。
调用图:外部调用 1 个(metadata)。
exec-server/src/regular_file.rs源码 ↗
这个文件解决的是一个很实际的问题:别人给你一个路径,你不能只要能打开就当成文件读。这个路径可能是目录,也可能是系统里的特殊对象,比如管道或设备。这里的 open 会先按“只读”方式准备打开选项,再根据不同操作系统加一点安全设置。比如 Unix 上会加非阻塞标志,Windows 上会限制安全身份级别。文件打开后,它还会再检查两件事:这个句柄是不是磁盘文件,以及元数据里它是不是普通文件。可以把它想成门卫:不光看你有没有钥匙开门,还要确认你进的是普通房间,不是机房、通风管或暗门。这样上层代码拿到返回的文件时,就可以放心按普通文件来读。
open4–17 ↗
async fn open(path: &Path) -> io::Result<tokio::fs::File>
作用:打开一个路径对应的普通文件,并返回可异步读取的文件对象。它不只是“打开”,还会确认这个路径真的是普通磁盘文件,防止后续代码把目录、管道或设备误当文件读。
数据流:进去的是一个文件路径。它先创建打开选项,设成只读,再交给 configure_open 按操作系统补上特殊设置;然后真正打开路径。打开成功后,它用 is_disk_file 和文件元数据再检查一遍:如果不是磁盘上的普通文件,就返回一个 InvalidInput 错误;如果检查通过,就把 tokio 的异步文件对象交出去。
调用关系:它是这个文件对外提供的主要入口,会被 open_file_for_read 在需要读取文件时调用。它自己把“按平台设置打开方式”的活交给 configure_open,把“是不是磁盘文件”的判断交给 is_disk_file;如果任何一步不合格,就提前报错,不让问题流到后面的读取流程里。
调用图:调用 2 个内部函数(configure_open, is_disk_file);被 1 处调用(open_file_for_read);外部调用 3 个(new, format!, new)。
configure_open32–32 ↗
fn configure_open(_options: &mut tokio::fs::OpenOptions)
作用:给文件打开选项加上不同操作系统需要的额外设置。这样 open 不用到处写平台判断,只要调用它,就能用比较合适、比较安全的方式打开文件。
数据流:进去的是一份还没真正打开文件的 OpenOptions,也就是“怎么开文件”的设置单。它会根据系统不同修改这份设置:Unix 上加 O_NONBLOCK,意思是尽量不要因为特殊文件阻塞住;Windows 上加 security_qos_flags,限制安全身份相关行为;其他系统上可能什么也不改。出来的不是新对象,而是原来的设置单被改好了。
调用关系:它只在 open 准备打开文件前被调用,是 open 的一个平台适配小零件。它会调用系统库提供的 custom_flags 或 security_qos_flags 来把底层选项写进去,目的就是让同一套打开文件逻辑在不同系统上都更稳妥。
调用图:被 1 处调用(open);外部调用 2 个(custom_flags, security_qos_flags)。
is_disk_file46–48 ↗
fn is_disk_file(_file: &tokio::fs::File) -> bool
作用:判断已经打开的文件句柄是不是来自真正的磁盘文件。这个检查在 Windows 上特别重要,因为有些特殊对象也能被打开,但不能当普通文件处理。
数据流:进去的是一个已经打开的 tokio 文件对象。在 Windows 上,它会取出底层系统句柄,再问操作系统这个句柄的类型是不是 FILE_TYPE_DISK;如果是就返回 true,不是就返回 false。在非 Windows 系统上,这个函数直接返回 true,因为普通文件判断主要交给后面的元数据检查来做。
调用关系:它由 open 在文件打开成功后调用,是第二道把关。open 会把它的结果和 metadata().is_file() 的结果合在一起看;只要发现不像普通磁盘文件,就立刻返回错误,避免上层 open_file_for_read 拿到不安全或不符合预期的文件对象。
调用图:被 1 处调用(open);外部调用 1 个(as_raw_handle)。
file-watcher/src/lib.rs源码 ↗
这个文件把底层操作系统的文件监听能力包装成一个多人共用的服务。外部先创建 FileWatcher,再给每个使用者分配一个 FileWatcherSubscriber;使用者登记路径后,会拿到 Receiver 等待变化消息。这里有几个重要设计:第一,同一个路径被多人监听时,系统只向操作系统登记一次,用 PathWatchCounts 记“有几个人在用”,最后一个人退出才取消监听。第二,如果要监听的文件还不存在,它会先监听最近存在的父目录,等文件或目录被创建后再把监听点挪过去,避免一开始就失败。第三,通知会合并成一批,路径会去重并排序,避免保存一次文件触发一串乱七八糟的消息。它还提供节流和防抖包装:节流像“限流闸门”,防抖像“等一小会儿把连续变化攒成一包”。
Receiver::recv116–132 ↗
async fn recv(&mut self) -> Option<FileWatcherEvent>
作用:等待这个订阅者的下一批文件变化。如果以后再也不会有消息了,它会返回空值,让调用方知道可以停了。
数据流:进去的是一个 Receiver,它里面攒着还没取走的变化路径,也知道发送端还剩几个 → 函数先看有没有已攒好的路径,有就一次性取出、去重后的列表;没有就等通知;如果发送端都没了,就返回 None → 出来的是一个 FileWatcherEvent,或者表示结束的 None,同时内部已取走的路径会被清空。
调用关系:它是订阅者真正收消息的入口。FileWatcher::notify_subscribers 把变化塞进发送端后,这里被各种上层循环调用来取消息;ThrottledWatchReceiver::recv 和 DebouncedWatchReceiver::recv 也会包住它,给消息加节流或防抖。
调用图:被 26 处调用(next_event, recv_broadcast_message, next_event, read_response, read_thread_started_notification, recv_status_changed_notification, next_runtime_command, next_event, forward_ops, next_event (+15 more));外部调用 1 个(take)。
WatchSender::add_changed_paths136–147 ↗
async fn add_changed_paths(&self, paths: &[PathBuf])
作用:把一批变化路径放进某个订阅者的收件箱,并叫醒正在等待的 Receiver。空列表会直接忽略。
数据流:进去的是路径列表 → 函数把这些路径加入一个有序集合,所以重复路径会自动合并;如果确实新增了路径,就发出唤醒信号 → 出来没有返回值,但接收端之后能收到这批路径。
调用关系:FileWatcher::notify_subscribers 判断某个订阅者该收到哪些路径后,会调用它投递消息。它和 Receiver::recv 是一对:一个放消息并敲门,一个等门响后取消息。
WatchSender::clone151–156 ↗
fn clone(&self) -> Self
作用:复制一个发送端,让同一个订阅者的通知通道可以被临时持有。它会顺便记录“还有一个发送者活着”。
数据流:进去的是已有 WatchSender → 函数增加内部发送者计数,并共享同一份收件箱 → 出来是新的 WatchSender,和原来的发往同一个 Receiver。
调用关系:FileWatcher::notify_subscribers 在准备异步投递前会克隆发送端,避免拿着全局锁去等待异步操作。克隆后的发送端最终会由 WatchSender::drop 减少计数。
调用图:外部调用 1 个(clone)。
WatchSender::drop160–164 ↗
fn drop(&mut self)
作用:发送端被销毁时做收尾。如果最后一个发送端没了,它会叫醒接收端,让接收端知道不会再有新消息。
数据流:进去的是即将被释放的 WatchSender → 函数把发送者计数减一;如果减完是零,就通知所有等待者 → 出来没有返回值,但 Receiver::recv 可能因此返回 None。
调用关系:它配合 WatchSender::clone 和 Receiver::recv 使用。订阅者被删除或通道关闭时,接收方不会永远卡住,因为这里会发出最后的结束提醒。
watch_channel167–179 ↗
fn watch_channel() -> (WatchSender, Receiver)
作用:创建一对内部通知通道:一个 WatchSender 用来投递变化,一个 Receiver 用来接收变化。
数据流:进去不需要参数 → 函数新建共享的路径集合、唤醒器和发送者计数 → 出来是一组连接好的 WatchSender 和 Receiver。
调用关系:FileWatcher::add_subscriber 每新增一个订阅者都会调用它。之后 FileWatcher 留着 WatchSender,外部拿走 Receiver。
调用图:被 1 处调用(add_subscriber);外部调用 6 个(clone, new, new, new, new, new)。
PathWatchCounts::increment188–194 ↗
fn increment(&mut self, recursive: bool, amount: usize)
作用:给某个实际监听路径增加引用次数。引用次数可以理解为“有多少登记正在用这个监听”。
数据流:进去的是是否递归监听、增加多少 → 函数把对应的计数加上去,递归和非递归分开记 → 出来没有返回值,但这个路径的使用人数变多了。
调用关系:FileWatcher::register_paths 和 FileWatcher::apply_actual_watch_move 在新增或移动监听时会用它。增加后通常会用 PathWatchCounts::effective_mode 判断操作系统监听模式要不要调整。
PathWatchCounts::decrement196–202 ↗
fn decrement(&mut self, recursive: bool, amount: usize)
作用:给某个实际监听路径减少引用次数,用在取消登记或订阅者退出时。
数据流:进去的是是否递归监听、减少多少 → 函数从对应计数里扣掉,扣到零就停,不会变成负数 → 出来没有返回值,但这个路径的使用人数变少了。
调用关系:FileWatcher::unregister_paths、FileWatcher::remove_subscriber 和 FileWatcher::apply_actual_watch_move 会调用它。扣完后会检查是否还需要继续监听这个路径。
PathWatchCounts::effective_mode204–212 ↗
fn effective_mode(self) -> Option<RecursiveMode>
作用:根据当前计数决定操作系统应该用哪种监听方式:递归、非递归,还是完全不监听。
数据流:进去的是当前递归和非递归计数 → 如果有人需要递归,就返回递归模式;否则如果有人需要非递归,就返回非递归模式;都没有就返回 None → 出来是下一步应该配置给底层监听器的模式。
调用关系:登记、取消、移动监听点时都会先看旧模式和新模式是否不同。只有不同才会调用 FileWatcher::reconfigure_watch,避免无意义地反复配置操作系统监听。
PathWatchCounts::is_empty214–216 ↗
fn is_empty(self) -> bool
作用:判断某个实际路径是不是已经没有任何订阅在用了。
数据流:进去的是两个计数 → 函数检查递归和非递归是否都为零 → 出来是 true 或 false。
调用关系:取消登记、删除订阅者、移动监听点时会用它。为 true 时,这个路径会从引用计数表里移除,并可能取消底层监听。
ThrottledWatchReceiver::new233–239 ↗
fn new(rx: Receiver, interval: Duration) -> Self
作用:把普通 Receiver 包成“节流版”接收器。节流就是两次吐出消息之间至少隔一段时间,防止消息太密。
数据流:进去的是原始 Receiver 和最小间隔时间 → 函数保存它们,并把下一次允许发送的时间设为空 → 出来是 ThrottledWatchReceiver。
调用关系:上层想降低文件变化通知频率时会创建它。之后真正收消息会走 ThrottledWatchReceiver::recv,再由它调用底层 Receiver::recv。
调用图:被 10 处调用(spawn_event_loop, ancestor_events_notify_child_watches, matching_subscribers_are_notified, missing_directory_watch_moves_to_created_directory_for_child_events, missing_file_watch_reports_requested_path_when_parent_changes, missing_file_watch_reports_requested_path_when_parent_delete_event_arrives, non_recursive_watch_ignores_grandchildren, spawn_event_loop_filters_non_mutating_events, throttled_receiver_coalesces_within_interval, throttled_receiver_flushes_pending_on_shutdown)。
ThrottledWatchReceiver::recv243–253 ↗
DebouncedWatchReceiver::new266–272 ↗
fn new(rx: Receiver, interval: Duration) -> Self
作用:把普通 Receiver 包成“防抖版”接收器。防抖就是第一次变化来了以后等一小段时间,把这段时间内的变化合成一批。
数据流:进去的是原始 Receiver 和等待窗口 → 函数保存它们,并准备一个空的路径集合 → 出来是 DebouncedWatchReceiver。
调用关系:需要把编辑器保存文件时产生的连续事件合并时,上层会使用它。后续由 DebouncedWatchReceiver::recv 执行真正的等待和合并。
调用图:被 3 处调用(watch, debounced_receiver_coalesces_each_event_batch, debounced_receiver_flushes_pending_on_shutdown);外部调用 1 个(new)。
DebouncedWatchReceiver::recv275–296 ↗
async fn recv(&mut self) -> Option<FileWatcherEvent>
作用:等待并返回下一批防抖后的文件变化。它会从第一条变化开始计时,时间窗口内来的都合成一包。
数据流:进去的是防抖接收器和已有暂存路径 → 如果还没有路径,就先等底层 Receiver 给第一批;然后设定截止时间,在截止前继续收新事件并加入集合;到点或通道关闭后,把集合取出 → 出来是合并、去重、排序后的 FileWatcherEvent,或 None。
调用关系:它调用 Receiver::recv 获取原始消息,但改变消息的节奏。调用方看到的是更稳定的大批次,而不是底层系统可能产生的多次零散事件。
调用图:调用 1 个内部函数(recv);外部调用 5 个(extend, is_empty, now, take, select!)。
FileWatcherSubscriber::register_paths308–331 ↗
fn register_paths(&self, watched_paths: Vec<WatchPath>) -> WatchRegistration
作用:让某个订阅者登记一组想监听的文件或目录,并返回一个自动取消登记的守卫。守卫是 RAII 风格,也就是对象一销毁就自动清理。
数据流:进去的是用户请求的 WatchPath 列表 → 函数先排序去重,再为每个路径算出实际能交给操作系统监听的路径和用于匹配的路径;然后把登记写进 FileWatcher → 出来是 WatchRegistration,调用方保存它就保持监听,丢掉它就取消监听。
调用关系:这是外部订阅路径的主要入口。它先用 dedupe_watched_paths 清理重复项,再用 actual_watch_path 处理不存在路径,最后交给 FileWatcher::register_paths 修改全局状态。
调用图:调用 1 个内部函数(dedupe_watched_paths);被 3 处调用(register_runtime_extra_roots, register_thread_config, register_path);外部调用 1 个(downgrade)。
FileWatcherSubscriber::register_path334–336 ↗
fn register_path(&self, path: PathBuf, recursive: bool) -> WatchRegistration
作用:测试用的简化入口,只登记一个路径,而不是一组路径。
数据流:进去的是路径和是否递归 → 函数把它包装成只有一个元素的列表 → 出来是 FileWatcherSubscriber::register_paths 返回的 WatchRegistration。
调用关系:它只在测试代码中启用,方便测试少写样板代码。真正逻辑仍然交给 FileWatcherSubscriber::register_paths。
调用图:调用 1 个内部函数(register_paths);外部调用 1 个(vec!)。
FileWatcherSubscriber::drop340–342 ↗
fn drop(&mut self)
作用:订阅者对象被销毁时,把这个订阅者的所有监听都撤掉。
数据流:进去的是即将销毁的 FileWatcherSubscriber → 函数拿它的订阅者编号,让 FileWatcher 删除该订阅者及其路径引用 → 出来没有返回值,但全局监听状态会被清理。
调用关系:它是订阅者生命周期的自动收尾。实际清理工作交给 FileWatcher::remove_subscriber,后者会调整引用计数和底层监听。
WatchRegistration::default353–359 ↗
fn default() -> Self
作用:创建一个空的登记守卫。它不代表任何真实监听,丢掉也不会取消任何东西。
数据流:进去不需要参数 → 函数放入空的弱引用、编号 0 和空路径列表 → 出来是一个安全的空 WatchRegistration。
调用关系:有些上层结构需要先有一个占位登记,之后再替换成真正登记。它的 Drop 逻辑会发现没有可用 FileWatcher,因此不会做事。
调用图:被 3 处调用(new, register_thread_config, clear_listener);外部调用 2 个(new, new)。
WatchRegistration::drop363–367 ↗
fn drop(&mut self)
作用:登记守卫被销毁时,自动取消它代表的那些路径监听。
数据流:进去的是即将释放的 WatchRegistration → 函数尝试把弱引用升级成还活着的 FileWatcher;如果成功,就按订阅者编号和路径列表取消登记 → 出来没有返回值,但对应路径引用会减少。
调用关系:它让调用方不用手动记得取消监听。实际工作交给 FileWatcher::unregister_paths;如果 FileWatcher 本身已经没了,就什么也不用清。
调用图:外部调用 1 个(upgrade)。
FileWatcher::new379–396 ↗
fn new() -> notify::Result<Self>
作用:创建一个真正连接操作系统的文件监听器,并启动后台事件循环。
数据流:进去不需要参数,但要求当前线程里有 Tokio 运行时;Tokio 运行时可以理解为异步任务调度器 → 函数创建底层 notify 监听器和原始事件通道,初始化共享状态,然后启动事件循环 → 出来是可用的 FileWatcher,或者底层监听器创建失败时返回错误。
调用关系:这是生产环境创建监听器的入口。它创建的底层事件会进入 FileWatcher::spawn_event_loop,再被转成订阅者能读的 Receiver 消息。
调用图:被 5 处调用(new, new, dropping_live_watcher_releases_inner_watcher, recursive_registration_downgrades_to_non_recursive_after_drop, unregister_holds_state_lock_until_unwatch_finishes);外部调用 7 个(new, new, new, new, default, unbounded_channel, recommended_watcher)。
FileWatcher::noop400–405 ↗
fn noop() -> Self
作用:创建一个“不真的监听磁盘”的 FileWatcher,主要给测试或特殊场景使用。
数据流:进去不需要参数 → 函数不创建底层操作系统监听器,只创建订阅状态表 → 出来是一个可以登记订阅、也可以由测试手动注入事件的 FileWatcher。
调用关系:测试会用它配合 FileWatcher::send_paths_for_test 验证匹配和分发规则。因为 inner 为空,reconfigure_watch_inner 会直接跳过真实操作系统配置。
调用图:被 16 处调用(new, manager_with_noop_watcher, new, ancestor_events_notify_child_watches, deeply_missing_path_registers_nearest_existing_directory_ancestor, matching_subscribers_are_notified, missing_directory_watch_moves_to_created_directory_for_child_events, missing_file_watch_reports_requested_path_when_parent_changes, missing_file_watch_reports_requested_path_when_parent_delete_event_arrives, missing_path_registers_nearest_existing_parent (+6 more));外部调用 3 个(new, new, default)。
FileWatcher::add_subscriber409–430 ↗
fn add_subscriber(self: &Arc<Self>) -> (FileWatcherSubscriber, Receiver)
作用:新增一个逻辑订阅者,并给它配一条专属收消息通道。
数据流:进去的是共享的 FileWatcher → 函数创建 WatchSender/Receiver,分配新的订阅者编号,把发送端存入全局状态 → 出来是 FileWatcherSubscriber 和对应 Receiver。
调用关系:外部要开始监听前先调用它。之后用 FileWatcherSubscriber::register_paths 登记路径,用 Receiver::recv 接收变化。
调用图:调用 1 个内部函数(watch_channel);外部调用 1 个(new)。
FileWatcher::register_paths432–476 ↗
fn register_paths(
&self,
subscriber_id: SubscriberId,
watched_paths: &[SubscriberWatchRegistration],
)
作用:把某个订阅者的一组路径真正合并进全局监听状态,并按需要配置底层监听器。
数据流:进去的是订阅者编号和已算好的登记信息 → 函数更新该订阅者自己的路径表;如果同一订阅重复登记就只增加计数;再更新全局路径引用计数;如果监听模式从无到有、非递归到递归等发生变化,就重新配置底层监听 → 出来没有返回值,但状态表和操作系统监听可能改变。
调用关系:它由 FileWatcherSubscriber::register_paths 调用。它主要依赖 PathWatchCounts 判断模式变化,并通过 FileWatcher::reconfigure_watch 把变化落到底层 notify 监听器。
调用图:调用 1 个内部函数(reconfigure_watch)。
FileWatcher::unregister_paths478–516 ↗
fn unregister_paths(&self, subscriber_id: SubscriberId, watched_paths: &[SubscriberWatchKey])
作用:取消某个订阅者之前登记的一组路径,并在没人需要时撤掉底层监听。
数据流:进去的是订阅者编号和要取消的路径身份 → 函数找到对应订阅记录,减少该订阅内的计数,计数到零就删除;再减少全局路径引用计数;如果底层监听模式需要变化,就重新配置 → 出来没有返回值,但这些路径不再由该登记守卫保持监听。
调用关系:它通常由 WatchRegistration::drop 自动调用。它和 FileWatcher::register_paths 是一进一出,保证登记和取消成对改变引用计数。
调用图:调用 1 个内部函数(reconfigure_watch)。
FileWatcher::remove_subscriber518–554 ↗
fn remove_subscriber(&self, subscriber_id: SubscriberId)
作用:一次性删除某个订阅者,并清掉它所有还没取消的路径登记。
数据流:进去的是订阅者编号 → 函数从订阅者表中移除它,遍历它所有监听,把对应数量从全局引用计数里扣掉,必要时取消或降级底层监听 → 出来没有返回值,但该订阅者不会再收到消息。
调用关系:它由 FileWatcherSubscriber::drop 调用。这个函数是兜底清理,防止调用方忘了逐个丢掉 WatchRegistration 时留下无用监听。
调用图:调用 1 个内部函数(reconfigure_watch)。
FileWatcher::reconfigure_watch556–563 ↗
fn reconfigure_watch(
&'a self,
path: &Path,
next_mode: Option<RecursiveMode>,
inner_guard: &mut Option<std::sync::MutexGuard<'a, FileWatcherInner>>,
)
作用:根据新的需要,调整某个路径在底层操作系统监听器里的状态。
数据流:进去的是路径、下一步模式和可能已持有的内部锁 → 函数把活转交给 reconfigure_watch_inner,并传入当前 FileWatcher 的底层监听器 → 出来没有返回值,但底层 watch/unwatch 可能发生。
调用关系:FileWatcher::register_paths、FileWatcher::unregister_paths 和 FileWatcher::remove_subscriber 在发现监听模式变化时会调用它。它是实例方法外壳,真正操作在 FileWatcher::reconfigure_watch_inner。
调用图:被 3 处调用(register_paths, remove_subscriber, unregister_paths);外部调用 1 个(reconfigure_watch_inner)。
FileWatcher::reconfigure_watch_inner565–608 ↗
fn reconfigure_watch_inner(
inner: Option<&'a Arc<Mutex<FileWatcherInner>>>,
path: &Path,
next_mode: Option<RecursiveMode>,
inner_guard: &mut Option<std::sync::MutexGua
作用:真正对底层 notify 监听器执行“开始监听、停止监听、换监听模式”。
数据流:进去的是可选的底层监听器、路径、目标模式和锁缓存 → 如果没有真实监听器就直接返回;否则拿锁,查看当前模式;模式相同就不动;需要改变时先取消旧监听,再在路径存在且有目标模式时重新监听 → 出来没有返回值,但内部 watched_paths 表和操作系统监听状态会同步更新;失败会写警告日志。
调用关系:这是所有底层监听配置的最终出口。register、unregister、remove 和 apply_actual_watch_move 都会通过它改变 notify 的实际 watch 列表。
调用图:外部调用 3 个(exists, to_path_buf, warn!)。
FileWatcher::apply_actual_watch_move610–641 ↗
fn apply_actual_watch_move(
path_ref_counts: &mut HashMap<PathBuf, PathWatchCounts>,
old_actual: WatchPath,
new_actual: WatchPath,
count: usize,
inner: Option<&
作用:当原先不存在的路径逐步被创建时,把实际监听点从父目录挪到更准确的位置。
数据流:进去的是全局引用计数表、旧实际路径、新实际路径、引用数量和底层监听器 → 如果新旧一样就不动;否则从旧路径扣掉引用并按需重配,再给新路径加上引用并按需重配 → 出来没有返回值,但全局计数和底层监听点可能迁移。
调用关系:FileWatcher::notify_subscribers 在处理事件时会重新计算 actual_watch_path,发现监听点该移动就收集起来,最后调用这个函数统一应用。
调用图:外部调用 1 个(reconfigure_watch_inner)。
FileWatcher::spawn_event_loop645–672 ↗
fn spawn_event_loop(&self, mut raw_rx: mpsc::UnboundedReceiver<notify::Result<Event>>)
作用:启动后台循环,把 notify 库回调来的原始文件事件接到 Tokio 异步世界里,再分发给订阅者。
数据流:进去的是原始事件接收通道 → 函数尝试取得当前 Tokio 运行时;成功就启动异步任务,不断读取原始事件;只保留创建、修改、删除这类会改变内容的事件;有路径的事件交给 notify_subscribers;错误会写警告 → 出来没有直接结果,但后台任务开始工作。
调用关系:FileWatcher::new 创建底层监听器后会调用它;测试也可通过 FileWatcher::spawn_event_loop_for_test 调用。它用 is_mutating_event 过滤事件,再把分发交给 FileWatcher::notify_subscribers。
调用图:调用 1 个内部函数(is_mutating_event);被 1 处调用(spawn_event_loop_for_test);外部调用 5 个(clone, try_current, notify_subscribers, recv, warn!)。
FileWatcher::notify_subscribers674–733 ↗
async fn notify_subscribers(
state: &RwLock<WatchState>,
inner: Option<&Arc<Mutex<FileWatcherInner>>>,
event_paths: &[PathBuf],
)
作用:根据一批发生变化的真实路径,找出哪些订阅者应该收到哪些对他们有意义的路径。
数据流:进去的是共享状态、可选底层监听器和原始变化路径 → 函数锁住状态,逐个订阅者、逐个登记匹配事件;匹配到的路径会转换回订阅者当初登记的写法;同时重新计算不存在路径的实际监听点是否该移动;释放状态锁后,再把消息投递到各订阅者通道 → 出来没有返回值,但对应 Receiver 会收到变化事件,底层监听点也可能被移动。
调用关系:它是事件分发的核心。FileWatcher::spawn_event_loop 收到真实磁盘事件后调用它,测试的 FileWatcher::send_paths_for_test 也调用它。它依赖 changed_path_for_event 做路径匹配,依赖 actual_watch_path 和 apply_actual_watch_move 处理缺失路径的迁移。
调用图:调用 2 个内部函数(actual_watch_path, changed_path_for_event);外部调用 2 个(apply_actual_watch_move, new)。
FileWatcher::send_paths_for_test736–738 ↗
async fn send_paths_for_test(&self, paths: Vec<PathBuf>)
作用:测试用:不经过操作系统,直接假装这些路径发生了变化。
数据流:进去的是一组路径 → 函数把它们交给 FileWatcher::notify_subscribers → 出来没有返回值,但测试里的订阅者 Receiver 会像收到真实文件事件一样收到通知。
调用关系:它只在测试中编译。它让测试可以专注验证匹配和分发规则,而不用真的创建、修改文件。
调用图:外部调用 1 个(notify_subscribers)。
FileWatcher::spawn_event_loop_for_test741–746 ↗
fn spawn_event_loop_for_test(
&self,
raw_rx: mpsc::UnboundedReceiver<notify::Result<Event>>,
)
作用:测试用:用测试自己提供的原始事件通道启动事件循环。
数据流:进去的是一条 notify 原始事件接收通道 → 函数直接调用 FileWatcher::spawn_event_loop → 出来没有返回值,但后台循环开始消费这条测试通道。
调用关系:它只在测试中编译,用来验证 spawn_event_loop 的过滤和分发行为。真实运行时由 FileWatcher::new 自动启动事件循环。
调用图:调用 1 个内部函数(spawn_event_loop)。
FileWatcher::watch_counts_for_test749–758 ↗
fn watch_counts_for_test(&self, path: &Path) -> Option<(usize, usize)>
作用:测试用:查看某个路径当前的非递归和递归引用计数。
数据流:进去的是路径 → 函数读全局状态表,找到该路径的 PathWatchCounts → 出来是两个数字的组合,或者这个路径没有记录时返回 None。
调用关系:它只在测试中编译,用来检查 register、unregister、remove、监听点移动之后,内部引用计数是否符合预期。
is_mutating_event761–766 ↗
fn is_mutating_event(event: &Event) -> bool
作用:判断一个底层文件事件是不是“会改变文件系统内容”的事件。
数据流:进去的是 notify 的 Event → 函数只把创建、修改、删除算作有效变化,其他例如访问类事件不算 → 出来是 true 或 false。
调用关系:FileWatcher::spawn_event_loop 用它过滤噪声。这样订阅者不会因为无关事件被频繁打扰。
调用图:被 1 处调用(spawn_event_loop);外部调用 1 个(matches!)。
dedupe_watched_paths768–777 ↗
fn dedupe_watched_paths(mut watched_paths: Vec<WatchPath>) -> Vec<WatchPath>
作用:把用户登记的路径列表排序并去掉完全重复的项。
数据流:进去的是 WatchPath 列表 → 函数按路径文字和是否递归排序,然后删除相邻重复项 → 出来是更干净、顺序稳定的列表。
调用关系:FileWatcherSubscriber::register_paths 在正式登记前调用它。这样同一次登记里重复写同一个路径,不会造成多余计数和多余底层配置。
调用图:被 1 处调用(register_paths)。
actual_watch_path785–823 ↗
fn actual_watch_path(requested: &WatchPath) -> (WatchPath, WatchPath, bool)
作用:把用户想监听的路径,转换成“现在实际能监听的路径”和“后面用来匹配事件的路径”。这主要是为了解决目标文件还不存在的情况。
数据流:进去的是用户请求的 WatchPath → 如果目标存在,就实际监听它,并尽量用规范化路径匹配;如果目标不存在,就向上找最近存在的目录,先非递归监听这个父目录,同时记住将来要匹配回用户请求的路径;如果连父目录都找不到,就原样返回 → 出来是实际监听路径、匹配路径,以及是否走了缺失路径兜底。
调用关系:FileWatcherSubscriber::register_paths 初次登记时调用它;FileWatcher::notify_subscribers 处理事件时也会重新调用它,判断实际监听点能不能从父目录移动到更接近目标的位置。
调用图:被 1 处调用(notify_subscribers);外部调用 1 个(clone)。
changed_path_for_event830–852 ↗
fn changed_path_for_event(
subscriber_watch: &SubscriberWatchKey,
subscriber_watch_state: &mut SubscriberWatchState,
event_path: &Path,
) -> Option<PathBuf>
作用:判断一个原始事件路径,对某个订阅登记来说算不算变化;如果算,就返回订阅者应该看到的路径。
数据流:进去的是订阅登记的身份、该登记的可变状态和事件路径 → 函数先用规范化后的匹配路径判断;如果没匹配且规范化路径和原请求写法不同,再用原请求写法试一次 → 出来是应该通知的路径,或者 None。
调用关系:FileWatcher::notify_subscribers 对每个事件和每个订阅登记都会调用它。它把具体匹配规则交给 changed_path_for_matched_path,并负责在“系统报告的真实路径写法”和“用户登记的路径写法”之间兜底。
调用图:调用 1 个内部函数(changed_path_for_matched_path);被 1 处调用(notify_subscribers)。
changed_path_for_matched_path856–895 ↗
fn changed_path_for_matched_path(
subscriber_watch: &SubscriberWatchKey,
subscriber_watch_state: &mut SubscriberWatchState,
matched: &WatchPath,
event_path: &Path,
) -> Option<PathBuf>
作用:在同一套路径写法里应用具体匹配规则,并把结果转换回用户当初登记的路径写法。
数据流:进去的是订阅登记、登记状态、用于匹配的 WatchPath 和事件路径 → 函数检查事件是不是正好命中、是不是父目录变化影响了缺失目标、是不是递归范围内的子路径变化;同时更新 last_exists,记住目标上次是否存在 → 出来是要通知给订阅者的路径,或 None。
调用关系:它由 changed_path_for_event 调用,是路径匹配最细的规则层。它处理非递归不看孙辈、递归看所有后代、缺失文件创建/删除要通知原请求路径等关键细节。
调用图:被 1 处调用(changed_path_for_event);外部调用 4 个(parent, starts_with, strip_prefix, to_path_buf)。
file-system/src/lib.rs源码 ↗
这个文件像一份“文件操作合同”。上层代码只说“读这个文件、写那个文件、列目录”,具体是本地磁盘执行,还是远程环境执行,由实现这个合同的组件决定。这样可以避免代码到处直接碰磁盘,也方便加安全限制。这里的沙箱,就是一圈安全围栏,用来限制程序能读写哪些路径、能不能碰整个磁盘。FileSystemSandboxContext 保存这圈围栏的规则,还会判断当前规则是否真的需要启用沙箱,以及工作目录 cwd 是否必须保留。文件还定义了读目录、文件元数据、复制/删除选项等小数据结构。FileSystemReadStream 则把大文件拆成一块块读,避免一次把超大文件全塞进内存。ExecutorFileSystem 是核心接口,规定了规范化路径、读写文件、创建目录、删除、复制等操作。
FileSystemSandboxContext::from_legacy_sandbox_policy73–92 ↗
fn from_legacy_sandbox_policy(
sandbox_policy: SandboxPolicy,
cwd: PathUri,
) -> io::Result<Self>
作用:把旧格式的沙箱规则转换成这个文件现在使用的新沙箱上下文。这样老配置还能继续用,不会因为内部权限模型升级就失效。
数据流:输入是一份旧的 SandboxPolicy 和当前工作目录 cwd。函数先把 cwd 从 URI 形式转成本机绝对路径,再按这个目录推导文件系统权限、网络权限和沙箱强制方式,最后包装成 FileSystemSandboxContext 返回;如果路径转换失败,就返回输入输出错误。
调用关系:这是兼容旧规则的入口。它会调用旧规则转换、cwd 转绝对路径、运行时权限生成等步骤,最后把活交给 FileSystemSandboxContext::from_permission_profile_with_cwd 来生成统一的新上下文。
调用图:调用 4 个内部函数(from_legacy_sandbox_policy, from_legacy_sandbox_policy_for_cwd, from, to_abs_path);外部调用 2 个(from_runtime_permissions_with_enforcement, from_permission_profile_with_cwd)。
FileSystemSandboxContext::from_permission_profile94–96 ↗
fn from_permission_profile(permissions: PermissionProfile<AbsolutePathBuf>) -> Self
作用:用一份已经整理好的权限配置创建沙箱上下文,但不绑定当前工作目录。适合权限规则不依赖 cwd 的场景。
数据流:输入是一份以本机绝对路径表示的 PermissionProfile。函数把 cwd 设为没有,然后交给内部构造函数统一填默认值,输出一个 FileSystemSandboxContext。
调用关系:这是最常见的简化构造入口之一。测试和只读沙箱、工作区可写沙箱等场景会用它;它自己不做复杂判断,只把数据交给 FileSystemSandboxContext::from_permissions_and_cwd。
调用图:被 7 处调用(read_only_sandbox, workspace_write_sandbox, test_environment_rejects_sandboxed_filesystem_without_runtime_paths, sandbox_cwd_rejects_cwd_dependent_profile_without_context_cwd, sandboxed_file_system_rejects_non_native_uri_as_invalid_input, read_only_sandbox, sandbox_context);外部调用 1 个(from_permissions_and_cwd)。
FileSystemSandboxContext::from_permission_profile_with_cwd98–103 ↗
fn from_permission_profile_with_cwd(
permissions: PermissionProfile<AbsolutePathBuf>,
cwd: PathUri,
) -> Self
作用:用一份权限配置和一个当前工作目录一起创建沙箱上下文。适合权限里有“相对当前目录”或“项目根目录”这类规则的情况。
数据流:输入是 PermissionProfile 和 cwd。函数把 cwd 包成 Some,交给内部构造函数;输出的 FileSystemSandboxContext 会记住这个 cwd,供之后解释相对路径权限时使用。
调用关系:这是需要 cwd 的构造入口。远程文件系统、协议序列化和 cwd 相关沙箱测试会用它;它把具体组装工作交给 FileSystemSandboxContext::from_permissions_and_cwd。
调用图:被 5 处调用(sandbox_context_with_cwd, filesystem_protocol_accepts_legacy_absolute_paths_and_serializes_path_uris, remote_sandbox_context_drops_unused_cwd, remote_sandbox_context_preserves_required_cwd, remote_file_system_sends_path_and_sandbox_cwd_uris_without_native_conversion);外部调用 1 个(from_permissions_and_cwd)。
FileSystemSandboxContext::from_permissions_and_cwd105–116 ↗
fn from_permissions_and_cwd(
permissions: PermissionProfile<AbsolutePathBuf>,
cwd: Option<PathUri>,
) -> Self
作用:这是创建 FileSystemSandboxContext 的内部统一装配点。它负责把权限和可选 cwd 放到同一个结构里,并设置各平台沙箱相关的默认值。
数据流:输入是本机路径形式的权限配置,以及可能存在的 cwd。函数把权限转换成可序列化、可跨环境传递的形式,然后填入 cwd、Windows 沙箱默认关闭、旧版 Landlock 默认关闭等字段,输出完整上下文。
调用关系:它是两个公开构造函数背后的共同零件。from_permission_profile 和 from_permission_profile_with_cwd 都把最终组装交给它,避免同样的默认值逻辑写两遍。
调用图:外部调用 1 个(into)。
FileSystemSandboxContext::should_run_in_sandbox118–128 ↗
fn should_run_in_sandbox(&self) -> bool
作用:判断这份权限规则是否需要真的放进沙箱里执行。简单说,就是看它是不是受限制,而且没有完整磁盘写权限。
数据流:输入是当前沙箱上下文。函数先尝试把权限从 URI 形式转回本机绝对路径形式;如果转不了,说明这可能是别的主机的上下文,为了安全直接认为要沙箱。能转换时,它读取文件系统策略,只有“受限制”并且没有全盘写权限时才返回 true。
调用关系:这是选择执行方式时会用的安全判断。它依赖权限转换和文件系统策略检查,结果会影响后续到底使用受限文件系统,还是可以走不加沙箱的文件系统。
FileSystemSandboxContext::has_cwd_dependent_permissions130–149 ↗
fn has_cwd_dependent_permissions(&self) -> bool
作用:检查权限规则里有没有依赖当前工作目录 cwd 的内容。比如相对路径通配符,或者“项目根目录”这种需要上下文才能解释的路径。
数据流:输入是当前沙箱上下文里的权限配置。函数逐条查看受限制的文件系统权限:如果发现非绝对路径的通配规则,或 ProjectRoots 这种特殊路径,就返回 true;如果权限不受限制、禁用、外部托管,或规则都不依赖 cwd,就返回 false。
调用关系:它是判断 cwd 能不能丢掉的依据。sandbox_cwd 相关流程和 FileSystemSandboxContext::drop_cwd_if_unused 会调用它,避免远程传输时既漏掉必要 cwd,也携带没用信息。
调用图:被 2 处调用(sandbox_cwd, drop_cwd_if_unused)。
FileSystemSandboxContext::drop_cwd_if_unused151–156 ↗
fn drop_cwd_if_unused(mut self) -> Self
作用:如果权限规则不需要当前工作目录,就把 cwd 从沙箱上下文里删掉。这样传给远程环境时更干净,也避免无谓暴露本地路径信息。
数据流:输入是一个 FileSystemSandboxContext。函数先调用 has_cwd_dependent_permissions 判断 cwd 是否有用;如果没用,就把 cwd 改成 None;最后返回修改后的上下文。
调用关系:它通常用在准备发送或保存沙箱上下文之前。它把判断工作交给 FileSystemSandboxContext::has_cwd_dependent_permissions,自己只负责按判断结果清理 cwd。
调用图:调用 1 个内部函数(has_cwd_dependent_permissions)。
FileSystemReadStream::new172–176 ↗
fn new(stream: impl Stream<Item = FileSystemResult<Bytes>> + Send + 'static) -> Self
作用:把一个底层字节流包装成项目统一使用的文件读取流。这样不同来源的文件内容,都能用同一种方式一块块读出来。
数据流:输入是一个会不断产出 Bytes 的流,每一块可能成功也可能带错误。函数把这个流固定到堆上,方便异步系统安全地轮询它,然后输出 FileSystemReadStream。
调用关系:它是流式读文件的包装入口。具体文件系统实现 read_file_stream,或者打开文件时,会用它把自己的底层流包成统一类型,交给上层消费。
调用图:被 2 处调用(read_file_stream, open);外部调用 1 个(pin)。
FileSystemReadStream::poll_next182–184 ↗
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>>
作用:让 FileSystemReadStream 能像普通异步流一样被读取下一块数据。异步流可以理解成“货到了再通知你取”的队列。
数据流:输入是当前流对象和异步运行时给的唤醒上下文。函数把请求转发给内部真正的流;如果有下一块,就返回这块数据或错误;如果暂时没有,就告诉运行时稍后再来;如果结束,就返回结束信号。
调用关系:这是 Stream 接口要求实现的方法。上层读取 FileSystemReadStream 时会间接触发它,它本身不加工文件内容,只把轮询动作交给内部 inner 流。
调用图:外部调用 1 个(as_mut)。
ExecutorFileSystem::read_file_text211–220 ↗
fn read_file_text(
&'a self,
path: &'a PathUri,
sandbox: Option<&'a FileSystemSandboxContext>,
) -> ExecutorFileSystemFuture<'a, String>
作用:把“读文件字节”这个基础操作变成“读文本文件”。它默认按 UTF-8 解码,UTF-8 是最常见的文本编码方式。
数据流:输入是文件路径和可选沙箱上下文。函数先调用同一个文件系统实现的 read_file 拿到原始字节,再尝试把字节转成 String;成功就输出文本,失败就返回“数据不是合法文本”的输入输出错误。
调用关系:这是 ExecutorFileSystem 接口提供的默认便捷方法。很多需要读配置、打补丁、检查文件内容的流程会调用它;它把真正读取文件的工作交给 read_file,只额外负责文本解码和错误包装。
调用图:被 18 处调用(apply_hunks_to_files, derive_new_contents_from_chunks, verify_apply_patch_args, read_optional_file_text_for_delta, remove_failure_was_side_effect_free, read_config_from_path, load_config_toml_for_required_layer, load_project_layers, load_requirements_toml, merge_root_checkout_project_hooks (+8 more));外部调用 2 个(pin, from_utf8)。
可执行文件和资源解析
本组涵盖用于在 Unix、Windows、Cargo、Bazel 和 WSL 环境中定位可运行程序以及构建/测试资源的工具。
cli/src/wsl_paths.rs源码 ↗
WSL 是 Windows Subsystem for Linux,也就是“在 Windows 里跑 Linux 环境”。同一个文件,在 Windows 里可能叫 C:\Temp\codex.zip,但在 WSL 里通常要写成 /mnt/c/Temp/codex.zip。这个文件就像一个“地址翻译员”:先判断当前程序是不是跑在 WSL 里;如果是,再看看传进来的路径是不是 Windows 盘符开头的绝对路径;如果符合,就把盘符变成 /mnt/<盘符>,并把反斜杠换成普通斜杠。它也很谨慎:如果不是 WSL,或者传入的本来就是 Linux 路径,就原样返回,避免把正常路径改坏。文件末尾的测试用几个典型例子确认这个翻译不会出错。
win_path_to_wsl8–23 ↗
fn win_path_to_wsl(path: &str) -> Option<String>
作用:把 Windows 绝对路径转换成 WSL 里的挂载路径。比如把 C:\foo\bar 变成 /mnt/c/foo/bar;如果看起来不像 Windows 盘符路径,就返回“没有结果”。
数据流:进去的是一段路径文字 → 它先检查格式是不是“字母盘符 + 冒号 + 斜杠”,比如 C:\ 或 D:/ → 如果不符合,就返回 None;如果符合,就把盘符改成小写,把后面的反斜杠换成 /,再拼成 /mnt/<盘符>/... 这样的路径返回。
调用关系:它是实际做路径翻译的小工具。normalize_for_wsl 在确认当前环境需要兼容 WSL 后,会把路径交给它尝试转换;它自己只负责字符串转换,最后用格式化拼接出新路径。
调用图:被 1 处调用(normalize_for_wsl);外部调用 1 个(format!)。
normalize_for_wsl27–36 ↗
fn normalize_for_wsl(path: P) -> String
作用:根据当前运行环境,决定要不要把传入路径改成 WSL 能识别的形式。调用者可以放心把路径交给它,不用自己判断是不是在 WSL 里。
数据流:进去的是一个路径对象,不一定是普通字符串 → 它先把路径转成可处理的文字 → 再调用 is_wsl 判断当前是不是 WSL 环境 → 如果不是,就原样返回;如果是,就调用 win_path_to_wsl 尝试把 Windows 路径转成 /mnt/...,能转就返回新路径,不能转就仍然返回原路径。
调用关系:它是外部代码真正会用的入口。比如 run_update_action 做更新操作时需要处理文件路径,就会调用它来避免 Windows 路径在 WSL 里失效;它把环境判断交给 is_wsl,把具体路径改写交给 win_path_to_wsl。
调用图:调用 1 个内部函数(win_path_to_wsl);被 1 处调用(run_update_action);外部调用 2 个(as_ref, is_wsl)。
tests::win_to_wsl_basic43–53 ↗
fn win_to_wsl_basic()
作用:检查 Windows 路径到 WSL 路径的基本转换是否正确。它用几个典型路径,确认盘符、斜杠和非 Windows 路径都按预期处理。
数据流:进去的是测试里写死的几个路径例子 → 它调用 win_path_to_wsl 看转换结果 → 用断言确认 C:\Temp\codex.zip、D:/Work/codex.tgz 会变成正确的 /mnt/... 路径,同时确认普通 Linux 路径不会被误转。
调用关系:它只在运行测试时活跃,用来守住 win_path_to_wsl 的行为不被以后改坏。它不参与正常程序运行,但能及时发现路径翻译规则被破坏。
调用图:外部调用 2 个(assert!, assert_eq!)。
tests::normalize_is_noop_on_unix_paths56–58 ↗
fn normalize_is_noop_on_unix_paths()
作用:确认普通 Unix/Linux 风格路径不会被随便改动。也就是说,/home/u/x 这种路径交进去,出来还应该是它自己。
数据流:进去的是测试里写死的 Linux 路径 /home/u/x → 它调用 normalize_for_wsl → 用断言检查返回值仍然是 /home/u/x,说明这个辅助函数不会乱改正常路径。
调用关系:它在测试阶段保护 normalize_for_wsl 的一个重要承诺:只在该改的时候改,不该改的时候保持原样。这样调用者,比如更新流程里的代码,才能放心使用这个函数。
调用图:外部调用 1 个(assert_eq!)。
linux-sandbox/src/bazel_bwrap.rs源码 ↗
这个文件专门处理一个比较尴尬的问题:程序在 Bazel 里跑测试或调试时,bwrap 不一定在普通路径上,而是藏在 Bazel 的“运行文件”里。运行文件可以理解成 Bazel 给测试准备的一包随身行李,里面有测试要用到的文件。这里的核心函数是 candidate,它先确认当前像不像 Bazel 环境,再从环境变量里拿到 bwrap 的位置。如果拿到的是绝对路径,就直接用;如果只是一个逻辑名字,就去 Bazel 的 runfiles 目录或清单文件里把它翻译成真实磁盘路径。值得注意的是,这套逻辑只在调试构建里启用;非调试构建里它直接返回空,避免正式运行时依赖 Bazel 的测试环境。
candidate24–26 ↗
fn candidate() -> Option<PathBuf>
作用:这个函数尝试给出一个可能可用的 bwrap 程序路径,主要服务于 Bazel 调试或测试环境。它的作用像“先看看测试包里有没有自带工具,有的话告诉主程序在哪里”。
数据流:进去的是当前进程的环境变量,比如是否在 Bazel 包里、是否有 runfiles 信息、以及 CARGO_BIN_EXE_bwrap 指向什么。它先判断环境是否合适;不合适就返回空。合适时,它读取 CARGO_BIN_EXE_bwrap:如果已经是完整路径,就原样返回;如果是 Bazel 的逻辑路径,就交给 resolve_runfile 找真实文件。出来的是一个可能存在的 PathBuf 路径,或者什么也没有。
调用关系:candidate 是这个文件对外提供的入口,会被 legacy_candidates_for_exe 调用,用来补充一条寻找 bwrap 的候选路径。它自己先问 runfiles_env_present 当前有没有 Bazel runfiles 环境,再在需要时把路径解析工作交给 resolve_runfile。
调用图:调用 2 个内部函数(resolve_runfile, runfiles_env_present);被 1 处调用(legacy_candidates_for_exe);外部调用 3 个(from, option_env!, var_os)。
runfiles_env_present29–33 ↗
fn runfiles_env_present() -> bool
作用:这个函数用来判断当前进程是不是带着 Bazel 的 runfiles 信息在运行。简单说,它是在问:“Bazel 有没有告诉我那包测试文件放在哪里?”
数据流:进去的是当前进程环境变量。它检查 RUNFILES_DIR、TEST_SRCDIR、RUNFILES_MANIFEST_FILE 这几个变量,只要其中一个存在,就认为有 Bazel 运行文件环境。出来的是一个布尔值:有就是 true,没有就是 false;它不改动任何东西。
调用关系:它只被 candidate 使用,是 candidate 的第一道门禁。如果这里判断没有 runfiles 环境,candidate 就不会继续尝试用 Bazel 的方式找 bwrap,避免在普通运行环境里乱猜路径。
resolve_runfile36–68 ↗
fn resolve_runfile(logical_path: &str) -> Option<PathBuf>
作用:这个函数把 Bazel 里的“逻辑文件名”变成真实磁盘路径。逻辑文件名像包裹清单上的名字,而这个函数负责找到包裹实际放在哪个柜子里。
数据流:进去的是一个逻辑路径字符串,比如 Bazel 认为的 bwrap 位置。它会先把这个路径和可能的 TEST_WORKSPACE 工作区名前缀组合成几个候选名字。然后它依次查看 RUNFILES_DIR 和 TEST_SRCDIR 指向的目录,看看候选文件是否真的存在。目录里没找到时,它再读取 RUNFILES_MANIFEST_FILE 清单文件,一行行查找逻辑路径对应的真实路径。出来的是找到的真实 PathBuf,找不到就返回空。
调用关系:resolve_runfile 只在 candidate 拿到的路径不是绝对路径时出场。它承担最具体的查找工作:先查目录,再查清单。这样 candidate 不需要知道 Bazel runfiles 的各种摆放方式,只要把“帮我解析这个名字”的任务交给它。
调用图:被 1 处调用(candidate);外部调用 7 个(open, from, format!, var, var_os, new, vec!)。
utils/cargo-bin/src/lib.rs源码 ↗
Rust 项目在测试时经常要启动另一个刚编译好的命令行程序,或者读取一份测试资源文件。问题是 Cargo 和 Bazel 放这些东西的位置不一样:Cargo 往往给绝对路径,Bazel 则常给一种叫 runfiles 的运行时资源路径(可以理解成 Bazel 给测试准备的“随身文件包”)。这个文件就是中间的翻译员。它先看环境变量里有没有 Cargo/Bazel 提供的可执行文件路径;如果有,就按当前运行方式把它变成真正能打开的路径;如果没有,再用 assert_cmd 这个测试辅助库兜底查找。它还提供找测试资源、找仓库根目录的工具,并把找不到、路径不存在等情况包装成清楚的错误。一个容易忽略的点是,这里特别照顾了二进制名字里的短横线:Cargo 导出环境变量时可能把短横线换成下划线,所以它会两个名字都试。
cargo_bin39–69 ↗
fn cargo_bin(name: &str) -> Result<PathBuf, CargoBinError>
作用:根据一个二进制程序的名字,找出当前这次测试里编译出来的那个程序的真实路径。调用者用它就不用关心测试是用 Cargo 还是 Bazel 跑的。
数据流:输入是程序名,比如某个命令行工具的名字。它先生成可能的环境变量名,再逐个读取这些环境变量;如果读到了,就交给 resolve_bin_from_env 把值解析成真实路径。要是环境变量没有提供,它会退一步用 assert_cmd 的 cargo_bin 查找,并把相对路径补成绝对路径。最后输出一个存在的 PathBuf 路径;如果路径不存在或完全找不到,就输出 CargoBinError 错误。
调用关系:它是外部测试代码最常用的入口。它自己先请 cargo_bin_env_keys 准备候选环境变量名;如果发现环境变量,就把具体解析工作交给 resolve_bin_from_env;如果这条路走不通,再调用 assert_cmd 的兜底能力。
调用图:调用 2 个内部函数(cargo_bin_env_keys, resolve_bin_from_env);外部调用 5 个(from, cargo_bin, format!, current_dir, var_os)。
cargo_bin_env_keys71–82 ↗
fn cargo_bin_env_keys(name: &str) -> Vec<String>
作用:为某个二进制名字生成 Cargo 可能使用的环境变量名。它专门处理名字里有短横线的情况,因为 Cargo 可能会把短横线换成下划线。
数据流:输入是二进制名字。它先拼出 CARGO_BIN_EXE_加原名;如果原名里有短横线,就再拼一个把短横线换成下划线后的版本。输出是一组候选环境变量名,不改动外部状态。
调用关系:它只被 cargo_bin 调用,属于找二进制路径前的准备步骤。cargo_bin 拿到这些名字后,才会去系统环境变量里逐个查找。
调用图:被 1 处调用(cargo_bin);外部调用 2 个(with_capacity, format!)。
runfiles_available84–86 ↗
fn runfiles_available() -> bool
作用:判断当前运行环境里是否有 Bazel 的 runfiles 机制。runfiles 可以理解成 Bazel 给程序运行时准备的一包依赖文件和路径索引。
数据流:它不需要显式输入,只读取名为 RUNFILES_MANIFEST_ONLY 的环境变量。这个变量存在,就返回 true;不存在,就返回 false。它不会改动任何东西。
调用关系:它是判断“现在像不像 Bazel 环境”的小开关。resolve_bin_from_env 用它决定要不要用 runfiles 查二进制;repo_root 用它决定仓库根目录应该按 Bazel 方式找,还是按 Cargo 方式找。
调用图:被 2 处调用(repo_root, resolve_bin_from_env);外部调用 1 个(var_os)。
resolve_bin_from_env88–112 ↗
fn resolve_bin_from_env(key: &str, value: OsString) -> Result<PathBuf, CargoBinError>
作用:把环境变量里的二进制路径值,转换成真正存在、可以执行的文件路径。它处理 Cargo 给绝对路径和 Bazel 给 runfiles 路径这两种情况。
数据流:输入是环境变量名和环境变量的值。它先把值当成路径看;如果当前有 Bazel runfiles,就创建 runfiles 查询器,用 rlocation 把 Bazel 的资源路径翻译成真实文件路径,并在必要时把相对路径补到当前目录下面;如果不是 Bazel 环境,就只接受已经存在的绝对路径。成功时输出真实 PathBuf;失败时输出“解析出的路径不存在”的错误,并带上是哪个环境变量导致的。
调用关系:它由 cargo_bin 在发现 CARGO_BIN_EXE 这类环境变量后调用。它内部会先问 runfiles_available 当前是不是 Bazel 场景,然后根据答案选择不同的路径解析办法。
调用图:调用 1 个内部函数(runfiles_available);被 1 处调用(cargo_bin);外部调用 4 个(from, create, rlocation!, current_dir)。
resolve_bazel_runfile140–166 ↗
fn resolve_bazel_runfile(
bazel_package: Option<&str>,
resource: &Path,
) -> std::io::Result<PathBuf>
作用:在 Bazel 运行测试时,根据包名和资源文件名,找到某个测试资源的真实路径。它解决的是“Bazel 给的是逻辑位置,我要的是磁盘上能读的文件”这个问题。
数据流:输入是编译时记录的 Bazel 包名,以及资源文件相对路径。它先创建 runfiles 查询器;如果没有包名,就直接返回“编译时没有设置 BAZEL_PACKAGE”的错误。然后它拼出 Bazel 期望的 runfile 路径,交给 normalize_runfile_path 清理掉多余的 ./ 和可抵消的 ../,再用 rlocation 找真实位置。找到且文件存在就输出路径;否则输出找不到资源的 io 错误。
调用关系:它通常通过 find_resource 这个宏在测试代码里间接使用。find_resource 会先判断当前是不是 Bazel 环境;如果是,就把资源查找工作交给 resolve_bazel_runfile。它还会调用 normalize_runfile_path,避免拼出来的路径带着不必要的绕路片段。
调用图:调用 1 个内部函数(normalize_runfile_path);外部调用 5 个(from, new, format!, create, rlocation!)。
resolve_cargo_runfile168–171 ↗
fn resolve_cargo_runfile(resource: &Path) -> std::io::Result<PathBuf>
作用:在 Cargo 运行测试时,把一个资源文件相对路径变成项目包目录下的路径。它是 Cargo 场景下找测试资源的简单版本。
数据流:输入是资源文件相对路径。它读取编译时的 CARGO_MANIFEST_DIR,也就是当前 Rust 包的目录,然后把资源路径接在这个目录后面。输出拼好的 PathBuf;这个函数本身不检查文件是否真的存在。
调用关系:它是 Cargo 环境下找资源的基础工具。repo_root 在非 Bazel 环境里会用它去找 repo_root.marker;find_resource 宏在 Cargo 环境里也采用同样思路,把资源定位到包目录下面。
repo_root173–207 ↗
fn repo_root() -> io::Result<PathBuf>
作用:找出整个代码仓库的根目录,而不是当前 Rust 包的小目录。测试需要读仓库级文件时,会用到它。
数据流:它不需要显式输入。它先判断 runfiles 是否可用:如果是 Bazel 环境,就从编译时变量 CODEX_REPO_ROOT_MARKER 里拿到标记文件路径,再通过 runfiles 找到这个标记文件;如果是 Cargo 环境,就用 resolve_cargo_runfile 找当前包里的 repo_root.marker。拿到标记文件后,它连续向上走四层父目录,得到仓库根目录并返回。任何一步缺少环境变量、找不到 runfiles、父目录层级不符合预期,都会返回 io 错误。
调用关系:它是需要仓库根目录的测试或工具代码会调用的公共入口。它先用 runfiles_available 分流到 Bazel 或 Cargo 两条路线;Cargo 路线会调用 resolve_cargo_runfile,Bazel 路线会使用 runfiles 查询。
调用图:调用 2 个内部函数(resolve_cargo_runfile, runfiles_available);外部调用 4 个(new, option_env!, create, rlocation!)。
normalize_runfile_path209–231 ↗
fn normalize_runfile_path(path: &Path) -> PathBuf
作用:清理 runfiles 路径里的多余片段,比如去掉 ./,以及把普通目录后面的 ../ 抵消掉。这样交给 Bazel 查询时,路径更规整、更容易匹配。
数据流:输入是一条路径。它逐段查看路径组成部分:遇到当前目录符号 ./ 就跳过;遇到父目录符号 ../ 时,如果前一段是普通目录,就把前一段撤销;否则保留这个 ../;其他部分照常保留。最后把剩下的片段重新拼成一个 PathBuf 输出,不访问磁盘,也不改动外部状态。
调用关系:它只被 resolve_bazel_runfile 调用,属于 Bazel 资源查找前的清洗步骤。resolve_bazel_runfile 拼好逻辑 runfile 路径后,先让它整理路径,再拿整理后的路径去 runfiles 里查询真实文件。
调用图:被 1 处调用(resolve_bazel_runfile);外部调用 4 个(components, new, new, matches!)。
rmcp-client/src/program_resolver.rs源码 ↗
这个文件的重点是抹平 Unix 和 Windows 启动外部程序时的差异。Unix 系统通常可以直接根据 PATH 找到程序,也能靠脚本开头的 shebang(脚本第一行告诉系统用什么解释器运行)执行脚本,所以这里基本原样返回程序名。Windows 不一样,很多脚本必须带上 .cmd、.bat 这类扩展名,单写 npx 可能找不到或跑不起来。于是 Windows 版本会用 which 这个查找工具,根据环境变量 PATH 和当前目录去找真正可执行的完整路径;如果找不到,也不会提前报错,而是把原程序名交回去,让后面的启动流程自己失败并给出错误。文件后半部分是测试:它临时造一个假程序,放进 PATH,分别验证 Unix、Windows 的原生行为,以及 resolve 处理后能不能正常执行。
resolve41–61 ↗
fn resolve(
program: OsString,
env: &HashMap<OsString, OsString>,
cwd: &Path,
) -> std::io::Result<OsString>
作用:这个函数把用户写的程序名变成当前系统更容易执行的形式。它主要是为了 Windows:把没有扩展名的脚本名,尽量解析成带 .cmd、.bat 等扩展名的真实路径。
数据流:进去的是一个程序名、给 MCP 服务使用的环境变量、以及当前工作目录。Unix 上它直接把程序名原样送回;Windows 上它从环境变量里取 PATH,用 which_in 去搜索真实可执行文件,找到就返回完整路径,找不到就记录调试信息并返回原程序名。出来的是一个 OsString,也就是操作系统能接受的程序路径或程序名。
调用关系:它是启动外部 MCP 服务前的一道“找路牌”步骤。真正启动服务的 launch_server 会调用它;测试里的 tests::test_resolved_program_executes_successfully 也会调用它来证明解析后的程序确实能跑。Windows 版本会把具体查找工作交给 which_in,并用 debug! 记录找到或没找到的情况。
调用图:被 2 处调用(test_resolved_program_executes_successfully, launch_server);外部调用 3 个(new, debug!, which_in)。
tests::test_unix_executes_script_without_extension76–102 ↗
async fn test_unix_executes_script_without_extension() -> Result<()>
作用:这个测试验证 Unix 系统可以直接运行 PATH 里的脚本,即使命令里没有写文件扩展名。它是在确认 Unix 不需要像 Windows 那样额外补 .cmd。
数据流:它先用 tests::TestExecutableEnv::new 创建一个临时假程序和配套环境变量,然后用程序名直接启动命令,并把测试环境塞进去。由于某些 Linux 文件系统刚写完脚本时可能短暂提示“可执行文件正忙”,它最多等一下重试两次。最后它检查命令启动是否成功。
调用关系:这是 Unix 专用测试,用来证明 resolve 在 Unix 上可以很简单地原样返回程序名。它依赖 tests::TestExecutableEnv::new 准备临时脚本和 PATH,但不直接调用 resolve,而是测试操作系统本身的行为。
调用图:外部调用 5 个(assert!, new, new, from_millis, sleep)。
tests::test_windows_fails_without_extension107–118 ↗
async fn test_windows_fails_without_extension() -> Result<()>
作用:这个测试验证 Windows 上直接运行没有扩展名的脚本通常会失败。它说明为什么这个文件需要专门为 Windows 做程序解析。
数据流:它先创建一个临时测试环境,里面有一个名为 test_mcp_server.cmd 的脚本。然后它故意只用不带 .cmd 的名字去启动,并带上测试环境变量。出来的结果应该是启动失败,测试会检查这一点。
调用关系:这是 Windows 专用测试,用来展示原始问题:Windows 不像 Unix 那样自然处理这类脚本名。它和后面的成功测试、resolve 成功测试一起,构成了“先证明会坏,再证明修好”的测试链条。
tests::test_windows_succeeds_with_extension123–136 ↗
async fn test_windows_succeeds_with_extension() -> Result<()>
作用:这个测试验证 Windows 上如果明确写上 .cmd 扩展名,脚本就能正常执行。它是在确认失败不是脚本本身坏了,而是缺少扩展名导致的。
数据流:它创建临时测试环境后,把程序名拼成 test_mcp_server.cmd,再用这个完整名字启动命令。命令使用测试环境变量运行,最后测试检查启动结果是成功的。
调用关系:这是 Windows 专用测试,和 tests::test_windows_fails_without_extension 成对出现。前者证明不带扩展名会失败,这个函数证明带扩展名就能成功,从而解释 resolve 在 Windows 上要去查找带扩展名的真实文件。
tests::test_resolved_program_executes_successfully140–157 ↗
async fn test_resolved_program_executes_successfully() -> Result<()>
作用:这个测试验证经过 resolve 处理后的程序名,在所有平台上都应该能成功执行。它是对这个文件核心功能的总体验收。
数据流:它先创建一个临时可执行程序环境,把测试程序名转成 OsString,再把程序名、环境变量和当前目录交给 resolve。resolve 返回解析后的程序名或路径后,它用这个结果启动命令,并检查命令能不能成功启动。
调用关系:这是直接测试 resolve 的主测试。它先靠 tests::TestExecutableEnv::new 搭建假程序,再调用 resolve,最后用 Command 真正执行解析结果,模拟 launch_server 在真实运行时会做的事情。
调用图:调用 1 个内部函数(resolve);外部调用 5 个(from, assert!, new, new, current_dir)。
tests::TestExecutableEnv::new170–190 ↗
fn new() -> Result<Self>
作用:这个函数搭建一个临时的、可控的测试环境。它会造一个假 MCP 服务程序,并把它所在目录放进 PATH,让测试像运行真实命令一样运行它。
数据流:进去不需要外部参数。它创建临时目录,在里面生成平台对应的脚本;然后构造额外环境变量,把临时目录加到 PATH 前面。Windows 上还会确保 PATHEXT 里有 .CMD。接着它调用 create_env_for_mcp_server 合成 MCP 服务要用的环境,最后返回包含临时目录、程序名和环境变量的 TestExecutableEnv。
调用关系:多个测试都会先调用它来准备舞台。它把创建脚本的工作交给 tests::TestExecutableEnv::create_executable,把 PATH 拼接交给 tests::TestExecutableEnv::build_path_env_var,Windows 上还会调用 tests::TestExecutableEnv::ensure_cmd_extension,最后借助 create_env_for_mcp_server 生成最终环境。
调用图:调用 1 个内部函数(create_env_for_mcp_server);外部调用 6 个(new, from, build_path_env_var, create_executable, ensure_cmd_extension, new)。
tests::TestExecutableEnv::create_executable193–208 ↗
fn create_executable(dir: &Path) -> Result<()>
作用:这个函数在临时目录里创建一个最简单的测试程序。它让测试不用依赖电脑上真的安装了 npx 或其他工具。
数据流:进去的是一个目录路径。Windows 上它在目录里写入 test_mcp_server.cmd,内容只是退出成功;Unix 上它写入没有扩展名的 test_mcp_server 脚本,并把它设为可执行。出来没有额外数据,但磁盘上多了一个可供测试启动的假程序。
调用关系:它由 tests::TestExecutableEnv::new 调用,是测试环境搭建的一部分。Unix 上它还会继续调用 tests::TestExecutableEnv::set_executable,确保脚本真的有执行权限。
调用图:外部调用 4 个(join, set_executable, format!, write)。
tests::TestExecutableEnv::set_executable211–217 ↗
fn set_executable(path: &Path) -> Result<()>
作用:这个 Unix 专用函数给测试脚本加上“可以执行”的权限。没有这一步,脚本文件虽然存在,但系统可能不允许把它当程序运行。
数据流:进去的是脚本路径。它读取这个文件当前的权限,把权限改成 0o755,也就是拥有者可读写执行、其他人可读执行,然后把新权限写回文件系统。出来没有新数据,但文件权限被改好了。
调用关系:它只在 Unix 测试环境中使用,由 tests::TestExecutableEnv::create_executable 调用。它保证后续 tests::test_unix_executes_script_without_extension 和解析执行测试能真正启动这个临时脚本。
调用图:外部调用 2 个(metadata, set_permissions)。
tests::TestExecutableEnv::build_path_env_var220–228 ↗
fn build_path_env_var(dir: &Path) -> OsString
作用:这个函数把临时测试目录放到 PATH 的最前面。PATH 是系统找命令时会查的一串目录,把临时目录放最前面,就能优先找到测试造出来的假程序。
数据流:进去的是临时目录路径。它先把这个目录转成环境变量能用的字符串,再读取系统当前 PATH;如果原本有 PATH,就按平台使用分隔符追加在后面,Windows 用分号,Unix 用冒号。出来的是新的 PATH 值。
调用关系:它由 tests::TestExecutableEnv::new 调用,用来构造测试环境变量。这样后面测试用 Command::new 启动 test_mcp_server 时,系统会先在临时目录里找到测试脚本。
tests::TestExecutableEnv::ensure_cmd_extension232–245 ↗
fn ensure_cmd_extension() -> OsString
作用:这个 Windows 专用函数确保 PATHEXT 里包含 .CMD。PATHEXT 是 Windows 用来判断哪些扩展名可以当命令运行的环境变量。
数据流:它读取当前 PATHEXT。如果里面已经有 .CMD,就原样返回;如果没有,就把 .CMD; 加到最前面,再接上原来的内容。出来的是一个保证包含 .CMD 的 PATHEXT 值。
调用关系:它由 tests::TestExecutableEnv::new 在 Windows 上调用。它配合 tests::TestExecutableEnv::create_executable 创建的 .cmd 文件,让 which_in 和 Windows 命令查找机制能够发现这个测试脚本。
环境和终端适配
这些文件检测运行时终端上下文,并据此构造调整后的进程环境或 UI 行为。
core/src/exec_env.rs源码 ↗
程序在运行用户命令时,不能直接把当前进程的所有环境变量都交给子进程。环境变量可以理解成系统给程序的小纸条,里面可能有路径、密钥、代理设置等信息。这个文件就是一个薄薄的转接层:它接收一套 ShellEnvironmentPolicy,也就是“哪些纸条能带、哪些不能带”的规则,再交给 codex_protocol 里的 shell_environment 去真正生成环境变量表。它还会在有线程编号时加入 CODEX_THREAD_ID,让后续运行的命令知道自己属于哪一次对话或任务。这里的关键点是:调用方通常会先清空子进程环境,再只放入这里生成的变量,这样更安全、更可控。文件里另外两个函数主要给测试用,帮助用假的环境变量输入来验证规则是否正确,Windows 上还有专门的测试入口。
create_env20–26 ↗
fn create_env(
policy: &ShellEnvironmentPolicy,
thread_id: Option<ThreadId>,
) -> HashMap<String, String>
作用:根据给定的环境变量规则,生成一份可以交给子进程的环境变量表。有人要运行 shell 命令时会用它,避免把当前程序里的敏感或无关变量随便传出去。
数据流:进去的是一份 ShellEnvironmentPolicy,也就是环境变量筛选规则,以及一个可选的 ThreadId,也就是这次任务的线程编号。它先把线程编号转成字符串,再把规则和编号交给底层的 shell_environment::create_env。出来的是一个 HashMap,也就是“变量名 → 变量值”的表;如果有线程编号,会额外包含 CODEX_THREAD_ID。
调用关系:它位于“准备执行命令”这一步,run_command_under_sandbox、execute_user_shell_command、to_exec_params、open_session_with_sandbox 等地方在启动或整理执行参数时会调用它。它自己不亲自判断每条变量该不该留,而是把实际筛选工作交给 codex_protocol::shell_environment::create_env。
调用图:调用 1 个内部函数(create_env);被 7 处调用(run_command_under_sandbox, execute_user_shell_command, to_exec_params, shell_command_handler_to_exec_params_uses_session_shell_and_turn_context, open_session_with_sandbox, create_env_from_core_vars, create_env_from_core_vars)。
create_env_from_vars29–39 ↗
fn create_env_from_vars(
vars: I,
policy: &ShellEnvironmentPolicy,
thread_id: Option<ThreadId>,
) -> HashMap<String, String>
作用:这是 Windows 测试里用的辅助函数,用来从一批指定的假环境变量生成最终环境表。它让测试不用真的依赖电脑当前的系统环境。
数据流:进去的是一组人为提供的变量、环境变量规则,以及可选线程编号。它把线程编号转成字符串,然后交给 shell_environment::create_env_from_vars。出来的是按规则筛过、补过线程编号的环境变量表;它不会去读取真实系统环境。
调用关系:它只在测试并且目标系统是 Windows 时编译存在。测试代码用它来模拟不同环境,实际筛选逻辑仍然交给 codex_protocol::shell_environment::create_env_from_vars,这样核心规则和正式运行保持一致。
调用图:调用 1 个内部函数(create_env_from_vars)。
populate_env42–52 ↗
fn populate_env(
vars: I,
policy: &ShellEnvironmentPolicy,
thread_id: Option<ThreadId>,
) -> HashMap<String, String>
作用:这是测试用的辅助函数,用来验证“把一组变量按规则填充成最终环境”的过程。它方便测试各种包含、排除、补充变量的情况。
数据流:进去的是一组输入变量、一份环境变量策略,以及可选线程编号。它把线程编号变成字符串,再调用 shell_environment::populate_env。出来的是整理后的环境变量表;这个过程会按策略决定保留什么、添加什么。
调用关系:它只在测试中出现,配合 exec_env_tests.rs 使用。它不是生产路径上的入口,而是把测试数据送到底层 populate_env 逻辑里,帮助确认环境变量规则在各种场景下都按预期工作。
调用图:调用 1 个内部函数(populate_env)。
terminal-detection/src/lib.rs源码 ↗
终端不会主动递名片,所以这个文件像侦探一样读环境变量(系统给程序的小纸条),必要时还会调用 tmux 或 zellij 命令,拼出一份 TerminalInfo。它先看最明确的 TERM_PROGRAM,再看 WezTerm、iTerm、kitty 等各家留下的专属标记,最后才用 TERM 这种比较笼统的能力字符串兜底。遇到 tmux、Zellij 这类终端复用器(一个终端里开多个会话的工具)时,它还会记录复用器信息;tmux 场景下会尽量识别背后真正的终端,而不是只说“这是 tmux”。结果会被缓存起来,避免每次都重新探测。它还能把终端信息整理成 User-Agent(发给服务端时说明自己是谁的一小段文字),并清理掉不适合放进请求头的字符。
TerminalInfo::new91–105 ↗
fn new(
name: TerminalName,
term_program: Option<String>,
version: Option<String>,
term: Option<String>,
multiplexer: Option<Multiplexer>,
) -> Self
作用:把已经探测到的终端名字、版本、TERM 值和复用器信息装进一个 TerminalInfo。它是最底层的“装盒子”函数,其他更方便的创建函数都会靠它。
数据流:进去的是终端类别、TERM_PROGRAM、版本、TERM、tmux 或 Zellij 信息 → 它不再判断真假,只是原样放到结构体的对应位置 → 出来一份完整的 TerminalInfo。
调用关系:它位于创建流程的最底层。TerminalInfo::from_term_program、TerminalInfo::from_name、TerminalInfo::from_term 等函数先做一点分类,再把最终字段交给它组装。
TerminalInfo::from_term_program108–121 ↗
fn from_term_program(
name: TerminalName,
term_program: String,
version: Option<String>,
multiplexer: Option<Multiplexer>,
) -> Self
作用:用 TERM_PROGRAM 这个明确标记创建终端信息。比如终端直接告诉程序“我是 WezTerm”时,就走这条路。
数据流:进去的是识别出的终端类别、TERM_PROGRAM 原文、可选版本和复用器信息 → 它把 TERM_PROGRAM 放进去,并把 TERM 留空 → 出来一份以 TERM_PROGRAM 为主要依据的 TerminalInfo。
调用关系:detect_terminal_info_from_env 在发现 TERM_PROGRAM 后会调用它。它自己不负责判断名字,只负责把上游判断好的结果交给 TerminalInfo::new 打包。
调用图:被 1 处调用(detect_terminal_info_from_env);外部调用 1 个(new)。
TerminalInfo::from_term_program_and_term124–132 ↗
fn from_term_program_and_term(
name: TerminalName,
term_program: String,
version: Option<String>,
term: Option<String>,
multiplexer: Option<Multiplexer>,
)
作用:同时用“终端程序名”和 TERM 能力字符串创建终端信息。主要用于 tmux 场景,因为 tmux 能告诉程序背后客户端终端的名字和 TERM。
数据流:进去的是终端类别、程序名、可选版本、可选 TERM、复用器信息 → 它把这些信息一起保留下来 → 出来一份既知道真实终端,也知道 TERM 能力字符串的 TerminalInfo。
调用关系:terminal_from_tmux_client_info 会在解析 tmux 客户端信息后调用它。它再把字段交给 TerminalInfo::new 统一组装。
调用图:被 1 处调用(terminal_from_tmux_client_info);外部调用 1 个(new)。
TerminalInfo::from_name135–147 ↗
fn from_name(
name: TerminalName,
version: Option<String>,
multiplexer: Option<Multiplexer>,
) -> Self
作用:在没有 TERM_PROGRAM、但能从专属环境变量看出终端是谁时,用这个函数创建信息。比如看到 KITTY_WINDOW_ID,就能认出 kitty。
数据流:进去的是终端类别、可选版本和复用器信息 → 它把 TERM_PROGRAM 和 TERM 都留空,只记录“认出来的名字” → 出来一份按终端类别标记的 TerminalInfo。
调用关系:detect_terminal_info_from_env 在检查 WezTerm、iTerm2、Apple Terminal、kitty、Konsole 等专属标记时会调用它。它把结果交给 TerminalInfo::new。
调用图:被 1 处调用(detect_terminal_info_from_env);外部调用 1 个(new)。
TerminalInfo::from_term150–163 ↗
fn from_term(term: String, multiplexer: Option<Multiplexer>) -> Self
作用:用 TERM 这个兜底字段创建终端信息。TERM 通常描述终端能力,不一定等于终端品牌,所以这里只能识别少数情况。
数据流:进去的是 TERM 字符串和复用器信息 → 它如果看到 dumb 就标成 Dumb,看到 wezterm 或 wezterm-mux 就标成 WezTerm,否则标成 Unknown → 出来一份带 TERM 原文的 TerminalInfo。
调用关系:detect_terminal_info_from_env 在前面所有更明确的线索都没有时调用它。tmux 只拿到 client_termname 而没有 termtype 时,也可能通过 terminal_from_tmux_client_info 间接走到这里。
调用图:被 1 处调用(detect_terminal_info_from_env);外部调用 1 个(new)。
TerminalInfo::unknown166–174 ↗
fn unknown(multiplexer: Option<Multiplexer>) -> Self
作用:在完全认不出终端时创建一个“未知终端”的结果。这样调用方总能拿到结构完整的信息,而不是空值。
数据流:进去的是可选复用器信息 → 它把终端名设为 Unknown,其余终端字段留空 → 出来一份明确表示“没认出来”的 TerminalInfo。
调用关系:detect_terminal_info_from_env 在所有环境变量和 TERM 都没有可用线索时调用它。它仍会保留 tmux 或 Zellij 这类复用器信息。
调用图:被 1 处调用(detect_terminal_info_from_env);外部调用 1 个(new)。
TerminalInfo::user_agent_token177–209 ↗
fn user_agent_token(&self) -> String
作用:把终端信息变成适合放进 User-Agent 的一小段文字。User-Agent 是日志或请求里用来说明“我从什么环境来”的标识。
数据流:进去的是一份 TerminalInfo → 它优先用 TERM_PROGRAM 和版本,其次用 TERM,最后根据终端类别拼一个名字和版本 → 再清理非法字符 → 出来一个安全的字符串。
调用关系:user_agent 会通过 terminal_info 拿到 TerminalInfo,然后调用它生成最终文本。它会调用 format_terminal_version 拼版本,也会调用 sanitize_header_value 做请求头安全清理。
调用图:调用 2 个内部函数(format_terminal_version, sanitize_header_value);外部调用 1 个(format!)。
TerminalInfo::is_zellij212–214 ↗
fn is_zellij(&self) -> bool
作用:快速判断当前是否在 Zellij 这个终端复用器里。调用方不用自己拆开 multiplexer 字段看。
数据流:进去的是 TerminalInfo 自身 → 它检查 multiplexer 是否是 Zellij → 出来 true 或 false,不改动任何数据。
调用关系:它是给其他模块使用的小判断工具。探测流程先由 detect_multiplexer 写入复用器信息,之后外部代码可以用它决定是否采用 Zellij 专门行为。
调用图:外部调用 1 个(matches!)。
Environment::has227–229 ↗
fn has(&self, name: &str) -> bool
作用:判断某个环境变量是否存在。环境变量可以理解为系统塞给程序的配置小纸条。
数据流:进去的是变量名 → 它调用 var 读取这个变量 → 如果读到值就返回 true,否则返回 false。
调用关系:这是 Environment 接口的默认小工具。detect_terminal_info_from_env 用它检查 ITERM_SESSION_ID、WT_SESSION 等只要“存在就算数”的线索。
Environment::var_non_empty232–234 ↗
fn var_non_empty(&self, name: &str) -> Option<String>
作用:读取环境变量,但会把空字符串或全是空格的值当作没有。这样可以避免把没意义的空值误认为有效信息。
数据流:进去的是变量名 → 它先用 var 取值,再用 none_if_whitespace 去掉空白值 → 出来是一个真正有内容的字符串,或 None。
调用关系:has_non_empty 和 zellij_version 会调用它。detect_terminal_info_from_env 也依靠这种“非空读取”来拿 TERM_PROGRAM_VERSION、TERM 等关键值。
调用图:被 2 处调用(has_non_empty, zellij_version)。
Environment::has_non_empty237–239 ↗
fn has_non_empty(&self, name: &str) -> bool
作用:判断某个环境变量不仅存在,而且内容不是空的。它比 has 更严格。
数据流:进去的是变量名 → 它调用 var_non_empty → 如果拿到非空内容就返回 true,否则返回 false。
调用关系:detect_multiplexer 用它检查 TMUX、TMUX_PANE、ZELLIJ 等标记,避免一个空变量误导复用器判断。
调用图:调用 1 个内部函数(var_non_empty)。
Environment::zellij_version245–247 ↗
fn zellij_version(&self) -> Option<String>
作用:从环境变量里读取 Zellij 的版本号。默认做法只看 ZELLIJ_VERSION。
数据流:进去的是环境读取对象本身 → 它读取 ZELLIJ_VERSION,并忽略空白值 → 出来是版本字符串或 None。
调用关系:detect_multiplexer 识别到 Zellij 后会通过它填版本。ProcessEnvironment 重写了这个方法:环境变量没有时,还会尝试运行 zellij --version。
调用图:调用 1 个内部函数(var_non_empty)。
ProcessEnvironment::var254–263 ↗
fn var(&self, name: &str) -> Option<String>
作用:从真实运行中的进程环境里读取变量。这是把抽象 Environment 接口接到真实系统上的地方。
数据流:进去的是变量名 → 它调用 std::env::var 读取系统环境 → 变量不存在就返回 None;值不是合法 UTF-8 时记一条警告并返回 None;正常则返回字符串。
调用关系:detect_terminal_info_from_env 通过 Environment 接口间接用到它。测试可以换成假的 Environment,真实运行时则由 ProcessEnvironment 提供数据。
ProcessEnvironment::tmux_client_info265–267 ↗
fn tmux_client_info(&self) -> TmuxClientInfo
作用:在真实系统里获取 tmux 客户端终端信息。它是 Environment 接口中 tmux 专用信息的真实实现。
数据流:进去的是 ProcessEnvironment 自身 → 它调用模块里的 tmux_client_info 函数去问 tmux → 出来 TmuxClientInfo,里面可能有客户端终端类型和 TERM 名称。
调用关系:detect_terminal_info_from_env 只有在确认 TERM_PROGRAM 是 tmux 且确实在 tmux 里时,才会通过 Environment 调用它。它把实际查询工作交给 tmux_client_info。
调用图:调用 1 个内部函数(tmux_client_info)。
ProcessEnvironment::zellij_version269–272 ↗
fn zellij_version(&self) -> Option<String>
作用:在真实系统里获取 Zellij 版本。它先看环境变量,看不到再尝试运行 zellij 命令。
数据流:进去的是 ProcessEnvironment 自身 → 它先读取非空 ZELLIJ_VERSION → 如果没有,再调用 zellij_version_from_command → 出来版本号或 None。
调用关系:detect_multiplexer 在确认 Zellij 存在时会用它补版本。这个方法比 Environment 的默认实现更积极,因为真实系统可以执行外部命令。
user_agent276–278 ↗
fn user_agent() -> String
作用:给外部调用者一个简单入口,直接拿到当前终端的 User-Agent 片段。调用者不用关心探测细节。
数据流:进去没有参数 → 它调用 terminal_info 拿当前终端信息 → 再把信息格式化成安全字符串 → 出来一个可以用于日志或请求头的终端标识。
调用关系:它是公开 API。内部把工作交给 terminal_info 和 TerminalInfo::user_agent_token,适合 OpenTelemetry 这类日志上报代码使用。
调用图:调用 1 个内部函数(terminal_info)。
terminal_info281–285 ↗
fn terminal_info() -> TerminalInfo
作用:返回当前进程的结构化终端信息。它会缓存第一次探测结果,后面再问就不用重复读环境或执行命令。
数据流:进去没有参数 → 它查看全局缓存 TERMINAL_INFO;如果还没有,就用 ProcessEnvironment 调 detect_terminal_info_from_env 探测 → 出来 TerminalInfo 的克隆副本。
调用关系:user_agent 会调用它。其他需要知道终端类型的代码也可以直接调用它;它负责把真实环境接到探测逻辑上,并用 OnceLock 保证只初始化一次。
调用图:被 1 处调用(user_agent)。
detect_terminal_info_from_env301–388 ↗
fn detect_terminal_info_from_env(env: &dyn Environment) -> TerminalInfo
作用:这是整套终端识别的主流程。它按可信度从高到低查看各种环境线索,最后产出一份 TerminalInfo。
数据流:进去的是一个 Environment,也就是“从哪里读环境”的对象 → 它先识别 tmux 或 Zellij,再按 TERM_PROGRAM、各终端专属变量、TERM 的顺序判断 → 出来结构化的 TerminalInfo。
调用关系:terminal_info 在真实运行时会调用它,测试也可以传假的 Environment 调它。它会调用 detect_multiplexer、is_tmux_term_program、terminal_from_tmux_client_info、terminal_name_from_term_program,以及多个 TerminalInfo::from_* 构造函数。
调用图:调用 8 个内部函数(from_name, from_term, from_term_program, unknown, detect_multiplexer, is_tmux_term_program, terminal_from_tmux_client_info, terminal_name_from_term_program);外部调用 5 个(has, tmux_client_info, var, var_non_empty, matches!)。
detect_multiplexer390–407 ↗
fn detect_multiplexer(env: &dyn Environment) -> Option<Multiplexer>
作用:判断当前是不是跑在 tmux 或 Zellij 这种终端复用器里面。复用器会包在真实终端外面,不单独记录就容易误判。
数据流:进去的是 Environment → 它检查 TMUX、TMUX_PANE、ZELLIJ、ZELLIJ_SESSION_NAME、ZELLIJ_VERSION 等非空变量 → 出来 Tmux、Zellij 或 None,并尽量带上版本。
调用关系:detect_terminal_info_from_env 一开始就调用它,因为后面的终端判断需要知道是否被复用器包住。它会调用 tmux_version_from_env,也会通过 Environment 的 has_non_empty 和 zellij_version 取信息。
调用图:调用 1 个内部函数(tmux_version_from_env);被 1 处调用(detect_terminal_info_from_env);外部调用 2 个(has_non_empty, zellij_version)。
is_tmux_term_program409–411 ↗
fn is_tmux_term_program(value: &str) -> bool
作用:判断 TERM_PROGRAM 的值是不是 tmux。它忽略大小写,避免 TMUX、tmux 这种写法差异造成误判。
数据流:进去的是一个字符串 → 它和 tmux 做大小写不敏感比较 → 出来 true 或 false。
调用关系:detect_terminal_info_from_env 用它决定是否要去读取 tmux 客户端信息。tmux_version_from_env 也用它确认版本号确实属于 tmux。
调用图:被 2 处调用(detect_terminal_info_from_env, tmux_version_from_env)。
terminal_from_tmux_client_info413–435 ↗
fn terminal_from_tmux_client_info(
client_info: TmuxClientInfo,
multiplexer: Option<Multiplexer>,
) -> Option<TerminalInfo>
作用:把 tmux 提供的客户端信息转成 TerminalInfo。这样程序能知道 tmux 背后真正用的是 Ghostty、WezTerm 等终端,而不只是看到 tmux。
数据流:进去的是 TmuxClientInfo 和复用器信息 → 它清掉空值;如果有 termtype,就拆成程序名和版本并识别终端类别;如果只有 termname,就用 TERM 方式兜底 → 出来 TerminalInfo 或 None。
调用关系:detect_terminal_info_from_env 在 TERM_PROGRAM=tmux 且确认处于 tmux 时调用它。它会调用 split_term_program_and_version、terminal_name_from_term_program,以及 TerminalInfo::from_term_program_and_term。
调用图:调用 3 个内部函数(from_term_program_and_term, split_term_program_and_version, terminal_name_from_term_program);被 1 处调用(detect_terminal_info_from_env)。
tmux_version_from_env437–444 ↗
fn tmux_version_from_env(env: &dyn Environment) -> Option<String>
作用:从环境变量里读取 tmux 版本,但只在 TERM_PROGRAM 明确是 tmux 时才相信这个版本值。
数据流:进去的是 Environment → 它先读 TERM_PROGRAM;如果不是 tmux 就返回 None;如果是,再读非空 TERM_PROGRAM_VERSION → 出来 tmux 版本或 None。
调用关系:detect_multiplexer 在识别 tmux 时调用它,用来填 Multiplexer::Tmux 的 version 字段。它依赖 is_tmux_term_program 防止把别的终端版本误当成 tmux 版本。
调用图:调用 1 个内部函数(is_tmux_term_program);被 1 处调用(detect_multiplexer);外部调用 2 个(var, var_non_empty)。
split_term_program_and_version446–451 ↗
fn split_term_program_and_version(value: &str) -> (String, Option<String>)
作用:把像“ghostty 1.2.3”这样的 tmux 客户端 termtype 拆成程序名和版本号。
数据流:进去的是一整段字符串 → 它按空白分割,第一段当程序名,第二段当版本 → 出来一个程序名字符串和一个可选版本。
调用关系:terminal_from_tmux_client_info 用它解析 tmux 的 client_termtype。解析后的程序名会继续交给 terminal_name_from_term_program 识别。
调用图:被 1 处调用(terminal_from_tmux_client_info)。
tmux_client_info453–458 ↗
fn tmux_client_info() -> TmuxClientInfo
作用:向 tmux 查询当前客户端暴露的终端类型和 TERM 名称。它把两次 tmux display-message 的结果合成一个小结构。
数据流:进去没有参数 → 它分别查询 #{client_termtype} 和 #{client_termname} → 出来 TmuxClientInfo,两个字段都可能有值也可能为空。
调用关系:ProcessEnvironment::tmux_client_info 会调用它。它自己把具体命令执行交给 tmux_display_message。
调用图:调用 1 个内部函数(tmux_display_message);被 1 处调用(tmux_client_info)。
tmux_display_message460–472 ↗
fn tmux_display_message(format: &str) -> Option<String>
作用:执行 tmux display-message -p,读取 tmux 的某个格式化字段。可以把它理解成“问 tmux 一个问题”。
数据流:进去的是 tmux 格式字符串,比如 #{client_termtype} → 它运行 tmux 命令,检查命令是否成功,再把输出按 UTF-8 转成文本并去掉空白值 → 出来字符串或 None。
调用关系:tmux_client_info 用它查询两个字段。它调用 none_if_whitespace 过滤空输出;命令失败、tmux 不存在或输出不是合法文本时都会安静返回 None。
调用图:调用 1 个内部函数(none_if_whitespace);被 1 处调用(tmux_client_info);外部调用 2 个(from_utf8, new)。
zellij_version_from_command474–487 ↗
fn zellij_version_from_command() -> Option<String>
作用:在环境变量没有版本时,尝试运行 zellij --version 获取版本。这个失败不会影响终端探测。
数据流:进去没有参数 → 它执行 zellij --version,确认成功后把输出转成文本 → 再交给 parse_zellij_version 提取版本 → 出来版本字符串或 None。
调用关系:ProcessEnvironment::zellij_version 会把它作为备用方案。它体现的是“尽力而为”:命令不存在、执行失败、输出异常都只返回 None。
调用图:调用 1 个内部函数(parse_zellij_version);外部调用 2 个(from_utf8, new)。
parse_zellij_version489–498 ↗
fn parse_zellij_version(value: &str) -> Option<String>
作用:从 zellij --version 的输出里提取版本号。它能处理常见的“zellij 0.x.x”格式,也能保留不常见格式。
数据流:进去的是命令输出文本 → 它先把全空白内容视为 None;如果前两个词是 zellij 和版本,就返回版本;否则返回原文本 → 出来版本字符串或 None。
调用关系:zellij_version_from_command 在拿到命令输出后调用它。它用 none_if_whitespace 避免把空输出当成版本。
调用图:调用 1 个内部函数(none_if_whitespace);被 1 处调用(zellij_version_from_command)。
sanitize_header_value503–505 ↗
fn sanitize_header_value(value: String) -> String
作用:清理要放进 User-Agent 的字符串,把请求头里不合适的字符换成下划线。这样日志或网络请求不会因为奇怪字符出问题。
数据流:进去的是原始终端标识字符串 → 它逐个字符检查,不合法的字符替换为 _ → 出来安全的字符串。
调用关系:TerminalInfo::user_agent_token 在生成最终 User-Agent 片段前调用它。它的字符规则由 is_valid_header_value_char 定义。
调用图:被 1 处调用(user_agent_token)。
is_valid_header_value_char508–510 ↗
fn is_valid_header_value_char(c: char) -> bool
作用:判断一个字符能不能安全放进 User-Agent 片段里。允许的范围很保守,只放行字母数字和少数常见符号。
数据流:进去的是一个字符 → 它检查是否是 ASCII 字母、数字,或 -、_、.、/ → 出来 true 或 false。
调用关系:sanitize_header_value 用它决定哪些字符保留、哪些字符替换。它是请求头清理规则的最小判断单元。
terminal_name_from_term_program512–536 ↗
fn terminal_name_from_term_program(value: &str) -> Option<TerminalName>
作用:把 TERM_PROGRAM 里的各种写法统一识别成固定的终端类别。比如 iTerm.app、iterm2、iTerm 都会归到 Iterm2。
数据流:进去的是 TERM_PROGRAM 字符串 → 它先去掉空格、横线、下划线、点,并统一小写 → 再和已知终端名单匹配 → 出来 TerminalName 或 None。
调用关系:detect_terminal_info_from_env 用它识别普通 TERM_PROGRAM。terminal_from_tmux_client_info 也用它识别 tmux 背后的客户端终端。
调用图:被 2 处调用(detect_terminal_info_from_env, terminal_from_tmux_client_info)。
format_terminal_version538–543 ↗
fn format_terminal_version(name: &str, version: &Option<String>) -> String
作用:把终端名和版本号拼成“名字/版本”的形式;没有版本时只返回名字。
数据流:进去的是终端显示名和可选版本 → 它检查版本是否存在且非空 → 有版本就拼成 name/version,没有就只输出 name。
调用关系:TerminalInfo::user_agent_token 在根据终端类别生成 User-Agent 片段时调用它,避免每种终端都重复写同样的版本拼接逻辑。
调用图:被 1 处调用(user_agent_token);外部调用 1 个(format!)。
none_if_whitespace545–547 ↗
fn none_if_whitespace(value: String) -> Option<String>
作用:把空字符串或全是空白的字符串变成 None。这样后续代码可以把“没有内容”和“有有效内容”分清楚。
数据流:进去的是一个字符串 → 它 trim 后检查是否为空 → 空就出来 None,不空就返回原字符串。
调用关系:parse_zellij_version 和 tmux_display_message 会调用它。Environment::var_non_empty 也用同样思想过滤环境变量里的空值。
调用图:被 2 处调用(parse_zellij_version, tmux_display_message)。
tui/src/clipboard_copy.rs源码 ↗
复制文字看起来简单,其实终端里很容易踩坑:如果人在 SSH 远程机器上,直接写系统剪贴板可能写到远程机器,用户本机根本粘贴不到;如果在 Linux 图形环境里,有些剪贴板还要求写入者进程一直活着。这个文件就像一个“复制路线调度员”。它先判断当前环境:是不是 SSH、是不是 tmux、是不是 WSL。远程时优先让终端帮忙复制,也就是通过 tmux 或 OSC 52(一种终端转发剪贴板的控制码)把内容送回本机终端;本机时先试原生剪贴板 arboard,WSL 里失败后再试 PowerShell 写 Windows 剪贴板,最后才退到终端方案。它还用 ClipboardLease 在 Linux 上留住剪贴板句柄,避免用户还没粘贴,内容就“蒸发”。
copy_to_clipboard40–53 ↗
fn copy_to_clipboard(text: &str) -> Result<Option<ClipboardLease>, String>
作用:这是外部最常用的入口:给它一段文字,它负责把文字复制到用户真正能粘贴到的剪贴板里。调用者不用自己判断 SSH、tmux、WSL 这些复杂环境。
数据流:输入是一段要复制的文字。它先读取环境变量,判断当前是否在 SSH、tmux、WSL 中,然后把这些环境信息和几种复制方法一起交给核心函数;最后返回成功、错误,或者一个需要保存的 ClipboardLease。
调用关系:它是这个文件对外的总开关。它先调用 is_ssh_session、is_tmux_session、is_wsl_session 做环境判断,再把真正的选择工作交给 copy_to_clipboard_with。
调用图:调用 4 个内部函数(copy_to_clipboard_with, is_ssh_session, is_tmux_session, is_wsl_session)。
ClipboardLease::native_linux70–74 ↗
fn native_linux(clipboard: arboard::Clipboard) -> Self
作用:在 Linux 上,它把原生剪贴板对象包起来,提醒程序“这东西要留着,不能马上丢”。这样用户之后粘贴时,剪贴板内容还在。
数据流:输入是一个已经写好文字的 arboard 剪贴板对象。它把这个对象放进 ClipboardLease 里;输出是一个租约式对象,调用者保存它就等于保存剪贴板所有权。
调用关系:它由 arboard_copy 在 Linux 路径里调用。arboard_copy 写完剪贴板后,用它生成 ClipboardLease 返回给上层。
调用图:被 1 处调用(arboard_copy)。
ClipboardLease::test77–82 ↗
fn test() -> Self
作用:这是测试用的假 ClipboardLease,用来让测试模拟“复制后需要保留租约”的情况,而不用真的碰系统剪贴板。
数据流:它不需要真实输入。它创建一个测试用的 ClipboardLease;输出给测试代码,用来检查上层逻辑是否正确处理了“有租约返回”的情况。
调用关系:它只服务测试。测试里的 local_uses_native_clipboard_first 会用它假装原生剪贴板复制成功并返回租约。
copy_to_clipboard_with94–175 ↗
fn copy_to_clipboard_with(
text: &str,
environment: CopyEnvironment,
tmux_copy_fn: impl Fn(&str) -> Result<(), String>,
osc52_copy_fn: impl Fn(&str) -> Result<(), String>,
arboard_
作用:这是复制路线的核心决策器。它决定先试哪条路、失败后怎么换路、最后给用户什么错误信息。
数据流:输入包括要复制的文字、当前环境标记,以及四个可替换的复制函数。它先看是不是 SSH:是的话跳过本机剪贴板,走终端复制;不是的话先试原生剪贴板,WSL 里再试 PowerShell,最后试 tmux 或 OSC 52。输出是成功时的 ClipboardLease 或 None,失败时是把各条失败原因串起来的说明。
调用关系:copy_to_clipboard 会调用它,很多测试也直接调用它并注入假的复制函数。它需要终端路线时,会把工作交给 terminal_clipboard_copy_with。
调用图:调用 1 个内部函数(terminal_clipboard_copy_with);被 13 处调用(copy_to_clipboard, local_non_wsl_falls_back_to_osc52_when_native_fails, local_reports_both_errors_when_native_and_osc52_fail, local_tmux_fallback_prefers_tmux_when_native_fails, local_uses_native_clipboard_first, local_wsl_falls_back_to_osc52_when_native_and_powershell_fail, local_wsl_native_failure_uses_powershell_and_skips_osc52_on_success, local_wsl_reports_native_powershell_and_osc52_errors_when_all_fail, ssh_inside_tmux_falls_back_to_osc52_when_tmux_copy_fails, ssh_inside_tmux_prefers_tmux_clipboard (+3 more));外部调用 1 个(warn!)。
terminal_clipboard_copy_with178–197 ↗
fn terminal_clipboard_copy_with(
text: &str,
tmux_session: bool,
tmux_copy_fn: &impl Fn(&str) -> Result<(), String>,
osc52_copy_fn: &impl Fn(&str) -> Result<(), String>,
) -> Result<()
作用:它负责“让终端帮忙复制”这一条路线。人在 tmux 里时先试 tmux 自带剪贴板转发,失败再退到 OSC 52。
数据流:输入是文字、是否在 tmux 中,以及 tmux 和 OSC 52 两个复制函数。它根据 tmux_session 决定调用顺序;成功输出 Ok,失败输出包含 tmux 和 OSC 52 原因的错误。
调用关系:它只被 copy_to_clipboard_with 调用。它是远程 SSH 和本机原生剪贴板失败后的共同兜底路线。
调用图:被 1 处调用(copy_to_clipboard_with);外部调用 1 个(warn!)。
is_ssh_session200–202 ↗
fn is_ssh_session() -> bool
作用:它判断程序是不是跑在 SSH 远程会话里。这个判断很重要,因为 SSH 下本机剪贴板和远程剪贴板不是一回事。
数据流:它读取 SSH_TTY 和 SSH_CONNECTION 两个环境变量。只要其中一个存在,就返回 true;否则返回 false。
调用关系:copy_to_clipboard 在开始复制前调用它,用这个结果决定要不要绕开远程机器的原生剪贴板。
调用图:被 1 处调用(copy_to_clipboard);外部调用 1 个(var_os)。
is_tmux_session205–207 ↗
fn is_tmux_session() -> bool
作用:它判断当前是不是在 tmux 里。tmux 是一个终端复用工具,里面复制到外层终端剪贴板需要特殊处理。
数据流:它读取 TMUX 和 TMUX_PANE 环境变量。只要检测到其中一个,就返回 true;否则返回 false。
调用关系:copy_to_clipboard 调用它后,把结果传给 copy_to_clipboard_with,后者再决定是否优先走 tmux_clipboard_copy。
调用图:被 1 处调用(copy_to_clipboard);外部调用 1 个(var_os)。
is_wsl_session215–217 ↗
fn is_wsl_session() -> bool
作用:它判断当前是不是 WSL,也就是 Windows 里的 Linux 环境。WSL 里有时 Linux 剪贴板不可用,但 Windows 剪贴板可以通过 PowerShell 写入。
数据流:在 Linux 平台上,它调用 clipboard_paste 模块里的 is_probably_wsl 来判断;非 Linux 平台直接返回 false。输出是一个布尔值。
调用关系:copy_to_clipboard 调用它。结果会影响 copy_to_clipboard_with 是否在原生剪贴板失败后尝试 wsl_clipboard_copy。
调用图:调用 1 个内部函数(is_probably_wsl);被 1 处调用(copy_to_clipboard)。
arboard_copy258–260 ↗
fn arboard_copy(_text: &str) -> Result<Option<ClipboardLease>, String>
作用:它尝试使用 arboard 这个跨平台剪贴板库写入系统原生剪贴板。这是本机复制时最直接、最像普通应用复制的方式。
数据流:输入是要复制的文字。它先创建 SuppressStderr,避免某些平台初始化剪贴板时把杂音打印到终端;然后创建 arboard 剪贴板对象并写入文字。Linux 上输出 Some(ClipboardLease),其他可用平台通常输出 None;失败时输出可读错误。
调用关系:copy_to_clipboard_with 在本机会优先调用它。Linux 成功时它会调用 ClipboardLease::native_linux,把剪贴板对象保留下来。
调用图:调用 2 个内部函数(native_linux, new);外部调用 1 个(new)。
wsl_clipboard_copy309–311 ↗
fn wsl_clipboard_copy(_text: &str) -> Result<(), String>
作用:它在 WSL 里把文字写进 Windows 剪贴板。也就是说,即使程序跑在 Linux 子系统里,用户也能在 Windows 应用里粘贴。
数据流:输入是文字。Linux 上它启动 powershell.exe,把文字通过标准输入送进去,让 PowerShell 执行 Set-Clipboard;然后等待进程结束。成功输出 Ok,失败时收集启动、写入、等待或 PowerShell stderr 的错误。非 Linux 平台则直接说这个兜底不可用。
调用关系:copy_to_clipboard_with 只在检测到 WSL 且原生剪贴板失败后调用它。如果它也失败,流程会继续尝试终端复制。
调用图:外部调用 5 个(from_utf8_lossy, new, format!, null, piped)。
tmux_clipboard_copy318–361 ↗
fn tmux_clipboard_copy(text: &str) -> Result<(), String>
作用:它通过 tmux 自己的剪贴板机制复制文字。这样既能保留 tmux 的粘贴缓冲区,也有机会转发到外层终端的系统剪贴板。
数据流:输入是文字。它先调用 tmux_clipboard_copy_ready 检查 tmux 是否允许剪贴板转发;然后启动 tmux load-buffer,把文字写进它的标准输入;最后根据 tmux 进程退出状态返回成功或错误。
调用关系:terminal_clipboard_copy_with 在 tmux 环境里优先使用它。它内部会先请 tmux_clipboard_copy_ready 做可用性检查。
调用图:调用 1 个内部函数(tmux_clipboard_copy_ready);外部调用 5 个(from_utf8_lossy, new, format!, null, piped)。
tmux_clipboard_copy_ready364–379 ↗
fn tmux_clipboard_copy_ready(
set_clipboard_fn: impl FnOnce() -> Result<String, String>,
tmux_info_fn: impl FnOnce() -> Result<String, String>,
) -> Result<(), String>
作用:它检查 tmux 当前配置是否真的能把剪贴板内容转发到外层终端。这样可以提前发现“看似在 tmux 里复制,实际传不出去”的问题。
数据流:输入是两个查询函数:一个读取 tmux 的 set-clipboard 配置,一个读取 tmux info 信息。它先拒绝 set-clipboard 为 off 的情况,再检查是否缺少 Ms 能力;都没问题就输出 Ok。
调用关系:tmux_clipboard_copy 调用它做前置检查。相关测试也直接调用它,验证启用、禁用、缺能力三种情况。
调用图:被 4 处调用(tmux_clipboard_copy_ready_accepts_forwarding_configuration, tmux_clipboard_copy_ready_rejects_disabled_forwarding, tmux_clipboard_copy_ready_rejects_missing_ms_capability, tmux_clipboard_copy)。
tmux_command_output381–398 ↗
fn tmux_command_output(args: [&str; N]) -> Result<String, String>
作用:它负责运行一条 tmux 命令,并把命令输出拿回来。这样上层不用重复写启动 tmux、检查退出码、解析错误这些杂活。
数据流:输入是一组 tmux 命令参数。它启动 tmux,收集 stdout 和 stderr;成功时把 stdout 转成 UTF-8 字符串输出,失败时输出带状态码或 stderr 的错误。
调用关系:tmux_clipboard_copy 用闭包间接调用它,读取 set-clipboard 和 tmux info。它是 tmux 检查流程里的小工具。
调用图:外部调用 4 个(from_utf8, from_utf8_lossy, new, format!)。
SuppressStderr::drop437–444 ↗
fn drop(&mut self)
作用:它在 macOS 上把之前临时隐藏的 stderr 恢复回来。stderr 是程序打印错误信息的通道,如果不恢复,后续错误信息可能全被丢掉。
数据流:输入是对象里保存的旧 stderr 文件描述符。对象销毁时,它把 fd 2 指回原来的地方,再关闭保存的副本;没有保存成功则什么也不做。
调用关系:它配合 SuppressStderr::new 使用。arboard_copy 创建的保护对象离开作用域时,会自动触发这个恢复动作。
调用图:外部调用 2 个(close, dup2)。
SuppressStderr::new452–454 ↗
fn new() -> Self
作用:它创建一个临时保护罩,在 macOS 上把 stderr 指到 /dev/null,避免剪贴板库乱打印内容把 TUI 屏幕弄花。非 macOS 上它基本是空操作。
数据流:创建时,macOS 路径会复制当前 stderr,打开 /dev/null,再把 stderr 重定向过去;输出一个 guard 对象。这个对象稍后销毁时会恢复 stderr。
调用关系:arboard_copy 在使用 arboard 前调用它。它和 SuppressStderr::drop 成对工作,像进门前戴罩、出门后摘罩。
调用图:被 1 处调用(arboard_copy);外部调用 4 个(close, dup, dup2, open)。
osc52_copy458–476 ↗
fn osc52_copy(text: &str) -> Result<(), String>
作用:它用 OSC 52 控制码请求终端把文字放进剪贴板。OSC 52 可以理解成“写给终端的一段特殊暗号”,很多现代终端收到后会帮你复制。
数据流:输入是文字。它先调用 osc52_sequence 生成控制码;在 Unix 上优先写到 /dev/tty,失败再写到标准输出;最后返回写入成功或错误。
调用关系:terminal_clipboard_copy_with 会在非 tmux 或 tmux 失败时使用它。它内部把生成控制码交给 osc52_sequence,把实际写出交给 write_osc52_to_writer。
调用图:调用 2 个内部函数(osc52_sequence, write_osc52_to_writer);外部调用 4 个(var_os, new, stdout, debug!)。
write_osc52_to_writer478–485 ↗
fn write_osc52_to_writer(mut writer: impl Write, sequence: &str) -> Result<(), String>
作用:它把已经生成好的 OSC 52 控制码写到某个输出目标里,并确保刷出去。这个函数把“怎么写”从“写什么”里拆出来,方便测试。
数据流:输入是一个可写目标和一段控制码字符串。它把字符串字节写进去,再 flush,也就是强制把缓冲内容送出;成功输出 Ok,写入或刷新失败输出错误。
调用关系:osc52_copy 调用它真正发送控制码。测试 write_osc52_to_writer_emits_sequence_verbatim 也用它确认写出的内容没有被改动。
调用图:被 1 处调用(osc52_copy);外部调用 2 个(flush, write_all)。
osc52_sequence487–501 ↗
fn osc52_sequence(text: &str, tmux: bool) -> Result<String, String>
作用:它把普通文字变成 OSC 52 终端控制码。终端只有收到这种格式,才知道这是“请复制到剪贴板”,不是普通文字。
数据流:输入是文字和是否在 tmux 中。它先检查原始字节数,太大就拒绝;然后把文字做 base64 编码,也就是转成适合放进控制码的安全文本;最后按普通终端或 tmux 包裹格式输出字符串。
调用关系:osc52_copy 调用它生成要发送的内容。多个测试也调用它,检查编码、大小限制和 tmux 包裹格式。
调用图:被 2 处调用(osc52_copy, osc52_encoding_roundtrips);外部调用 1 个(format!)。
tests::remote_environment515–521 ↗
fn remote_environment() -> CopyEnvironment
作用:它创建一个测试用的“SSH 远程、非 tmux”环境。这样测试不用真的开 SSH,也能验证远程复制路线。
数据流:它不读取真实环境变量,而是直接构造 CopyEnvironment:ssh_session 为 true,wsl_session 为 true,tmux_session 为 false。输出这个假环境给测试使用。
调用关系:多个 SSH 场景测试调用它,再把结果传给 copy_to_clipboard_with。
tests::remote_tmux_environment523–528 ↗
fn remote_tmux_environment() -> CopyEnvironment
作用:它创建一个测试用的“SSH 远程并且在 tmux 里”的环境。这个场景应该优先测试 tmux 剪贴板路线。
数据流:它先拿 remote_environment 的默认远程配置,再把 tmux_session 改成 true。输出修改后的 CopyEnvironment。
调用关系:SSH+tmux 相关测试调用它,然后检查 copy_to_clipboard_with 是否先试 tmux、再按需要退到 OSC 52。
调用图:外部调用 1 个(remote_environment)。
tests::local_environment530–536 ↗
fn local_environment() -> CopyEnvironment
作用:它创建一个测试用的普通本机环境。这个环境不在 SSH、不在 WSL、不在 tmux。
数据流:它直接构造 CopyEnvironment,三个标记都设为 false。输出给本机复制测试使用。
调用关系:本机非 WSL 的测试调用它,用来验证原生剪贴板优先、失败后走 OSC 52。
tests::local_wsl_environment538–543 ↗
fn local_wsl_environment() -> CopyEnvironment
作用:它创建一个测试用的本机 WSL 环境。这个场景用于确认原生剪贴板失败后,会去试 Windows PowerShell。
数据流:它先调用 local_environment 得到普通本机配置,再把 wsl_session 改成 true。输出这个假环境。
调用关系:WSL 相关测试调用它,然后把环境传给 copy_to_clipboard_with。
调用图:外部调用 1 个(local_environment)。
tests::local_tmux_environment545–550 ↗
fn local_tmux_environment() -> CopyEnvironment
作用:它创建一个测试用的本机 tmux 环境。这个场景用于确认原生剪贴板失败后,会优先走 tmux 而不是直接 OSC 52。
数据流:它先调用 local_environment,再把 tmux_session 改成 true。输出给本机 tmux 兜底测试。
调用关系:local_tmux_fallback_prefers_tmux_when_native_fails 调用它,用来驱动 copy_to_clipboard_with 的 tmux 分支。
调用图:外部调用 1 个(local_environment)。
tests::osc52_encoding_roundtrips553–564 ↗
fn osc52_encoding_roundtrips()
作用:这个测试确认 OSC 52 里的 base64 编码能还原出原文。它防止复制出去的文字被编码过程弄坏。
数据流:输入是一段包含 Markdown 和 Rust 代码块的文字。测试调用 osc52_sequence 生成控制码,取出其中的编码部分再解码,最后断言解码结果等于原文字节。
调用关系:它直接检查 osc52_sequence 的核心编码行为,是 OSC 52 路线的基础正确性测试。
调用图:调用 1 个内部函数(osc52_sequence);外部调用 1 个(assert_eq!)。
tests::osc52_rejects_payload_larger_than_limit567–576 ↗
fn osc52_rejects_payload_larger_than_limit()
作用:这个测试确认 OSC 52 不会接受超过限制的大文本。这样可以避免把终端撑爆或造成卡顿。
数据流:它构造一段比 OSC52_MAX_RAW_BYTES 多 1 字节的字符串,调用 osc52_sequence,期望得到明确的“太大”错误。
调用关系:它验证 osc52_sequence 的安全阀,确保 osc52_copy 发送前就会拦住过大的内容。
调用图:外部调用 1 个(assert_eq!)。
tests::osc52_wraps_tmux_passthrough579–584 ↗
fn osc52_wraps_tmux_passthrough()
作用:这个测试确认在 tmux 环境下,OSC 52 控制码会被正确包一层 tmux 透传格式。没有这层,外层终端可能收不到复制请求。
数据流:它输入 hello 和 tmux=true,调用 osc52_sequence,然后断言生成的字符串正好是预期的 tmux 包裹格式。
调用关系:它覆盖 osc52_sequence 的 tmux 分支,保障 tmux 中的 OSC 52 兜底可用。
调用图:外部调用 1 个(assert_eq!)。
tests::write_osc52_to_writer_emits_sequence_verbatim587–592 ↗
fn write_osc52_to_writer_emits_sequence_verbatim()
作用:这个测试确认写出 OSC 52 控制码时不会偷偷改内容。控制码多一个少一个字符都可能失效。
数据流:它准备一个内存 Vec 当作假输出目标,把控制码交给 write_osc52_to_writer,再检查 Vec 里的字节和原字符串完全一致。
调用关系:它直接测试 write_osc52_to_writer,为 osc52_copy 的实际发送步骤兜底。
调用图:外部调用 2 个(new, assert_eq!)。
tests::ssh_uses_osc52_and_skips_native_on_success595–626 ↗
fn ssh_uses_osc52_and_skips_native_on_success()
作用:这个测试确认 SSH 远程环境下复制成功时,会用 OSC 52,并且不会碰远程机器的原生剪贴板。
数据流:它创建远程环境和几个计数用的假复制函数。调用 copy_to_clipboard_with 后,检查 OSC 52 被调用一次,原生剪贴板、WSL、tmux 都没被调用。
调用关系:它验证 copy_to_clipboard_with 的 SSH 非 tmux 主路线。
调用图:调用 1 个内部函数(copy_to_clipboard_with);外部调用 4 个(new, assert!, assert_eq!, remote_environment)。
tests::ssh_returns_osc52_error_and_skips_native629–663 ↗
fn ssh_returns_osc52_error_and_skips_native()
作用:这个测试确认 SSH 下 OSC 52 失败时,会返回清楚的错误,并且仍然不会去写远程原生剪贴板。
数据流:它让 OSC 52 假函数返回 blocked 错误。调用 copy_to_clipboard_with 后,检查错误文本是否说明 SSH 下 OSC 52 失败,同时确认其他复制函数没有被误调用。
调用关系:它覆盖 copy_to_clipboard_with 的 SSH 错误分支。
调用图:调用 1 个内部函数(copy_to_clipboard_with);外部调用 4 个(new, assert_eq!, panic!, remote_environment)。
tests::ssh_inside_tmux_prefers_tmux_clipboard666–697 ↗
fn ssh_inside_tmux_prefers_tmux_clipboard()
作用:这个测试确认 SSH 加 tmux 时,会优先使用 tmux 剪贴板路线。这样更符合 tmux 自己的转发机制。
数据流:它创建远程 tmux 环境,设置 tmux 假函数成功。调用 copy_to_clipboard_with 后,检查只有 tmux 被调用,OSC 52 和本机路线都没被调用。
调用关系:它验证 terminal_clipboard_copy_with 在 tmux=true 时的优先顺序。
调用图:调用 1 个内部函数(copy_to_clipboard_with);外部调用 4 个(new, assert!, assert_eq!, remote_tmux_environment)。
tests::ssh_inside_tmux_falls_back_to_osc52_when_tmux_copy_fails700–731 ↗
fn ssh_inside_tmux_falls_back_to_osc52_when_tmux_copy_fails()
作用:这个测试确认 SSH+tmux 里,如果 tmux 复制失败,会自动退到 OSC 52。用户不应该因为 tmux 配置问题就完全不能复制。
数据流:它让 tmux 假函数返回错误,让 OSC 52 假函数成功。调用 copy_to_clipboard_with 后,检查 tmux 和 OSC 52 都被调用,并且整体成功。
调用关系:它覆盖 terminal_clipboard_copy_with 的 tmux 失败兜底逻辑。
调用图:调用 1 个内部函数(copy_to_clipboard_with);外部调用 4 个(new, assert!, assert_eq!, remote_tmux_environment)。
tests::ssh_inside_tmux_reports_tmux_and_osc52_errors_when_both_fail734–751 ↗
fn ssh_inside_tmux_reports_tmux_and_osc52_errors_when_both_fail()
作用:这个测试确认 SSH+tmux 里两条终端路线都失败时,错误信息会同时说明 tmux 和 OSC 52 的失败原因。
数据流:它让 tmux 和 OSC 52 假函数都返回错误。调用 copy_to_clipboard_with 后,期望得到一条包含两个原因的错误字符串。
调用关系:它验证 copy_to_clipboard_with 和 terminal_clipboard_copy_with 组合后的错误拼接是否对用户有帮助。
调用图:调用 1 个内部函数(copy_to_clipboard_with);外部调用 3 个(assert_eq!, panic!, remote_tmux_environment)。
tests::tmux_clipboard_copy_ready_accepts_forwarding_configuration754–761 ↗
fn tmux_clipboard_copy_ready_accepts_forwarding_configuration()
作用:这个测试确认 tmux 配置正常时,前置检查会放行。也就是 set-clipboard 可用,并且 Ms 能力存在。
数据流:它传入两个假的查询函数:一个返回 external,一个返回含有 Ms 能力的信息。调用 tmux_clipboard_copy_ready 后,期望得到 Ok。
调用关系:它直接验证 tmux_clipboard_copy_ready 的成功路径。
调用图:调用 1 个内部函数(tmux_clipboard_copy_ready);外部调用 1 个(assert_eq!)。
tests::tmux_clipboard_copy_ready_rejects_disabled_forwarding764–774 ↗
fn tmux_clipboard_copy_ready_rejects_disabled_forwarding()
作用:这个测试确认如果 tmux 明确关闭剪贴板转发,程序会提前报错,而不是继续做无用复制。
数据流:它让 set-clipboard 查询返回 off,并让 tmux info 查询如果被调用就 panic。调用 tmux_clipboard_copy_ready 后,期望得到“转发被禁用”的错误。
调用关系:它验证 tmux_clipboard_copy_ready 的第一道拦截,并确认禁用时不会再查后续信息。
调用图:调用 1 个内部函数(tmux_clipboard_copy_ready);外部调用 1 个(assert_eq!)。
tests::tmux_clipboard_copy_ready_rejects_missing_ms_capability777–787 ↗
fn tmux_clipboard_copy_ready_rejects_missing_ms_capability()
作用:这个测试确认 tmux 缺少 Ms 能力时会报错。Ms 可以理解成 tmux 告诉程序“我能把剪贴板控制码传出去”的能力标记。
数据流:它让 set-clipboard 看起来可用,但 tmux info 返回 Ms 缺失。调用 tmux_clipboard_copy_ready 后,期望得到“缺少 Ms capability”的错误。
调用关系:它覆盖 tmux_clipboard_copy_ready 的第二道检查。
调用图:调用 1 个内部函数(tmux_clipboard_copy_ready);外部调用 1 个(assert_eq!)。
tests::local_uses_native_clipboard_first790–816 ↗
fn local_uses_native_clipboard_first()
作用:这个测试确认本机复制时优先使用原生剪贴板。即使环境标记为 WSL,只要原生方式成功,就不需要 PowerShell 或 OSC 52。
数据流:它创建本机 WSL 环境,让原生剪贴板假函数成功并返回测试租约。调用 copy_to_clipboard_with 后,检查返回 Some,并确认其他兜底路线没被调用。
调用关系:它验证 copy_to_clipboard_with 的本机优先级:原生剪贴板排第一。
调用图:调用 1 个内部函数(copy_to_clipboard_with);外部调用 4 个(new, assert!, assert_eq!, local_wsl_environment)。
tests::local_non_wsl_falls_back_to_osc52_when_native_fails819–845 ↗
fn local_non_wsl_falls_back_to_osc52_when_native_fails()
作用:这个测试确认普通本机环境下,原生剪贴板失败后会退到 OSC 52。这样即使系统剪贴板库不可用,也还有终端复制机会。
数据流:它创建普通本机环境,让原生假函数失败、OSC 52 假函数成功。调用 copy_to_clipboard_with 后,检查整体成功,原生和 OSC 52 各调用一次,WSL 没调用。
调用关系:它覆盖 copy_to_clipboard_with 的本机非 WSL 兜底路线。
调用图:调用 1 个内部函数(copy_to_clipboard_with);外部调用 4 个(new, assert!, assert_eq!, local_environment)。
tests::local_tmux_fallback_prefers_tmux_when_native_fails848–879 ↗
fn local_tmux_fallback_prefers_tmux_when_native_fails()
作用:这个测试确认本机在 tmux 里时,如果原生剪贴板失败,兜底会先走 tmux 而不是直接 OSC 52。
数据流:它创建本机 tmux 环境,让原生失败、tmux 成功。调用 copy_to_clipboard_with 后,检查 tmux 被调用,OSC 52 没被调用。
调用关系:它验证 copy_to_clipboard_with 失败后进入 terminal_clipboard_copy_with 时,tmux 优先级仍然生效。
调用图:调用 1 个内部函数(copy_to_clipboard_with);外部调用 4 个(new, assert!, assert_eq!, local_tmux_environment)。
tests::local_wsl_native_failure_uses_powershell_and_skips_osc52_on_success882–908 ↗
fn local_wsl_native_failure_uses_powershell_and_skips_osc52_on_success()
作用:这个测试确认 WSL 里原生剪贴板失败后,会先用 PowerShell 写 Windows 剪贴板,并且成功后不会再走 OSC 52。
数据流:它创建本机 WSL 环境,让原生失败、WSL PowerShell 成功。调用 copy_to_clipboard_with 后,检查 WSL 被调用一次,OSC 52 没被调用。
调用关系:它覆盖 copy_to_clipboard_with 的 WSL 专属兜底优先级。
调用图:调用 1 个内部函数(copy_to_clipboard_with);外部调用 4 个(new, assert!, assert_eq!, local_wsl_environment)。
tests::local_wsl_falls_back_to_osc52_when_native_and_powershell_fail911–937 ↗
fn local_wsl_falls_back_to_osc52_when_native_and_powershell_fail()
作用:这个测试确认 WSL 中原生剪贴板和 PowerShell 都失败时,还会继续退到 OSC 52。也就是三条路按顺序试,不会中途放弃。
数据流:它让原生和 WSL 假函数失败,让 OSC 52 成功。调用 copy_to_clipboard_with 后,检查三条相关路线都被调用,并且整体成功。
调用关系:它验证 copy_to_clipboard_with 的 WSL 多级兜底链。
调用图:调用 1 个内部函数(copy_to_clipboard_with);外部调用 4 个(new, assert!, assert_eq!, local_wsl_environment)。
tests::local_reports_both_errors_when_native_and_osc52_fail940–972 ↗
fn local_reports_both_errors_when_native_and_osc52_fail()
作用:这个测试确认普通本机环境里,如果原生剪贴板和 OSC 52 都失败,用户能看到两边的失败原因。
数据流:它让原生假函数返回 native unavailable,让 OSC 52 假函数返回 osc blocked。调用 copy_to_clipboard_with 后,检查错误字符串同时包含这两个原因。
调用关系:它覆盖 copy_to_clipboard_with 的本机非 WSL 全失败错误汇总。
调用图:调用 1 个内部函数(copy_to_clipboard_with);外部调用 4 个(new, assert_eq!, panic!, local_environment)。
tests::local_wsl_reports_native_powershell_and_osc52_errors_when_all_fail975–1007 ↗
fn local_wsl_reports_native_powershell_and_osc52_errors_when_all_fail()
作用:这个测试确认 WSL 中三种办法都失败时,错误信息会把原生剪贴板、PowerShell、OSC 52 的失败原因都告诉用户。
数据流:它创建 WSL 环境,让原生、PowerShell、OSC 52 假函数全部失败。调用 copy_to_clipboard_with 后,检查最终错误按顺序列出三条失败原因。
调用关系:它验证 copy_to_clipboard_with 在最坏情况下仍然给出完整、可诊断的错误信息。
调用图:调用 1 个内部函数(copy_to_clipboard_with);外部调用 4 个(new, assert_eq!, panic!, local_wsl_environment)。
tui/src/terminal_palette.rs源码 ↗
不同终端的颜色能力差很多:有的能显示任意 RGB 颜色,有的只能显示 256 色,有的甚至只适合用默认颜色。这个文件就像一个“调色翻译员”:界面代码说“我想要这个红绿蓝颜色”,它先看当前终端能不能显示;如果能显示真彩色,就原样用;如果只能显示 256 色,就在一张 Xterm 256 色表里找肉眼看起来最接近的颜色;如果能力太弱,就退回默认颜色,避免花屏或难看。它还会尝试读取终端当前的默认前景色和背景色,让主题能更好地适配深色/浅色背景。这里还特别照顾了 Windows Terminal:有些情况下它报告得保守,但实际支持真彩色,所以代码会做修正。默认颜色查询会被缓存,避免每次画界面都去问终端。
stdout_color_level14–21 ↗
fn stdout_color_level() -> StdoutColorLevel
作用:判断标准输出,也就是程序往终端打印内容的那条通道,最高支持多少颜色。别人需要知道“这个终端能不能显示彩色、能显示到什么程度”时会用它。
数据流:它不接收参数,而是读取系统里缓存过的终端颜色支持信息。读到支持 1600 万色就返回 TrueColor,读到支持 256 色就返回 Ansi256,只支持基础颜色就返回 Ansi16,完全判断不出来就返回 Unknown。
调用关系:这是颜色决策的第一步。effective_stdout_color_level 会在它的结果上再结合终端名字和环境变量做修正;其他地方比如当前主题、差异颜色、表格分隔线样式,也会直接用它来决定颜色策略。
调用图:被 4 处调用(current, diff_color_level, table_separator_style, effective_stdout_color_level);外部调用 1 个(on_cached)。
rgb_color24–26 ↗
fn rgb_color((r, g, b): (u8, u8, u8)) -> Color
作用:把一个普通的红绿蓝三元组变成界面库能使用的真彩色颜色值。简单说,就是把“(红, 绿, 蓝)”包装成绘制终端界面的颜色对象。
数据流:输入是三个 0 到 255 的数字,分别表示红、绿、蓝。它不做换算,直接交给 ratatui 的 Color::Rgb,输出一个 RGB 颜色对象。
调用关系:当终端支持真彩色时,best_color_for_color_level 会调用它。很多具体样式代码,比如新增行背景、删除行背景、浅色主题 gutter 颜色,也会用它来明确指定颜色。
调用图:被 10 处调用(truecolor_palette_blends_empty_cell_for_light_background, truecolor_palette_blends_theme_accent_against_dark_background, add_line_bg, color_from_rgb_for_level, del_line_bg, light_add_num_bg, light_del_num_bg, light_gutter_fg, table_separator_style_for, best_color_for_color_level);外部调用 1 个(Rgb)。
indexed_color29–31 ↗
fn indexed_color(index: u8) -> Color
作用:把一个 0 到 255 的颜色编号变成界面库能使用的 256 色颜色值。它用于那些不能直接显示任意 RGB、但能显示编号色的终端。
数据流:输入是一个颜色编号。它把编号包装成 ratatui 的 Indexed 颜色,输出给界面绘制层使用,不改变其他状态。
调用关系:当颜色需要被压缩到 ANSI 256 色时会用到它。best_color_for_color_level 找到最接近的 Xterm 颜色编号后,就靠它生成最终颜色。
调用图:被 6 处调用(add_line_bg, del_line_bg, light_add_num_bg, light_del_num_bg, light_gutter_fg, quantize_rgb_to_ansi256);外部调用 1 个(Indexed)。
best_color34–36 ↗
fn best_color(target: (u8, u8, u8)) -> Color
作用:给定一个理想的 RGB 颜色,返回当前终端最适合显示的颜色。调用者不用关心终端能力差异,直接问它“这个颜色该怎么画”就行。
数据流:输入是目标 RGB 颜色。它先调用 effective_stdout_color_level 查出当前终端实际可用的颜色等级,再把目标颜色和等级交给 best_color_for_color_level,最后输出一个安全可用的 Color。
调用关系:这是外部样式代码最常用的入口。加载遮罩、用户消息背景、表格分隔线、强调色等地方会调用它;它自己把判断终端能力和挑选颜色的细活交给后面的函数。
调用图:调用 2 个内部函数(best_color_for_color_level, effective_stdout_color_level);被 5 处调用(dense_row_background_style, transcript_loading_overlay_style, accent_style_for, table_separator_style_for, user_message_bg)。
best_color_for_level39–41 ↗
fn best_color_for_level(target: (u8, u8, u8), color_level: StdoutColorLevel) -> Color
作用:在已经知道终端颜色能力的情况下,为目标 RGB 颜色选一个合适的显示颜色。它适合那些不想重新探测终端、已经拿到颜色等级的代码。
数据流:输入是目标 RGB 颜色和一个明确的颜色等级。它把这两样直接交给 best_color_for_color_level,输出转换后的 Color。
调用关系:它是一个方便入口,主要被构建配色对象的 from_parts 使用。真正的颜色选择逻辑仍然在 best_color_for_color_level 里。
调用图:调用 1 个内部函数(best_color_for_color_level);被 1 处调用(from_parts)。
effective_stdout_color_level43–50 ↗
fn effective_stdout_color_level() -> StdoutColorLevel
作用:算出“实际应该按什么颜色能力来用”标准输出。它不只相信系统报告,还会结合终端名字和环境变量做修正。
数据流:它读取 stdout_color_level 的基础判断,再读取 terminal_info 的终端名称,还检查 WT_SESSION 和 FORCE_COLOR 两个环境变量。然后把这些信息交给 stdout_color_level_for_terminal,得到修正后的颜色等级。
调用关系:best_color 会先调用它,确保颜色选择建立在更靠谱的终端能力判断上。它把原始探测、环境读取和特殊终端修正串在一起。
调用图:调用 2 个内部函数(stdout_color_level, stdout_color_level_for_terminal);被 1 处调用(best_color);外部调用 2 个(terminal_info, var_os)。
stdout_color_level_for_terminal52–70 ↗
fn stdout_color_level_for_terminal(
stdout_level: StdoutColorLevel,
terminal_name: TerminalName,
has_wt_session: bool,
has_force_color_override: bool,
) -> StdoutColorLevel
作用:根据终端名字和环境变量,修正标准输出报告的颜色能力。它主要是为了解决 Windows Terminal 有时明明支持真彩色却报告得太保守的问题。
数据流:输入包括原始颜色等级、终端名称、是否存在 WT_SESSION、是否存在 FORCE_COLOR。若检测到 Windows Terminal 相关信号且用户没有用 FORCE_COLOR 强行指定,就把能力提升为 TrueColor;否则保留原始等级。
调用关系:它被 effective_stdout_color_level 调用,是终端能力判断里的“纠偏”步骤。测试代码也专门验证了它对 Windows Terminal 和 FORCE_COLOR 的处理。
调用图:被 1 处调用(effective_stdout_color_level)。
best_color_for_color_level72–84 ↗
fn best_color_for_color_level(target: (u8, u8, u8), color_level: StdoutColorLevel) -> Color
作用:这是核心选色函数:它根据终端颜色等级,把理想 RGB 颜色变成终端能显示的颜色。它决定是原样用、找近似色,还是退回默认颜色。
数据流:输入是目标 RGB 和颜色等级。如果是真彩色,就输出 rgb_color 的结果;如果是 256 色,就遍历固定的 Xterm 颜色表,用感知距离,也就是更接近人眼观感的颜色差距,找最像的颜色编号并输出 indexed_color;如果只有 16 色或未知,就输出默认颜色。
调用关系:best_color 和 best_color_for_level 都会把请求交给它。它又会调用 rgb_color、xterm_fixed_colors 和 indexed_color,完成从理想颜色到可显示颜色的转换。
调用图:调用 2 个内部函数(rgb_color, xterm_fixed_colors);被 2 处调用(best_color, best_color_for_level);外部调用 1 个(default)。
requery_default_colors86–88 ↗
fn requery_default_colors()
作用:让程序重新询问终端当前的默认前景色和背景色。当前景或背景可能变化时,比如终端主题切换后,这个函数能刷新缓存。
数据流:它不接收参数,只把请求转交给平台相关的 imp::requery_default_colors。实际结果是内部默认颜色缓存可能被更新,也可能因为平台不支持而什么都不做。
调用关系:这是外层统一入口,屏蔽了 Unix、Windows 和测试环境的差别。具体怎么重新查询由 imp 模块里的对应实现决定。
调用图:外部调用 1 个(requery_default_colors)。
default_colors96–98 ↗
fn default_colors() -> Option<DefaultColors>
作用:取得终端当前默认的文字颜色和背景颜色。如果能拿到,主题就可以根据真实背景来调整;拿不到就返回空,让上层走备用方案。
数据流:它不接收参数,调用平台相关的 imp::default_colors。输出要么是包含 fg 和 bg 的 DefaultColors,要么是 None,表示终端没有回答或平台不支持。
调用关系:default_fg 和 default_bg 都通过它取颜色。它是读取默认前景/背景颜色的公共入口,具体查询和缓存由 imp 模块完成。
调用图:被 2 处调用(default_bg, default_fg);外部调用 1 个(default_colors)。
default_fg100–102 ↗
fn default_fg() -> Option<(u8, u8, u8)>
作用:只取终端默认的前景色,也就是默认文字颜色。需要让文字颜色和终端主题协调时会用它。
数据流:它调用 default_colors 拿到一组默认颜色。如果成功,就取出 fg 这个 RGB 三元组;如果失败,就返回 None。
调用关系:当前主题、闪烁加载文字、表格分隔线等样式会调用它。它把完整默认颜色对象简化成调用者只关心的文字颜色。
调用图:调用 1 个内部函数(default_colors);被 3 处调用(current, shimmer_spans, table_separator_style)。
default_bg104–106 ↗
fn default_bg() -> Option<(u8, u8, u8)>
作用:只取终端默认的背景色。很多主题判断需要知道用户终端是偏深还是偏浅,这个函数就是拿这份信息的。
数据流:它调用 default_colors 获取默认颜色。如果有结果,就取出 bg 这个 RGB 三元组;如果没有,就返回 None。
调用关系:它被主题选择、对话消息样式、页脚提示、密集行背景等很多地方使用。它是让界面自动适配终端背景的关键小入口。
调用图:调用 1 个内部函数(default_colors);被 15 处调用(current, diff_theme, adaptive_default_theme_selection, conversation_assistant_style, conversation_user_style, dense_row_background_style, footer_hint_key_style, footer_hint_label_style, selected_session_style, transcript_loading_overlay_style (+5 more))。
set_default_colors_from_startup_probe109–113 ↗
fn set_default_colors_from_startup_probe(
colors: Option<crate::terminal_probe::DefaultColors>,
)
作用:把程序启动早期探测到的默认颜色写入这里的缓存。这样后面画界面时不用再立刻去问终端,减少等待和输入冲突。
数据流:输入是启动探测得到的颜色,可能有也可能没有。它转交给平台相关实现,把颜色转换成本文件的 DefaultColors 并标记为已经尝试过查询。
调用关系:这是启动探测和调色板缓存之间的桥。启动阶段的 terminal_probe 先拿颜色,随后这个函数把结果交给 imp 模块保存。
调用图:外部调用 1 个(set_default_colors_from_startup_probe)。
imp::color_to_tuple209–214 ↗
fn color_to_tuple(color: CrosstermColor) -> Option<(u8, u8, u8)>
作用:把 crossterm 返回的颜色格式转成普通 RGB 三元组。它只接受真正的 RGB 颜色,别的颜色形式会被丢弃。
数据流:输入是 CrosstermColor。如果它是 Rgb { r, g, b },就输出 Some((r, g, b));如果是编号色、重置色等其他形式,就输出 None。
调用关系:Unix 下重新查询默认颜色时,imp::requery_default_colors 会用它把 crossterm 的前景色和背景色转换成本文件统一使用的格式。
imp::Cache::default229–234 ↗
fn default() -> Self
作用:创建一个空缓存,表示“还没尝试过查询,也还没有值”。这是默认颜色缓存一开始的状态。
数据流:它不接收外部数据,生成 attempted 为 false、value 为 None 的 Cache。也就是说,之前既没有成功结果,也没有失败记录。
调用关系:default_colors_cache 第一次建立全局缓存时会用到它。它为后续 get_or_init_with 的“只查询一次”行为打基础。
imp::Cache::get_or_init_with238–244 ↗
fn get_or_init_with(&mut self, mut init: impl FnMut() -> Option<T>) -> Option<T>
作用:从缓存里拿值;如果以前没查过,就调用传进来的查询函数查一次并记住结果。它避免反复向终端发查询请求。
数据流:输入是一个可调用的初始化函数。若 attempted 是 false,它先运行这个函数,把返回值放进 value,并把 attempted 改成 true;之后无论成功还是失败,都直接返回缓存里的 value。
调用关系:imp::default_colors 会用它来懒加载默认颜色。query_default_colors 是常见的初始化函数,真正负责去问终端。
imp::default_colors_cache247–250 ↗
fn default_colors_cache() -> &'static Mutex<Cache<DefaultColors>>
作用:提供全局唯一的默认颜色缓存,并用互斥锁保护它。互斥锁可以理解成一把锁,防止多个任务同时改同一份缓存。
数据流:它不接收参数。第一次调用时创建一个 Mutex<Cache<DefaultColors>>,之后每次都返回同一个静态缓存引用。
调用关系:imp::default_colors、imp::set_default_colors_from_startup_probe 和 imp::requery_default_colors 都通过它访问缓存。它是平台实现里共享默认颜色结果的仓库。
调用图:外部调用 1 个(new)。
imp::query_default_colors272–280 ↗
fn query_default_colors() -> Option<DefaultColors>
作用:真正去询问终端默认文字色和背景色。它把探测失败、终端不支持、没有返回结果都统一当成“拿不到”。
数据流:它调用 terminal_probe::default_colors,并带上默认超时时间。若探测成功且有颜色,就转换成本文件的 DefaultColors;如果 I/O 出错或终端没响应,就返回 None。
调用关系:它通常由 imp::Cache::get_or_init_with 在第一次需要默认颜色时调用。这样查询只发生一次,后面都走缓存。
调用图:调用 1 个内部函数(default_colors)。
imp::default_colors287–289 ↗
fn default_colors() -> Option<DefaultColors>
作用:平台内部版本的默认颜色读取函数。它负责加锁访问缓存,并在必要时触发一次真实查询。
数据流:它先拿到 default_colors_cache,再尝试锁住缓存;如果锁失败就返回 None。锁成功后,用 get_or_init_with 调用 query_default_colors,最后返回缓存中的默认颜色。
调用关系:外层 default_colors 会调用它。它把“缓存在哪、什么时候查询、查询失败怎么处理”这些细节藏在平台实现里。
调用图:外部调用 1 个(default_colors_cache)。
imp::set_default_colors_from_startup_probe292–295 ↗
fn set_default_colors_from_startup_probe(
_colors: Option<crate::terminal_probe::DefaultColors>,
)
作用:平台内部函数,用来接收启动阶段已经探测到的默认颜色,并直接填进缓存。这样后续读取会认为已经查询过。
数据流:输入是 terminal_probe 返回的可选颜色。它锁住 default_colors_cache,若有颜色就转换成 DefaultColors 存入 value;无论有没有颜色,都会把 attempted 设为 true。
调用关系:外层 set_default_colors_from_startup_probe 会调用它。它让启动探测结果和运行期调色逻辑共用同一份缓存。
调用图:外部调用 1 个(default_colors_cache)。
imp::requery_default_colors297–297 ↗
fn requery_default_colors()
作用:平台内部的重新查询默认颜色函数。Unix 版本会在合适时通过 crossterm 再问一次终端;Windows 版本目前什么都不做。
数据流:它尝试锁住默认颜色缓存。如果以前已经查过并且失败了,就直接返回,避免一直打扰终端。否则它查询前景色和背景色,把 RGB 结果转成元组,两个都拿到时更新缓存,并标记已经尝试过。
调用关系:外层 requery_default_colors 会调用它。Unix 下它会使用 query_foreground_color、query_background_color 和 color_to_tuple;这样焦点事件后可以刷新颜色,同时避免启动探测那种直接读终端的方式误吃掉用户输入。
调用图:外部调用 3 个(query_background_color, query_foreground_color, default_colors_cache)。
xterm_fixed_colors301–303 ↗
fn xterm_fixed_colors() -> impl Iterator<Item = (usize, (u8, u8, u8))>
作用:提供 Xterm 256 色表中比较稳定的那一段颜色。它特意跳过前 16 个颜色,因为这些颜色经常会被用户终端主题改掉。
数据流:它不接收参数,从 XTERM_COLORS 常量表生成带编号的迭代器,并从编号 16 开始输出。每一项都是颜色编号和对应 RGB 值。
调用关系:best_color_for_color_level 在处理 ANSI 256 色时会调用它,用这些稳定颜色来找最接近目标 RGB 的编号。
调用图:被 1 处调用(best_color_for_color_level)。
tests::best_color_uses_truecolor_without_quantization574–579 ↗
fn best_color_uses_truecolor_without_quantization()
作用:测试真彩色终端不会把颜色压缩成近似色。也就是说,支持 RGB 时应该原样显示用户想要的颜色。
数据流:它输入一个示例颜色 (12, 34, 56) 和 TrueColor 等级,调用 best_color_for_color_level,然后断言结果等于 rgb_color 生成的同一个 RGB 颜色。
调用关系:这是 best_color_for_color_level 的单元测试之一。它保证真彩色情况不会误走 256 色近似逻辑。
调用图:外部调用 1 个(assert_eq!)。
tests::best_color_resets_for_ansi16582–587 ↗
fn best_color_resets_for_ansi16()
作用:测试只有 16 色能力时,代码会退回默认颜色,而不是硬选一个可能很难看的颜色。
数据流:它输入示例 RGB 颜色和 Ansi16 等级,调用 best_color_for_color_level,然后断言结果是 Color::Reset,也就是使用终端默认颜色。
调用关系:这是 best_color_for_color_level 的保护性测试。它确认低颜色能力终端走安全 fallback,不做不可靠的颜色匹配。
调用图:外部调用 1 个(assert_eq!)。
tests::windows_terminal_wt_session_promotes_to_truecolor590–600 ↗
fn windows_terminal_wt_session_promotes_to_truecolor()
作用:测试检测到 WT_SESSION 时,如果用户没有强制覆盖颜色设置,就把颜色能力提升为真彩色。WT_SESSION 是 Windows Terminal 常见的环境标记。
数据流:它传入原始 Ansi16、未知终端名、has_wt_session 为 true、has_force_color_override 为 false,然后断言 stdout_color_level_for_terminal 返回 TrueColor。
调用关系:这是 stdout_color_level_for_terminal 的行为测试。它保证 Windows Terminal 的环境变量线索能修正过低的颜色报告。
调用图:外部调用 1 个(assert_eq!)。
tests::windows_terminal_name_promotes_ansi16_to_truecolor603–613 ↗
fn windows_terminal_name_promotes_ansi16_to_truecolor()
作用:测试如果终端名称明确是 Windows Terminal,并且没有 FORCE_COLOR 覆盖,就把 Ansi16 提升为 TrueColor。
数据流:它传入原始 Ansi16、TerminalName::WindowsTerminal、没有 WT_SESSION、没有 FORCE_COLOR,然后断言修正结果是 TrueColor。
调用关系:这是 stdout_color_level_for_terminal 的另一个 Windows Terminal 测试。它保证即使没有 WT_SESSION,只要终端名可靠,也能启用真彩色。
调用图:外部调用 1 个(assert_eq!)。
tests::force_color_keeps_reported_stdout_level616–626 ↗
fn force_color_keeps_reported_stdout_level()
作用:测试用户设置 FORCE_COLOR 时,代码尊重用户或外部环境给出的颜色能力,不再自动把 Windows Terminal 提升为真彩色。
数据流:它传入原始 Ansi16、Windows Terminal 名称、WT_SESSION 为 true、FORCE_COLOR 为 true,然后断言结果仍然是 Ansi16。
调用关系:这是 stdout_color_level_for_terminal 的覆盖规则测试。它确保自动修正不会压过用户明确指定的颜色策略。
调用图:外部调用 1 个(assert_eq!)。
沙箱诊断和 Windows 支持
这些工具提供沙箱专用诊断,以及为沙箱执行准备路径、环境、权限和敏感依赖所需的 Windows 辅助工具。
cli/src/debug_sandbox/seatbelt.rs源码 ↗
macOS 的沙盒机制也叫 Seatbelt,可以限制程序做某些事。这个文件做的事像是在门口放一个记录员:先启动系统自带的 log stream 命令,专门监听沙盒拒绝记录;当真正要观察的子进程启动后,记下它和它的后代进程编号;最后收工时,把刚才听到的日志筛一遍,只留下属于这些进程的“deny”记录。这里的 DenialLogger 是主零件,它一边让后台任务持续读取日志,一边等外部告诉它子进程已经启动。结束时它会停掉日志流,把每行 JSON 日志里的 eventMessage 拿出来解析,再去重,输出一组 SandboxDenial,也就是“哪个进程名被拒绝了哪项能力”。这样调试沙盒问题时,报告会更清楚,而不是只给一个模糊的失败。
DenialLogger::new20–44 ↗
fn new() -> Option<Self>
作用:创建一个新的沙盒拒绝日志记录器。它会尝试打开 macOS 系统日志的实时输出,并启动一个后台任务,把日志先攒起来,等之后统一分析。
数据流:进去时不需要外部参数 → 它调用 start_log_stream 启动系统的 log stream 命令,拿到这个命令的标准输出,然后用异步任务一行一行读取日志字节 → 成功时出来一个 DenialLogger,里面保存着日志进程和读取任务;如果日志流打不开,出来的是 None,表示不能记录。
调用关系:这是整个记录流程的起点。它把真正启动系统日志命令的工作交给 start_log_stream,又用异步 spawn 开一个后台读日志的小工;后面外层代码会在子进程启动时调用 DenialLogger::on_child_spawn,结束时调用 DenialLogger::finish。
调用图:调用 1 个内部函数(start_log_stream);外部调用 3 个(new, new, spawn)。
DenialLogger::on_child_spawn46–50 ↗
fn on_child_spawn(&mut self, child: &Child)
作用:告诉日志记录器:要观察的子进程已经启动了。它会记住这个进程的编号,之后只统计这个进程以及它派生出来的子进程产生的沙盒拒绝。
数据流:进去的是刚启动的子进程对象 → 它从子进程里取出进程号,也就是操作系统给进程的身份证号码;如果取到了,就创建一个 PidTracker 来追踪这棵进程树 → 之后 DenialLogger 内部多了一份进程追踪信息,供收尾分析时过滤日志。
调用关系:它通常在外部真正启动被沙盒包住的命令之后调用。它不解析日志,只是为 DenialLogger::finish 准备“哪些进程算我们关心的进程”这张名单。
DenialLogger::finish52–84 ↗
async fn finish(mut self) -> Vec<SandboxDenial>
作用:结束日志记录,并把听到的系统日志整理成清楚的沙盒拒绝列表。它是把原始日志变成可读诊断结果的收尾步骤。
数据流:进去的是已经运行过一段时间的 DenialLogger → 它先停止进程追踪,拿到相关进程号集合;如果没有进程号,就直接返回空列表;否则它杀掉并等待日志流进程结束,取回后台任务攒下的日志字节,把字节转成文字,再逐行读取 JSON 日志;每行取出 eventMessage 后交给 parse_message 解析,并只保留进程号在名单里的记录;同一个“进程名 + 被拒绝能力”只保留一次 → 出来的是一组 SandboxDenial,也就是去重后的拒绝记录。
调用关系:这是流程的终点。它接收 DenialLogger::new 开始收集的日志,也使用 DenialLogger::on_child_spawn 准备好的进程范围;真正拆解单条沙盒消息的细活交给 parse_message。
调用图:调用 1 个内部函数(parse_message);外部调用 6 个(kill, wait, default, new, from_utf8_lossy, new)。
start_log_stream87–100 ↗
fn start_log_stream() -> Option<Child>
作用:启动 macOS 的系统日志监听命令,只听跟沙盒拒绝有关的日志。它把外部的 log stream 命令包装成程序里可以读取的子进程。
数据流:进去时没有参数 → 它构造 log stream --style ndjson --predicate ... 命令,其中 ndjson 是“一行一个 JSON”的日志格式,predicate 是过滤条件,只让沙盒相关日志通过;它关闭标准输入和错误输出,把标准输出接成管道,并设置对象丢弃时自动杀掉进程 → 成功时出来一个正在运行的子进程,失败时出来 None。
调用关系:DenialLogger::new 会调用它来打开日志水龙头。它只负责把日志流接进来,不判断哪些日志属于目标程序,也不解析沙盒消息。
parse_message102–114 ↗
fn parse_message(msg: &str) -> Option<(i32, String, String)>
作用:把一条沙盒日志里的文字消息拆成三部分:进程号、进程名、被拒绝的能力。没有符合预期格式时,它会返回空,表示这条消息不是它能识别的拒绝记录。
数据流:进去的是一段类似 Sandbox: processname(1234) deny(1) capability-name args... 的文字 → 它用正则表达式(一种按模板匹配文字的工具)找出进程名、括号里的进程号、以及 deny 后面的能力描述,再把进程号从文字转成数字 → 成功时出来 (进程号, 进程名, 能力描述),失败时出来 None。
调用关系:DenialLogger::finish 在逐行检查系统日志时会调用它。它像一个小翻译,只翻译单条 eventMessage,至于这条消息要不要保留、是否重复,则由 finish 决定。
windows-sandbox-rs/src/bin/command_runner/win/cwd_junction.rs源码 ↗
在 Windows 里,有些目录路径可能很长、带空格,或者不适合直接交给沙箱命令使用。这个文件的做法是:先在用户目录下面准备一个固定的隐藏位置,比如“.codex/.sandbox/cwd”,然后根据真实工作目录算出一个短名字,在那里创建一个 junction。junction 可以理解成“门牌号在这里,但打开后通向另一个真实目录”的目录入口。文件会尽量复用已经存在的 junction,避免每次都重新创建;如果发现同名位置不是 junction,就尝试删掉重建。真正创建时,它调用 Windows 的 cmd /c mklink /J 命令,并特别小心处理带空格的路径。整个过程都会写日志,失败时不硬撑,而是返回空结果,让上层决定怎么办。
junction_name_for_path11–15 ↗
fn junction_name_for_path(path: &Path) -> String
作用:这个函数把一个真实目录路径变成一个短的、可当文件夹名用的名字。它这样做是为了在统一的 junction 存放目录里,为不同工作目录生成不同入口,避免直接把完整路径塞进文件名里。
数据流:进去的是一个路径;函数先把路径转成字符串,再用哈希(一种把内容压缩成固定数字的算法)算出一个数字,最后把这个数字写成十六进制字符串。出来的是一个短名字,之后会被当作 junction 目录名使用;它不改动磁盘。
调用关系:它是 create_cwd_junction 的小帮手。create_cwd_junction 需要决定 junction 放在哪里、叫什么名时,会调用它来给 requested_cwd 生成唯一感较强的目录名。
调用图:被 1 处调用(create_cwd_junction);外部调用 3 个(new, to_string_lossy, format!)。
junction_root_for_userprofile17–22 ↗
fn junction_root_for_userprofile(userprofile: &str) -> PathBuf
作用:这个函数根据 Windows 用户目录,拼出专门存放这些工作目录 junction 的根目录。它让所有临时入口都集中放在一个固定位置,方便复用和查找。
数据流:进去的是 USERPROFILE 这个用户目录字符串;函数把它变成路径,然后依次接上“.codex”、“.sandbox”、“cwd”。出来的是类似“用户目录/.codex/.sandbox/cwd”的路径;它只拼路径,不创建文件夹。
调用关系:它由 create_cwd_junction 调用。create_cwd_junction 先拿到 USERPROFILE,再靠它算出 junction 的总仓库位置,随后才会创建目录和具体 junction。
调用图:被 1 处调用(create_cwd_junction);外部调用 1 个(from)。
create_cwd_junction24–140 ↗
fn create_cwd_junction(requested_cwd: &Path, log_dir: Option<&Path>) -> Option<PathBuf>
作用:这个函数为一个请求的工作目录创建或复用 Windows junction,并把可用的 junction 路径交给上层。有人会用它,是因为后续运行命令时需要一个更受控、更稳定的工作目录入口。
数据流:进去的是 requested_cwd,也就是命令想要使用的真实工作目录,以及可选的日志目录 log_dir。函数先读取 USERPROFILE,算出 junction 根目录并确保它存在;再用 requested_cwd 算出具体 junction 名。如果这个位置已经是 junction,就直接返回它;如果那里是别的东西,就尝试删掉。接着它把链接路径和目标路径加上引号,调用 cmd /c mklink /J 创建 junction。成功时出来的是 junction 路径;失败时会写日志并返回 None。它会改动磁盘,因为可能创建目录、删除旧目录、创建 junction。
调用关系:它是这个文件的主流程函数,由外面的 effective_cwd 在准备有效工作目录时调用。它自己把小活分给 junction_root_for_userprofile 和 junction_name_for_path,并在关键步骤通过 log_note 记录情况;真正创建 junction 的活交给 Windows 的 cmd 和 mklink 完成。
调用图:调用 2 个内部函数(junction_name_for_path, junction_root_for_userprofile);被 1 处调用(effective_cwd);外部调用 9 个(to_string_lossy, from_utf8_lossy, new, log_note, format!, var, create_dir_all, remove_dir, symlink_metadata)。
windows-sandbox-rs/src/bin/setup_main/win/read_acl_mutex.rs源码 ↗
这个文件解决的是“同一件系统设置不要被多个进程同时做”的问题。这里的 ACL 是访问控制列表,简单说就是 Windows 用来判断“谁能读、谁能改”的权限表。读取或准备这类权限信息时,如果两个流程同时动手,可能会重复执行,甚至互相打架。所以代码创建了一个固定名字的 Windows 互斥锁:Local\CodexSandboxReadAcl。read_acl_mutex_exists 用来问系统“这把锁现在有没有存在”。acquire_read_acl_mutex 用来尝试创建并占有这把锁:如果锁已经存在,就返回 None,表示别人已经在做;如果成功拿到,就返回一个 ReadAclMutexGuard。这个 guard 像一张临时通行证,活着时代表锁还被占着;一旦它被丢弃,Drop 会自动释放锁并关闭系统句柄,避免忘记收尾造成资源泄漏。
ReadAclMutexGuard::drop21–26 ↗
fn drop(&mut self)
作用:这是 ReadAclMutexGuard 被销毁时自动执行的收尾动作。它会释放 Windows 互斥锁,并关闭对应的系统句柄,防止这把“锁”一直占着不放。
数据流:进去的是 guard 里保存的 Windows 句柄 → 它先调用 ReleaseMutex 把互斥锁放开,再调用 CloseHandle 告诉系统这个句柄不用了 → 出来没有返回值,但系统里的锁状态和资源占用被正确清理了。
调用关系:它不是被普通代码手动调用的,而是 Rust 在 ReadAclMutexGuard 生命周期结束时自动调用。acquire_read_acl_mutex 成功后会产出这个 guard,等调用方不再需要它时,drop 就负责把前面拿到的系统锁还回去。
调用图:外部调用 2 个(CloseHandle, ReleaseMutex)。
read_acl_mutex_exists29–43 ↗
fn read_acl_mutex_exists() -> Result<bool>
作用:这个函数用来检查那把固定名字的读取 ACL 互斥锁是否已经存在。调用者可以用它判断“是不是已经有别的流程在做相关准备”。
数据流:进去没有业务参数,它读取文件里写死的锁名 Local\CodexSandboxReadAcl → 先把这个名字转换成 Windows API 需要的宽字符格式,再调用 OpenMutexW 尝试打开这把锁 → 如果系统说找不到,就返回 false;如果打开成功,就马上关闭句柄并返回 true;如果遇到其他 Windows 错误,就返回错误信息。
调用关系:它会被 run_setup_full 在完整 setup 流程里使用,用来提前观察这把锁是否存在。它自己不创建锁,只是问系统有没有;真正尝试拿锁的动作交给 acquire_read_acl_mutex。
调用图:被 1 处调用(run_setup_full);外部调用 6 个(new, anyhow!, to_wide, CloseHandle, GetLastError, OpenMutexW)。
acquire_read_acl_mutex45–61 ↗
fn acquire_read_acl_mutex() -> Result<Option<ReadAclMutexGuard>>
作用:这个函数用来尝试拿到读取 ACL 这件事的独占执行权。拿到了就允许当前流程继续做;如果发现别人已经拿了,就告诉调用者不要重复做。
数据流:进去没有业务参数,它使用固定锁名 Local\CodexSandboxReadAcl → 把名字转成 Windows 需要的格式,然后调用 CreateMutexW 创建并立刻占有这把锁 → 如果创建失败,就返回错误;如果系统提示锁已经存在,就关闭刚拿到的句柄并返回 None;如果是新创建成功,就返回 Some(ReadAclMutexGuard),让调用者持有这把锁直到工作结束。
调用关系:它会被 run_read_acl_only 在只执行读取 ACL 的流程里调用。它把底层 Windows 的 CreateMutexW、GetLastError、CloseHandle 这些细节包起来,让上层只需要看结果:有 guard 就继续干活,None 就说明别人已经在干。
调用图:被 1 处调用(run_read_acl_only);外部调用 7 个(new, anyhow!, to_wide, null_mut, CloseHandle, GetLastError, CreateMutexW)。
windows-sandbox-rs/src/env.rs源码 ↗
程序启动另一个命令时,会带上一包环境变量。它们像一张“运行说明纸”,告诉子程序去哪里找命令、用什么代理、分页器怎么显示等。这个文件就是在发出这张纸之前,把容易出问题的地方先修好:把类 Unix 的空设备写法改成 Windows 的 NUL;给 Git 或命令行分页器设置成不会卡住交互的模式;补上 PATH 和 PATHEXT,让 Windows 能找到可执行文件。它还提供“禁网”设置:把代理指到本机一个不会通的端口,告诉 pip、npm、cargo 等工具离线运行,并做一组假的 ssh/scp 命令,放到 PATH 最前面,让这些联网命令一被调用就失败。可以把它想成进沙盒前的“安检台”:该带的通行证补上,不该出去的出口堵住。
normalize_null_device_env12–22 ↗
fn normalize_null_device_env(env_map: &mut HashMap<String, String>)
作用:这个函数把环境变量里写成 /dev/null 或类似写法的空设备,改成 Windows 能认的 NUL。这样从 Unix/Linux 习惯迁移来的配置,在 Windows 沙盒里不至于指向一个不存在的地方。
数据流:进去的是一张可修改的环境变量表。它逐个查看里面的值,把值先去掉前后空格、转成小写来比较;如果发现某个值等于 /dev/null 或 Windows 反斜杠形式的 dev/null,就把这个变量的值改成 NUL。出来时还是同一张表,只是相关项被修正了。
调用关系:它会在准备普通启动上下文、按权限配置运行 Windows 沙盒捕获输出、以及准备提权启动上下文时被调用。也就是说,在真正启动沙盒命令前,外层流程会先请它把“空设备”这种跨系统差异处理掉。
调用图:被 3 处调用(run_windows_sandbox_capture_for_permission_profile, prepare_elevated_spawn_context_for_permissions, prepare_spawn_context_common)。
ensure_non_interactive_pager24–32 ↗
fn ensure_non_interactive_pager(env_map: &mut HashMap<String, String>)
作用:这个函数给分页器相关环境变量设置安全默认值,避免命令输出时弹出需要人操作的分页界面。分页器可以理解成“把长输出一页页显示的工具”,但在自动化运行里它可能让程序卡住。
数据流:进去的是环境变量表。它检查 GIT_PAGER、PAGER、LESS 这些变量:如果调用者已经设置了,就尊重原值;如果没设置,就把 Git 和普通分页器设成 Windows 的 more.com,并把 LESS 设为空。出来时环境变量表多了这些默认值,自动运行时更不容易等待人工按键。
调用关系:它和 normalize_null_device_env 一样,在沙盒命令启动前的准备阶段被多个流程调用,包括普通准备、提权准备和按权限配置运行。它不启动程序,只是提前把会影响输出显示的环境整理好。
调用图:被 3 处调用(run_windows_sandbox_capture_for_permission_profile, prepare_elevated_spawn_context_for_permissions, prepare_spawn_context_common)。
inherit_path_env35–46 ↗
fn inherit_path_env(env_map: &mut HashMap<String, String>)
作用:这个函数保证子程序能继承父进程的 PATH 和 PATHEXT。PATH 是“去哪几个文件夹找命令”,PATHEXT 是 Windows 上“哪些扩展名算可执行命令”。
数据流:进去的是环境变量表。它先看表里有没有 PATH,没有的话就从当前进程读取 PATH 并填进去;PATHEXT 也是同样处理。出来时,如果系统能读到这些值,表里就会有它们;如果原来已有,则不会覆盖。
调用关系:准备沙盒启动环境的几个上层流程会调用它,因为很多工具依赖 PATH 才能被找到。它自己只向操作系统读取当前进程环境变量,不再把任务交给项目里的其他函数。
调用图:被 3 处调用(run_windows_sandbox_capture_for_permission_profile, prepare_elevated_spawn_context_for_permissions, prepare_spawn_context_common);外部调用 1 个(var)。
prepend_path48–69 ↗
fn prepend_path(env_map: &mut HashMap<String, String>, prefix: &str)
作用:这个函数把一个目录放到 PATH 的最前面。放在最前面的意思是:系统找命令时会先看这个目录。
数据流:进去的是环境变量表和一个目录路径字符串。它读取当前表里的 PATH;如果表里没有,就读取父进程的 PATH;如果都没有,就当作空。然后它检查这个目录是否已经在 PATH 最前面,如果不是,就把它加到最前面,并用分号接上原来的 PATH。出来时,环境变量表里的 PATH 会优先指向这个目录。
调用关系:它只被 apply_no_network_to_env 调用。禁网流程会先准备一批“假命令”所在目录,再用它把这个目录插到 PATH 前面,好让 ssh、scp 这类命令优先命中假的失败脚本。
调用图:被 1 处调用(apply_no_network_to_env);外部调用 1 个(new)。
reorder_pathext_for_stubs71–103 ↗
fn reorder_pathext_for_stubs(env_map: &mut HashMap<String, String>)
作用:这个函数调整 PATHEXT 的顺序,让 .bat 和 .cmd 这类脚本扩展名排在更前面。这样 Windows 找命令时,会更早找到这个文件准备的假脚本。
数据流:进去的是环境变量表。它读取 PATHEXT;如果没有,就从父进程读取;再没有就使用常见默认值。然后它把扩展名拆开,找到 .BAT 和 .CMD,保留原有大小写但移动到最前面,其他扩展名跟在后面。出来时,环境变量表里的 PATHEXT 被改成新的顺序。
调用关系:它由 apply_no_network_to_env 调用,配合 prepend_path 使用。前者保证假脚本目录先被搜索,这个函数保证 .bat/.cmd 假脚本在 Windows 的扩展名匹配里也更容易先被选中。
调用图:被 1 处调用(apply_no_network_to_env);外部调用 1 个(new)。
ensure_denybin105–124 ↗
fn ensure_denybin(tools: &[&str], denybin_dir: Option<&Path>) -> Result<PathBuf>
作用:这个函数创建一个“拒绝执行命令”的小工具目录,并为指定工具生成会立刻失败的 .bat 和 .cmd 脚本。可以把它理解成放了一排假按钮,谁按都只会返回失败。
数据流:进去的是工具名列表,比如 ssh、scp,以及一个可选目录。如果给了目录就用它;如果没给,就用用户主目录下的 .sbx-denybin。它会确保目录存在,然后为每个工具生成 tool.bat 和 tool.cmd;如果文件已经存在就不动,没有才写入内容。脚本内容很简单:关闭回显,然后用失败状态退出。出来的是这个拒绝命令目录的路径;如果找不到主目录或创建文件失败,就返回错误。
调用关系:它只被 apply_no_network_to_env 调用,是禁网机制里的基础设施步骤。apply_no_network_to_env 先让它准备好 ssh、scp 的失败脚本,后面再把这个目录放到 PATH 前面,让这些脚本真正生效。
调用图:被 1 处调用(apply_no_network_to_env);外部调用 4 个(create, home_dir, format!, create_dir_all)。
apply_no_network_to_env126–177 ↗
fn apply_no_network_to_env(env_map: &mut HashMap<String, String>) -> Result<()>
作用:这个函数把一张环境变量表改造成“尽量不能联网”的状态。它主要用于沙盒需要离线运行时,让常见工具即使被调用也很难连出去。
数据流:进去的是可修改的环境变量表。它先写入 SBX_NONET_ACTIVE=1,表示禁网已启用;再为 HTTP、HTTPS、ALL_PROXY 等代理变量设置默认值,指向 127.0.0.1:9 这种通常不会通的地址;同时告诉 pip、npm、cargo 等包管理工具离线工作,禁止 Git 走 SSH 或任意协议。接着它创建 ssh、scp 的失败脚本目录,删除可能存在的 curl、wget 假脚本,把该目录放到 PATH 最前面,并调整 PATHEXT 顺序。出来时环境变量表已经包含一整套禁网设置;如果创建目录或文件失败,会返回错误。
调用关系:它被 prepare_legacy_spawn_context 调用,属于老式启动上下文准备流程中的禁网开关。它自己把具体工作分给 ensure_denybin、prepend_path 和 reorder_pathext_for_stubs:一个造失败脚本,一个让系统先找到这些脚本,一个调整扩展名搜索顺序。
调用图:调用 3 个内部函数(ensure_denybin, prepend_path, reorder_pathext_for_stubs);被 1 处调用(prepare_legacy_spawn_context);外部调用 2 个(format!, remove_file)。
windows-sandbox-rs/src/path_normalization.rs源码 ↗
这个文件像是给路径做“身份证号码”的小工具。Windows 路径很容易有不同写法,比如 C:\Users\Dev 和 c:/users/dev 看起来不同,但可能是同一个位置。如果程序要判断某个目录能不能读、能不能写,直接拿原始字符串比较就会出错。这里先尽量把路径转成系统认可的标准路径;如果转不了,比如路径还不存在,就保留原样,避免程序直接崩掉。然后再把反斜杠统一成正斜杠,并把大写变小写,得到一个适合拿来比较、当作“键”的字符串。这样权限检查、审计、沙箱规则就能用同一套口径看路径,少出漏判和误判。
canonicalize_path4–6 ↗
fn canonicalize_path(path: &Path) -> PathBuf
作用:这个函数尽量把一个路径变成系统标准路径。它的用处是减少 ..、符号链接、不同写法等带来的混乱;如果标准化失败,它不会报错中断,而是把原路径还回去。
数据流:进去的是一个路径。它先交给 dunce::canonicalize 去做标准化,也就是让操作系统帮忙确认这个路径的规范写法;如果成功,出来的是规范后的 PathBuf;如果失败,比如路径不存在或无法访问,出来的是原路径复制出来的新值,不改动外部数据。
调用关系:它是更底层的路径整理工具。canonical_path_key 会用它来继续生成可比较的字符串;权限和沙箱相关的代码,比如检查工作区写入根目录、生成拒绝读取规则、套用旧会话访问规则、判断命令当前目录是不是根目录时,也会先找它把路径口径统一。
调用图:被 6 处调用(workspace_write_root_contains_path, workspace_write_root_specificity, plan_deny_read_acl_paths, canonical_path_key, apply_legacy_session_acl_rules, is_command_cwd_root);外部调用 1 个(canonicalize)。
canonical_path_key8–13 ↗
fn canonical_path_key(path: &Path) -> String
作用:这个函数把路径变成一个稳定的比较用字符串。它主要解决 Windows 上同一路径可能大小写不同、斜杠方向不同的问题。
数据流:进去的是一个路径。它先调用 canonicalize_path 尽量拿到标准路径,再把路径转成字符串;遇到不能正常表示的字符时,用一种安全的替代方式转出来。然后它把 \ 换成 /,再把所有字母变成小写。最后出来的是一个普通字符串,适合用来做查找、去重、比较。
调用关系:它站在路径比较流程的中间层:先借助 canonicalize_path 打底,再产出统一格式的“路径钥匙”。审计可写目录、根据当前目录生成权限标识、为写入根目录生成权限标识、展开用户目录、过滤敏感写入根目录、识别用户目录排除项等地方,会用它来避免因为路径写法不同而判断错。
调用图:调用 1 个内部函数(canonicalize_path);被 9 处调用(audit_everyone_writable, workspace_cap_sid_for_cwd, workspace_write_cap_sid_for_root, writable_root_cap_sid_for_path, expand_user_profile_root_for, filter_sensitive_write_roots, filter_user_profile_root, is_user_profile_root_exclusion, user_profile_child_name)。
tests::canonical_path_key_normalizes_case_and_separators22–30 ↗
fn canonical_path_key_normalizes_case_and_separators()
作用:这是一个测试,确认路径大小写不同、斜杠不同的时候,最后得到的比较键仍然一样。它防止以后有人改代码时不小心破坏这个关键规则。
数据流:进去的是测试里临时写出的两个路径:一个像 C:\Users\Dev\Repo,一个像 c:/users/dev/repo。测试分别把它们交给 canonical_path_key,再比较两个结果。出来的结果不是业务数据,而是测试通过或失败;如果两个键不同,测试就会报错。
调用关系:它只在运行测试时活跃,不参与正常程序运行。它用 Path::new 造出两种写法的路径,再用断言检查 canonical_path_key 的承诺是否成立,等于给路径统一规则装了一个小报警器。
调用图:外部调用 2 个(new, assert_eq!)。
windows-sandbox-rs/src/sandbox_utils.rs源码 ↗
Windows 沙箱里跑命令时,用户身份可能和真实机器上的主用户不一样。这样一来,两个常见问题会冒出来:一是 Codex 需要的目录可能还不存在;二是 Git 会觉得“这个仓库不是当前用户拥有的,不安全”,于是拒绝执行。这个文件就是专门补这些坑的。它会先按需创建 Codex 的 home 目录;还会从当前工作目录一路往上找,看看附近有没有 Git 仓库根目录,也就是带有 .git 的目录。找到后,它把这个目录塞进 Git 的 safe.directory 配置里。safe.directory 可以理解成“Git 的白名单”,告诉 Git:这个目录虽然不是当前沙箱用户拥有的,但可以信任。文件底部的测试用临时目录造出普通 Git 仓库和 worktree 形式的仓库,确认注入进去的环境变量正好是 Git 需要的格式。
find_git_worktree_root_for_safe_directory13–25 ↗
fn find_git_worktree_root_for_safe_directory(start: &Path) -> Option<std::path::PathBuf>
作用:这个函数从一个目录开始,往父目录一层层找 Git 仓库的根目录。它的作用是找出应该加入 Git 安全白名单的那个目录。
数据流:进去的是一个起始路径。它先把路径规范化,避免相对路径、符号链接这类路径写法带来误判;然后一路向上检查每一层有没有 .git。找到就返回这个目录;如果一路走到磁盘根目录还没找到,就返回空,表示这里不在 Git 仓库里。
调用关系:它是 inject_git_safe_directory 背后的“找仓库”步骤。inject_git_safe_directory 需要先知道仓库根目录在哪里,才能把正确路径写进 Git 配置环境变量。
调用图:被 1 处调用(inject_git_safe_directory);外部调用 1 个(canonicalize)。
ensure_codex_home_exists28–31 ↗
fn ensure_codex_home_exists(p: &Path) -> Result<()>
作用:这个函数确保沙箱里给 Codex 使用的 home 目录已经存在。没有这个目录,后面的配置文件、缓存或临时数据可能没地方放。
数据流:进去的是一个目录路径。它调用系统的创建目录功能,递归创建这个目录以及缺失的父目录;如果成功,就返回成功;如果磁盘权限、路径等出错,就把错误传出去。
调用关系:它会在多个沙箱启动准备流程里被调用,包括普通沙箱路径、提权路径、捕获输出路径和旧版预检查路径。也就是说,只要准备启动沙箱命令,很多路线都会先经过它来保证基础目录到位。
调用图:被 4 处调用(run_windows_sandbox_capture_for_permission_profile, prepare_elevated_spawn_context_for_permissions, prepare_spawn_context_common, run_windows_sandbox_legacy_preflight);外部调用 1 个(create_dir_all)。
inject_git_safe_directory36–51 ↗
fn inject_git_safe_directory(env_map: &mut HashMap<String, String>, cwd: &Path)
作用:这个函数在环境变量里临时加入 Git 的 safe.directory 配置,让沙箱用户可以操作当前所在的 Git 仓库。它避免 Git 因为“仓库属于另一个用户”而拒绝工作。
数据流:进去的是一张环境变量表和当前工作目录。它先调用 find_git_worktree_root_for_safe_directory 找仓库根目录;如果没找到,就什么也不改。如果找到了,它读取已有的 GIT_CONFIG_COUNT,算出下一个配置编号,然后写入 GIT_CONFIG_KEY_编号=safe.directory 和 GIT_CONFIG_VALUE_编号=仓库路径,最后更新 GIT_CONFIG_COUNT。结果是这张环境变量表被原地改好,后续启动的 git 命令会读到这条临时配置。
调用关系:它是沙箱启动前准备环境变量的一步。捕获输出流程、提权启动流程和通用启动准备都会调用它;测试函数也直接调用它,检查普通 .git 目录和 .git 文件这两种仓库形式都能正确处理。
调用图:调用 1 个内部函数(find_git_worktree_root_for_safe_directory);被 5 处调用(run_windows_sandbox_capture_for_permission_profile, injects_safe_directory_for_git_directory, injects_worktree_root_for_gitfile, prepare_elevated_spawn_context_for_permissions, prepare_spawn_context_common);外部调用 1 个(format!)。
tests::safe_directory_value62–67 ↗
fn safe_directory_value(path: &Path) -> String
作用:这是测试里用的小帮手,用来算出测试期望看到的 safe.directory 路径字符串。它保证测试里比较的路径格式和正式代码一致。
数据流:进去的是一个路径。它把路径规范化成系统认可的绝对路径,再把路径转成字符串,并把反斜杠形式整理成 Git 配置里要用的样子;最后返回这个字符串。
调用关系:它只在测试中使用,为 tests::injects_safe_directory_for_git_directory 和 tests::injects_worktree_root_for_gitfile 准备期望值。这样测试不是手写路径字符串,而是用同一套路径规范来比较。
调用图:外部调用 1 个(canonicalize)。
tests::injects_safe_directory_for_git_directory70–89 ↗
fn injects_safe_directory_for_git_directory()
作用:这个测试确认:当当前目录位于一个普通 Git 仓库里面时,inject_git_safe_directory 会把仓库根目录加入 Git 的安全白名单环境变量。
数据流:测试先创建一个临时目录,里面造出 repo/.git 和 repo/nested。然后把 nested 当作当前工作目录,传给 inject_git_safe_directory。函数执行后,测试把得到的环境变量表和预期表比较:里面应该有一条 safe.directory,值应该是 repo 的路径。
调用关系:它直接验证 inject_git_safe_directory 的主要场景。它也间接验证 find_git_worktree_root_for_safe_directory 能从子目录往上找到带 .git 的仓库根目录。
调用图:调用 1 个内部函数(inject_git_safe_directory);外部调用 6 个(from, new, new, assert_eq!, create_dir_all, safe_directory_value)。
tests::injects_worktree_root_for_gitfile92–115 ↗
fn injects_worktree_root_for_gitfile()
作用:这个测试确认:如果 .git 不是目录而是一个文件,inject_git_safe_directory 也能把仓库根目录识别出来。Git worktree 常常会用这种形式。
数据流:测试创建临时 repo 和 nested 目录,然后在 repo 下写一个名为 .git 的文件,内容模拟 Git worktree 的指向信息。接着它调用 inject_git_safe_directory,并检查环境变量表里写入的是 repo 这个根目录,而不是 nested,也不是 .git 文件里指向的内部路径。
调用关系:它覆盖了 Git 仓库的另一种常见形态。它调用 inject_git_safe_directory,并用 tests::safe_directory_value 生成期望路径,确保沙箱工具对普通仓库和 worktree 仓库都能可靠工作。
调用图:调用 1 个内部函数(inject_git_safe_directory);外部调用 7 个(from, new, new, assert_eq!, create_dir_all, write, safe_directory_value)。
windows-sandbox-rs/src/ssh_config_dependencies.rs源码 ↗
SSH 的配置不只是一份 ~/.ssh/config。它里面可能写了 Include,把别的配置文件也拉进来;还可能写 IdentityFile、CertificateFile 这类路径,指向私钥、证书或主机记录文件。这个文件做的事,就是从用户目录出发,先找到 .ssh/config,然后一行行读配置,把相关路径都收集出来。它会识别 ~、%d、${HOME} 这些代表“用户主目录”的写法,也会处理引号、注释、反斜杠转义这些常见格式。遇到 Include 时,它还会按通配符展开,比如 conf.d/*.conf,并继续读被包含的文件。为了避免配置互相包含造成死循环,它会记住已经读过的文件,并且最多递归 32 层。可以把它想成一个搬家清单生成器:不是搬整个家,而是读说明书,把 SSH 真正会用到的东西列出来。
ssh_config_dependency_paths15–27 ↗
fn ssh_config_dependency_paths(user_profile: &Path) -> Vec<PathBuf>
作用:这是对外使用的入口函数。给它一个用户主目录,它会返回 SSH 配置可能依赖的一串文件路径,方便后续决定哪些文件要带进沙盒。
数据流:输入是用户主目录路径 → 它先拼出用户的 .ssh/config,并把这个主配置文件放进结果列表 → 然后调用 visit_config 继续深入读取配置和 Include → 输出是一组可能需要的 SSH 相关路径。
调用关系:它是这一套扫描流程的起点,会被 filter_ssh_config_dependency_roots 调用。它自己不解析细节,而是搭好初始路径和结果容器后,把真正的读取和递归扫描交给 visit_config。
调用图:调用 1 个内部函数(visit_config);被 1 处调用(filter_ssh_config_dependency_roots);外部调用 3 个(new, join, vec!)。
visit_config29–70 ↗
fn visit_config(
path: &Path,
user_profile: &Path,
ssh_dir: &Path,
visited: &mut HashSet<PathBuf>,
paths: &mut Vec<PathBuf>,
depth: usize,
)
作用:这个函数负责真正打开一个 SSH 配置文件,读里面每一行,找出 Include 和各种指向文件的配置项。它还防止重复读取同一个文件,避免 Include 互相绕圈导致程序卡住。
数据流:输入是当前配置文件路径、用户主目录、.ssh 目录、已访问集合、结果列表和递归深度 → 它先检查深度和是否读过,再读取文件内容 → 对每行配置,遇到 Include 就找出被包含的文件并继续递归,遇到私钥、证书等路径配置就转换成真实路径加入结果 → 它不单独返回值,而是直接扩充传进来的路径列表。
调用关系:它由 ssh_config_dependency_paths 启动。扫描过程中,如果碰到 Include,会把路径展开工作交给 include_paths;如果碰到普通路径参数,会交给 profile_path_arg 把 SSH 写法变成系统路径。
调用图:调用 2 个内部函数(include_paths, profile_path_arg);被 1 处调用(ssh_config_dependency_paths);外部调用 2 个(canonicalize, read_to_string)。
include_paths72–81 ↗
fn include_paths(arg: &str, user_profile: &Path, ssh_dir: &Path) -> Vec<PathBuf>
作用:这个函数专门处理 SSH 配置里的 Include 参数。Include 可能是一个具体文件,也可能带通配符,它负责把这种写法变成实际存在的一批文件路径。
数据流:输入是 Include 后面的参数、用户主目录和 .ssh 目录 → 它先用 profile_path_arg 把 ~、相对路径等写法换成真实路径 → 再用 glob 通配符匹配实际文件 → 输出匹配到的路径列表;如果参数无效或通配符写错,就返回空列表。
调用关系:它在 visit_config 发现 Include 时被调用。它先借助 profile_path_arg 解释路径含义,然后用 glob 展开通配符,把结果交回 visit_config 继续读取这些被包含的配置文件。
调用图:调用 1 个内部函数(profile_path_arg);被 1 处调用(visit_config);外部调用 2 个(new, glob)。
directive83–105 ↗
fn directive(line: &str) -> Option<(String, Vec<String>)>
作用:这个函数把一行 SSH 配置拆成“配置名”和“参数列表”。比如把 IdentityFile ~/.ssh/id 拆成 key 是 IdentityFile,参数是 ~/.ssh/id。
数据流:输入是一行文本 → 它先调用 words 把这一行按 SSH 常见规则拆成词 → 再处理 key=value、key = value 这类写法 → 输出一个可选的键和值列表;如果这一行没有有效内容,就输出空。
调用关系:它依赖 words 做底层拆词,自己负责理解“哪个是配置名、哪些是配置值”。在整体流程里,它是读配置文件时的翻译员,把人写的配置行变成程序能判断的结构。
words107–143 ↗
fn words(line: &str) -> Vec<String>
作用:这个函数负责把一行文字切成一个个词,但会尊重引号、注释和转义符。这样带空格的路径如果被引号包住,就不会被错误拆开。
数据流:输入是一行原始文本 → 它从左到右逐个字符看,遇到未加引号的 # 就当作注释结束,遇到引号就进入或退出引号状态,遇到可转义字符就按规则保留 → 输出拆好的字符串列表。
调用关系:它被 directive 使用,是配置解析里最底层的小工具。directive 只关心配置名和值,而 words 负责先把复杂文本安全地切开。
profile_path_arg145–173 ↗
fn profile_path_arg(
arg: &str,
user_profile: &Path,
relative_base: Option<&Path>,
) -> Option<PathBuf>
作用:这个函数把 SSH 配置里的路径写法转换成真正的系统路径。它认识 none、~、%d、${HOME},也能处理绝对路径和带基准目录的相对路径。
数据流:输入是一个路径参数、用户主目录,以及可选的相对路径基准目录 → 如果参数是 none,就表示没有路径,返回空 → 如果是 ~、%d 或 ${HOME} 开头,就换成用户主目录 → 如果本来就是绝对路径,就直接使用 → 如果是相对路径且提供了基准目录,就拼到基准目录下面 → 输出转换后的路径或空。
调用关系:它被 visit_config 和 include_paths 调用。visit_config 用它解释 IdentityFile、CertificateFile 等文件路径;include_paths 用它解释 Include 后面的路径,再继续做通配符展开。
调用图:被 2 处调用(include_paths, visit_config);外部调用 3 个(join, to_path_buf, from)。
tests::collects_path_directive_profile_entries184–218 ↗
fn collects_path_directive_profile_entries()
作用:这个测试确认程序能从 SSH 主配置里收集各种常见的路径配置项。它覆盖私钥、证书、known_hosts、控制连接路径、agent socket 等写法。
数据流:测试先创建一个临时用户目录和 .ssh/config → 写入多种带 ~、%d、${HOME}、引号和等号的配置 → 调用 ssh_config_dependency_paths 得到结果 → 用断言检查结果是否正好是预期路径。
调用关系:它是测试代码,用来保护 ssh_config_dependency_paths 以及背后的解析函数不被改坏。它不参与正常运行,只在测试时创建临时文件并验证扫描结果。
调用图:外部调用 4 个(new, assert_eq!, create_dir_all, write)。
tests::recursively_collects_include_dependencies221–241 ↗
fn recursively_collects_include_dependencies()
作用:这个测试确认 Include 能被正确处理,而且被 Include 的配置文件里的依赖路径也会继续被收集。
数据流:测试先创建临时 .ssh/config 和 conf.d/devbox.conf → 主配置写 Include conf.d/*.conf,被包含文件写 CertificateFile → 调用 ssh_config_dependency_paths → 检查结果里先有主配置,再有被包含配置,最后有证书路径。
调用关系:它验证 visit_config 和 include_paths 的配合是否正确。正常运行时这段不会执行,但测试时它能发现 Include 递归扫描或通配符展开被改坏的问题。
调用图:外部调用 4 个(new, assert_eq!, create_dir_all, write)。
tests::slash_paths243–248 ↗
fn slash_paths(paths: Vec<PathBuf>) -> Vec<PathBuf>
作用:这个测试辅助函数把路径里的反斜杠统一换成斜杠。这样同一组测试在 Windows 和其他系统上更容易比较结果。
数据流:输入是一组路径 → 它把每个路径转成文字,把 \ 替换成 /,再转回路径 → 输出格式统一后的路径列表,不改动真实文件。
调用关系:它只被测试用来整理断言里的路径显示形式。它不参与 SSH 依赖扫描本身,只是让测试结果在不同操作系统上更稳定。
windows-sandbox-rs/src/winutil.rs源码 ↗
Rust 程序直接调用 Windows 底层接口时,很多东西不能按普通字符串和普通参数来传。比如 Windows API 常要用 UTF-16 宽字符串,还要以 0 结尾;创建进程时,命令行参数里的空格、引号、反斜杠必须按 Windows 的规矩转义;权限控制里还会遇到 SID,也就是 Windows 用来代表用户或用户组的一串身份编号。这个文件就像一盒彩色转接头:把 Rust 里的普通文本转成 Windows 能读的格式,把一组参数拼成 Windows 能正确拆回来的命令行,把系统错误码翻译成人能看懂的话,还能在账户名和 SID 字节之间转换。它还内置了几个常见账户组,比如 Administrators、Users、Everyone,避免依赖不同语言系统里的显示名称。因为这些工具会被创建进程、设置权限、命名管道等地方反复用到,所以集中放在这里,减少各处重复写危险的 Windows 细节。
to_wide19–23 ↗
fn to_wide(s: S) -> Vec<u16>
作用:把 Rust 里的普通系统字符串转成 Windows API 常用的宽字符串。宽字符串可以简单理解成 Windows 更习惯的一种文字编码,并且最后要带一个 0 表示到头了。
数据流:输入是一段字符串或类似字符串的东西 → 它先把文字编码成 UTF-16 的数字列表,再在末尾加上 0 → 输出一个 u16 数组,后面可以直接交给 Windows 系统函数使用,不会改动外部数据。
调用关系:这是很多 Windows 调用前的准备动作。权限设置里的 add_allow_ace、add_deny_ace、revoke_ace,文件安全描述相关的 fetch_dacl_handle,进程启动里的 spawn_conpty_process_as_user、prepare,以及本文件的 resolve_sid 和 sid_bytes_from_string 都会先用它把名字或 SID 字符串变成 Windows 能吃的格式。
调用图:被 21 处调用(add_allow_ace, add_deny_ace, allow_null_device, ensure_allow_mask_aces_with_inheritance_impl, fetch_dacl_handle, revoke_ace, spawn_conpty_process_as_user, prepare, create, spawn_runner_transport (+11 more));外部调用 1 个(as_ref)。
quote_windows_arg29–65 ↗
fn quote_windows_arg(arg: &str) -> String
作用:把一个命令行参数按 Windows 的规则加引号和转义。这样参数里有空格、引号或反斜杠时,子程序仍然能收到原来的完整内容。
数据流:输入是一个单独参数文本 → 它先判断需不需要加引号;如果需要,就逐个字符检查空格、引号和反斜杠,并按 Windows 解析规则补上反斜杠和外层引号 → 输出一个安全的参数字符串。
调用关系:它是拼接整条 Windows 命令行的基础零件。argv_to_command_line 会对每个参数分别使用它,避免把多个参数粗暴拼在一起后被 Windows 错误拆分。
调用图:外部调用 1 个(with_capacity)。
argv_to_command_line69–74 ↗
fn argv_to_command_line(argv: &[String]) -> String
作用:把一组程序参数拼成一条 Windows CreateProcess 风格的命令行。Windows 创建进程时经常要的是一整行文本,而不是已经分好的参数数组。
数据流:输入是参数列表,比如程序名、开关、脚本文本 → 它把每个参数分别按 Windows 规则加引号或转义,再用空格连起来 → 输出一条完整命令行字符串。
调用关系:create_process_as_user 在替某个用户启动进程时会用到它。它处在“准备启动进程”的阶段,把项目内部清楚分开的参数,变成 Windows 创建进程接口需要的单条命令行。
调用图:被 1 处调用(create_process_as_user)。
format_last_error77–103 ↗
fn format_last_error(err: i32) -> String
作用:把 Win32 错误码翻译成人能读的错误说明。Win32 是 Windows 的传统系统接口,错误码本身通常只是数字,不直观。
数据流:输入是一个 Windows 错误码 → 它调用系统的 FormatMessageW 查找对应说明,把 UTF-16 文本转成 Rust 字符串,并清理系统分配的内存 → 输出可读的错误文字;如果查不到,就输出类似 Win32 error 123 的兜底说明。
调用关系:这是排查 Windows API 失败时用的解释器。它不直接驱动主流程,但当底层调用只给出数字错误时,其他代码可以用它把数字变成日志或报错里更容易理解的内容。
调用图:外部调用 7 个(from_utf16_lossy, format!, null, null_mut, from_raw_parts, LocalFree, FormatMessageW)。
string_from_sid_bytes105–124 ↗
fn string_from_sid_bytes(sid: &[u8]) -> Result<String, String>
作用:把 SID 的原始字节变成人能复制、能阅读的 SID 字符串。SID 是 Windows 用来标识用户或用户组的身份编号。
数据流:输入是一段 SID 字节 → 它交给 Windows 的 ConvertSidToStringSidW 转成字符串形式,再把 Windows 返回的宽字符串转成 Rust 字符串,并释放系统分配的内存 → 成功时输出类似 S-1-5-... 的文本,失败时输出错误说明。
调用关系:create_named_pipe 会用到它,通常是为了在创建命名管道并设置权限时,把手里的 SID 字节转成可读形式,方便构造安全信息或排查问题。
调用图:被 1 处调用(create_named_pipe);外部调用 6 个(from_utf16_lossy, format!, null_mut, from_raw_parts, LocalFree, ConvertSidToStringSidW)。
resolve_sid132–168 ↗
fn resolve_sid(name: &str) -> Result<Vec<u8>>
作用:把账户名或常见用户组名解析成 SID 字节。这样后续设置权限时,不用靠容易变动的显示名称,而是用 Windows 真正识别的身份编号。
数据流:输入是账户名,比如 Everyone 或某个本机用户 → 它先查内置的常见 SID 表;如果命中,就把 SID 字符串转成字节;如果没命中,就调用 Windows 的 LookupAccountNameW 查询,并在缓冲区不够时扩大空间重试 → 输出 SID 字节,失败时返回带原因的错误。
调用关系:create_named_pipe 会在设置管道访问权限时调用它。它内部先问 well_known_sid_str 有没有现成答案,必要时再调用 sid_bytes_from_string 或 Windows 的账户查询接口,属于权限配置前的身份解析步骤。
调用图:调用 3 个内部函数(sid_bytes_from_string, to_wide, well_known_sid_str);被 1 处调用(create_named_pipe);外部调用 7 个(new, new, anyhow!, null, vec!, GetLastError, LookupAccountNameW)。
well_known_sid_str170–179 ↗
fn well_known_sid_str(name: &str) -> Option<&'static str>
作用:识别几个 Windows 常见账户组,并直接给出它们固定的 SID 字符串。这样即使系统语言不同,也不会因为名字翻译不同而找不到账户。
数据流:输入是一个名称 → 它和 Administrators、Users、Authenticated Users、Everyone、SYSTEM 这些固定名字逐一匹配 → 命中时输出对应 SID 字符串,没命中时输出空值。
调用关系:它只被 resolve_sid 使用,是 resolve_sid 的快速查询表。resolve_sid 先问它有没有固定答案,有就不用再走较慢、也更依赖系统环境的账户查找。
调用图:被 1 处调用(resolve_sid)。
sid_bytes_from_string181–206 ↗
fn sid_bytes_from_string(sid_str: &str) -> Result<Vec<u8>>
作用:把文本形式的 SID 转成 Windows 内部使用的字节形式。权限相关的系统函数通常要的是这种二进制 SID。
数据流:输入是类似 S-1-5-32-544 的 SID 字符串 → 它先转成 Windows 宽字符串,再调用 ConvertStringSidToSidW 得到系统分配的 SID,读取 SID 长度,复制到 Rust 自己的字节数组里,最后释放系统内存 → 输出 SID 字节,任何一步失败都会返回错误。
调用关系:它被 resolve_sid 调用,主要处理 well_known_sid_str 找到的固定 SID。也就是说,常见用户组先被映射成字符串,再由它变成后续权限代码能直接使用的字节。
调用图:调用 1 个内部函数(to_wide);被 1 处调用(resolve_sid);外部调用 8 个(new, anyhow!, null_mut, vec!, LocalFree, ConvertStringSidToSidW, CopySid, GetLengthSid)。
tests::argv_to_command_line_quotes_each_argument_independently214–226 ↗
fn argv_to_command_line_quotes_each_argument_independently()
作用:这是一个测试,确认整条命令行里的每个参数都会单独、安全地加引号,而不是把已经包含引号和空格的复杂参数弄乱。
数据流:输入是一组模拟参数,其中第三个参数本身包含带引号的 PowerShell 路径和其他命令内容 → 测试调用 argv_to_command_line 得到实际结果,再和预期字符串比较 → 如果两者不同,测试失败,提醒命令行转义规则被破坏了。
调用关系:它在测试阶段运行,守住 argv_to_command_line 和 quote_windows_arg 的行为。这个场景接近 cmd.exe /c 后面再包一层命令的情况,能防止创建进程时参数边界被 Windows 解析错。
调用图:外部调用 2 个(assert_eq!, vec!)。
tests::argv_to_command_line_quotes_regular_program_args229–240 ↗
fn argv_to_command_line_quotes_regular_program_args()
作用:这是一个测试,确认普通程序参数里带空格和引号时,拼出来的 Windows 命令行仍然正确。
数据流:输入是一组模拟的 PowerShell 参数,其中命令文本包含 hello world 和双引号 → 测试生成命令行,再和手写的正确结果比较 → 一旦转义少了或多了,测试就会失败。
调用关系:它在测试阶段运行,覆盖更常见的程序启动场景。它和另一个测试一起说明:无论是复杂嵌套命令,还是普通带引号参数,argv_to_command_line 都必须稳定地把参数交给 Windows。
调用图:外部调用 2 个(assert_eq!, vec!)。