BigPizzaV3/CodexPlusPlus · error · anyhow::Error

压缩包条目路径为空

Error message

压缩包条目路径为空

What it means

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.

Solutions

  1. 检查 zip 包内条目名,剔除空名或纯 './' 的条目后重新打包上传
  2. 换用标准打包工具(如 zip -r 或 GitHub 官方 zip 产物)重新生成压缩包
  3. 若确需允许该条目,在代码中把空路径条目直接 skip 而不是报错(需自行评估风险)

Example fix

// before
if relative.as_os_str().is_empty() {
    anyhow::bail!("压缩包条目路径为空");
}
// after
if relative.as_os_str().is_empty() {
    tracing::warn!("跳过无有效路径的压缩包条目");
    return Ok(None); // 调用方按 Option<PathBuf> 处理
}
Defensive patterns

Strategy: validation

Validate before calling

fn is_safe_entry_name(name: &str) -> bool {
    let mut has_normal = false;
    for comp in std::path::Path::new(name).components() {
        match comp {
            std::path::Component::Normal(_) => has_normal = true,
            std::path::Component::CurDir => {}
            _ => return false,
        }
    }
    has_normal
}

Type guard

if !is_safe_entry_name(entry.name()) { continue; }

Prevention

When it happens

Trigger: 调用 extract_skill_subtree 解压一个 GitHub 仓库 zip,其中某个条目名(name)为空、仅为 '/'、或所有组件都是 CurDir('.'),safe_relative_path 递归处理每个 Component 后 relative 为空。

Common situations: 第三方仓库打包工具生成了畸形条目(空文件名或纯目录占位条目);手工构造或被篡改的 zip 包;非标准 zip 工具产出的以 ./ 前缀开头的条目被逐段剥光后只剩空路径。

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of BigPizzaV3/CodexPlusPlus@b1ed92e5e4 (2026-09-19). Data as JSON: /api/errors/0598b57fc5ed0b4c. Report an issue: GitHub.

Appendix: source

Thrown at crates/codex-plus-core/src/skills.rs:843

    }

    if !wrote_manifest {
        anyhow::bail!("{repo_path} 下没有 SKILL.md,不是一个有效的 skill");
    }
    Ok(())
}

fn safe_relative_path(value: &str) -> anyhow::Result<PathBuf> {
    let mut relative = PathBuf::new();
    for component in Path::new(value).components() {
        match component {
            std::path::Component::Normal(part) => relative.push(part),
            std::path::Component::CurDir => {}
            _ => anyhow::bail!("压缩包条目越界:{value}"),
        }
    }
    if relative.as_os_str().is_empty() {
        anyhow::bail!("压缩包条目路径为空");
    }
    Ok(relative)
}

pub fn repo_zip_url(repo: &SkillRepo) -> String {
    format!(
        "https://codeload.github.com/{}/{}/zip/refs/heads/{}",
        repo.owner, repo.name, repo.branch
    )
}

pub fn repo_tree_url(repo: &SkillRepo) -> String {
    format!(
        "https://api.github.com/repos/{}/{}/git/trees/{}?recursive=1",
        repo.owner, repo.name, repo.branch
    )
}

View on GitHub (pinned to b1ed92e5e4)