{"record":{"id":"0598b57fc5ed0b4c","repo":"BigPizzaV3/CodexPlusPlus","slug":"skills-0598b5","errorCode":null,"errorMessage":"压缩包条目路径为空","messagePattern":"压缩包条目路径为空","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/codex-plus-core/src/skills.rs","lineNumber":843,"sourceCode":"    }\n\n    if !wrote_manifest {\n        anyhow::bail!(\"{repo_path} 下没有 SKILL.md，不是一个有效的 skill\");\n    }\n    Ok(())\n}\n\nfn safe_relative_path(value: &str) -> anyhow::Result<PathBuf> {\n    let mut relative = PathBuf::new();\n    for component in Path::new(value).components() {\n        match component {\n            std::path::Component::Normal(part) => relative.push(part),\n            std::path::Component::CurDir => {}\n            _ => anyhow::bail!(\"压缩包条目越界：{value}\"),\n        }\n    }\n    if relative.as_os_str().is_empty() {\n        anyhow::bail!(\"压缩包条目路径为空\");\n    }\n    Ok(relative)\n}\n\npub fn repo_zip_url(repo: &SkillRepo) -> String {\n    format!(\n        \"https://codeload.github.com/{}/{}/zip/refs/heads/{}\",\n        repo.owner, repo.name, repo.branch\n    )\n}\n\npub fn repo_tree_url(repo: &SkillRepo) -> String {\n    format!(\n        \"https://api.github.com/repos/{}/{}/git/trees/{}?recursive=1\",\n        repo.owner, repo.name, repo.branch\n    )\n}\n","sourceCodeStart":825,"sourceCodeEnd":861,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6/crates/codex-plus-core/src/skills.rs#L825-L861","documentation":"safe_relative_path finishes by rejecting a result whose OsStr is empty: the entry path contained no usable components (e.g. a bare directory entry or an all-'.' path). Writing it would produce an invalid destination path, so it is rejected.","triggerScenarios":"调用 extract_skill_subtree 解压一个 GitHub 仓库 zip，其中某个条目名（name）为空、仅为 '/'、或所有组件都是 CurDir('.')，safe_relative_path 递归处理每个 Component 后 relative 为空。","commonSituations":"第三方仓库打包工具生成了畸形条目（空文件名或纯目录占位条目）；手工构造或被篡改的 zip 包；非标准 zip 工具产出的以 ./ 前缀开头的条目被逐段剥光后只剩空路径。","solutions":["检查 zip 包内条目名，剔除空名或纯 './' 的条目后重新打包上传","换用标准打包工具（如 zip -r 或 GitHub 官方 zip 产物）重新生成压缩包","若确需允许该条目，在代码中把空路径条目直接 skip 而不是报错（需自行评估风险）"],"exampleFix":"// before\nif relative.as_os_str().is_empty() {\n    anyhow::bail!(\"压缩包条目路径为空\");\n}\n// after\nif relative.as_os_str().is_empty() {\n    tracing::warn!(\"跳过无有效路径的压缩包条目\");\n    return Ok(None); // 调用方按 Option<PathBuf> 处理\n}","handlingStrategy":"validation","validationCode":"fn is_safe_entry_name(name: &str) -> bool {\n    let mut has_normal = false;\n    for comp in std::path::Path::new(name).components() {\n        match comp {\n            std::path::Component::Normal(_) => has_normal = true,\n            std::path::Component::CurDir => {}\n            _ => return false,\n        }\n    }\n    has_normal\n}","typeGuard":"if !is_safe_entry_name(entry.name()) { continue; }","tryCatchPattern":null,"preventionTips":["只用标准工具（zip -r、git archive、GitHub 官方 zip）打包技能仓库","安装第三方技能前用 unzip -l 检查条目名","定期更新 skills.rs 以获得最新的路径安全修复"],"tags":["zip","path-validation","security","skill-install"],"backgroundTag":"path-traversal-blocked","analyzedSha":"b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6","analyzedAt":"2026-09-19T23:35:21.129Z","contentChangedAt":"2026-09-19T23:35:21.129Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}