路段下载缓存:SHA256 是 URL 键哈希,不是内容校验和

作者:

在

结论:工具侧的路段下载链把「C++ 桥接 → API 元数据 → 下载与缓存 → 工具登录」串成一条线,核心判定只有一句:缓存文件名是「URL 去掉查询串后的 SHA256」,它是 URL 键哈希,而不是内容校验和。因此同一路径换内容、改用不同签名的查询串或缓存文件本身损坏时,缓存命中分支不会重新验证。另一处容易踩的坑是 api_call() 把很多异常序列化成 {"error": ...} 却仍以正常进程退出,调用方必须解析错误字段,不能只看退出码。本文为对源码快照 c8fb906 的静态分析与隔离探针,未登录账户、未查询云端、未下载文件、未运行网络测试。

调用链与关键函数

PyDownloader::runPython() 通过 posix_spawnp 启动 python3 -m,分别读取 stdout 与 stderr,以 100 ms 的 select 轮询 abort,子进程失败时返回空串。这里 stdout 是机读结果、stderr 是进度/错误,PROGRESS 不等于文件校验完成;C++ Replay 与 Cabana 共用这一桥接。

组件/函数 具体实现 契约与边界
PyDownloader::runPython() posix_spawnp 启动子进程,读 stdout/stderr,100 ms select 轮询 abort stdout 是机读结果、stderr 是进度/错误;PROGRESS 不等于校验完成
api_call() / CommaApi.request() 从本地配置读令牌,请求带 JWT Authorization;成功或异常统一输出 JSON 401/403 映射未授权、404 映射 not_found;异常序列化后仍以正常退出,须解析错误字段
cmd_route_files() / cmd_devices() / cmd_device_routes() 调用路线文件列表、用户设备与设备路段 API,查询与起止时间过滤走不同端点 返回元数据或 URL,不验证下载后字节与路线所属设备身份
cache_file_path() / cmd_download() 去 URL 查询串后 SHA256 得缓存名;优先复用缓存,未命中则单 GET 流式落临时文件,按魔数识别 bz2/zst SHA256 是 URL 键哈希而非内容摘要;同路径换内容或缓存损坏时命中分支不重验
decompress_file() / materialize_cached_file() 流式解压并检查压缩流 eof;缓存已有压缩文件时生成解压副本 EOF 检查可发现截断,但不证明远端内容可信;下载层未显式比较下载字节数与长度头
auth_redirect_link() / login() 打开浏览器并临时监听 localhost 回调,换令牌后请求验证,再原子写入 auth.json 回调只按路径记录首个含 code/error 请求;login() 未在本地生成随机 state 并核对回传 state

需要强调的是:cmd_route_files() 返回的是元数据或 URL,它不验证下载后的字节与路线所属设备身份——账户可见性和车端控制权是两个概念。

还要区分字节流与元数据:cmd_devices() 与 cmd_device_routes() 保留用户设备与设备路段两个不同端点,返回的同样是元数据或 URL;下载后字节是否完整、路线是否属于该设备,都不在本层验证,只能由上层的内容摘要与 manifest 承担。

生命周期与失败语义

cmd_download() 先写下载缓存目录内的临时文件;进入内层 try/finally 后的异常会删除临时文件并调用 release_conn() 归还连接。但在该内层 try 之前,HTTP 状态/长度拒绝会直接 sys.exit(1),不经过连接释放;缓存命中则直接返回路径,不再重新读取完整内容。runPython() 在 abort 时发 SIGTERM,取消下载不应被解释成文件已缓存完成。

两条协议要分别处理:Python 网络查询的错误可能是 JSON stdout,而下载/解压错误是 stderr 加非零退出。get_token() 读取失败返回 None,set_token() 写入 config root——工具认证文件与车端设备身份/JWT 不是同一信任域,插件不得把用户账户令牌继承为默认网络权限。

本地认证配置与缓存的实测契约

隔离探针把 Paths.config_root() 限定在临时目录后确认:写入/替换/清除往返成功,新 auth.json 在本机权限为 0600,JSON 损坏或缺少 access_token 均返回 None,序列化失败不会损坏旧令牌,也未留下临时文件。但 get_token() 不校验类型,{"access_token": 42} 会原样返回整数 42,随后若被当作真值传入 CommaApi.__init__() 会因与字符串拼接而抛 TypeError;宽泛的 except Exception 又把读取错误、权限错误与未登录折叠成同一个 None。

输入情形 实际结果 工程含义
Content-Length: 10,响应流仅给 3 字节 输出路径并缓存这 3 字节,进度停在 3:10;该分支会 release_conn() 正的长度头只是入口门,没有末尾下载字节数一致性断言
HTTP 404 或 Content-Length: 0 SystemExit(1);假响应的 release_conn() 未被调用 头部拒绝发生在内层资源 finally 之前,连接回收不能由本函数保证
相同路径、不同签名查询串且缓存已有旧内容 返回同一 SHA256 URL 键对应的旧文件,不发起新 GET 键哈希不是内容摘要;URL 权限/查询串更新不能保证刷新数据

这些观察只针对当前代码的无压缩分支和受控假响应,0600 也只是当前本地临时测试结果,不是所有平台或既有配置目录权限的证明;未测真实连接池在进程退出后的清理、bz2/zst 解压、网络中断、缓存竞争或最终日志解析。

插件重构与硬件验证建议

应把 RouteCatalog、AuthenticatedFetcher、ContentStore 和 Decompressor 分开声明权限:路线插件只提供元数据/URL,下载插件固定允许的域名与传输策略,内容库以内容摘要而非 URL 作验证标识并保存原 URL、查询时刻、压缩类型、长度与 schema 版本,解压插件限制输出体积与资源预算。给回放实验生成不可变 manifest,在启动控制算法前逐段核对哈希、事件数、首末单调时间、相机文件包数与帧 ID。

这条链只服务离线分析/开发回放,不应成为车端控制或安全网关的动态插件加载入口。对自制硬件,先在无物理总线的隔离环境跑损坏缓存、URL 同路径变内容、截断压缩流、缺失 qlog、401/404、取消下载和本地回调异常测试,再考虑封闭台架。

常见问题

缓存文件名的 SHA256 能当内容校验和用吗?

不能。它是「URL 去掉查询串后」的 SHA256,即 URL 键哈希而非内容摘要。同一路径换内容、不同签名查询串或缓存文件损坏时,缓存命中分支都不会重新验证,必须额外记录内容哈希并支持强制刷新。

api_call() 退出码为 0 就代表成功吗?

不代表。api_call() 会把很多异常序列化为 {"error": ...} 却仍以正常进程退出,调用方必须解析返回里的错误字段,不能只看退出码。

下载被取消,是否说明文件已经缓存完成?

不是。runPython() 在 abort 时发 SIGTERM,下载任务取消不应被解释成文件已缓存完成;下载与解压错误是 stderr 加非零退出,与网络查询的 JSON stdout 是两条协议。

auth.json 的 0600 权限是通用保证吗?

不是。它是当前本地临时目录测试的结果,不代表所有平台或既有配置目录的权限;探针也没有运行浏览器回调、服务端令牌交换、API 请求或并发写入。

延伸阅读

参考资料

  • 项目 02 —— openpilot-research/ROUTE_DOWNLOAD_AUTH_DEEP_DIVE.md(工具侧下载桥接、API 元数据、缓存与工具登录链)
  • 项目 02 —— openpilot-research/probes/test_tool_auth_config_contract.py(认证配置读写的隔离探针)
  • 项目 02 —— openpilot-research/probes/test_file_downloader_cache_contract.py(下载缓存契约的假 HTTP 探针)

说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

更新记录

首次发布 2026-10-11 最近核对:2026-10-11

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注