{"record":{"id":"301c7b08215be52c","repo":"BigPizzaV3/CodexPlusPlus","slug":"release-asset-name","errorCode":null,"errorMessage":"非法 Release asset 文件名: {name}","messagePattern":"非法 Release asset 文件名: (.+?)","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/codex-plus-core/src/update.rs","lineNumber":331,"sourceCode":"pub fn download_asset_to(\n    release: &Release,\n    bytes: &[u8],\n    download_dir: &Path,\n) -> anyhow::Result<PathBuf> {\n    let name = release\n        .asset_name\n        .as_ref()\n        .ok_or_else(|| anyhow::anyhow!(\"没有可下载的 Release asset\"))?;\n    let safe = safe_asset_name(name)?;\n    std::fs::create_dir_all(download_dir)?;\n    let path = download_dir.join(safe);\n    std::fs::write(&path, bytes)?;\n    Ok(path)\n}\n\npub fn safe_asset_name(name: &str) -> anyhow::Result<String> {\n    if name.trim().is_empty() {\n        anyhow::bail!(\"非法 Release asset 文件名: {name}\");\n    }\n    let path = Path::new(name);\n    if path.components().count() != 1 {\n        anyhow::bail!(\"非法 Release asset 文件名: {name}\");\n    }\n    let file_name = path\n        .file_name()\n        .and_then(|name| name.to_str())\n        .ok_or_else(|| anyhow::anyhow!(\"非法 Release asset 文件名: {name}\"))?;\n    if file_name == \".\" || file_name == \"..\" {\n        anyhow::bail!(\"非法 Release asset 文件名: {name}\");\n    }\n    Ok(file_name.to_string())\n}\n\nfn platform_asset_rank(name: &str) -> u8 {\n    // 0 = exact match (current OS + native arch)\n    // 1 = same OS, other arch (acceptable fallback, e.g. x86_64 on arm64 or vice versa)","sourceCodeStart":313,"sourceCodeEnd":349,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/1f431ae49b57b3055e0e6845ba6156c6b4232b4d/crates/codex-plus-core/src/update.rs#L313-L349","documentation":"safe_asset_name is the path-safety gate for release asset file names; its first check rejects names that are empty or only whitespace (\"非法 Release asset 文件名: {name}\"). download_asset_to calls it before joining the name onto download_dir, so a blank name can never cause a write to the directory itself. select_update_asset already filters empty names upstream, so hitting this usually means download_asset_to/safe_asset_name was called directly or the payload changed between selection and download.","triggerScenarios":"download_asset_to with release.asset_name = Some(\"\") or Some(\"   \"); direct calls to safe_asset_name(\"\") in tests or custom pipelines; a manifest whose asset name field is present but blank.","commonSituations":"Hand-edited latest.json with \"name\": \"\"; a generator emitting an empty name when the artifact filename variable is unset; the manifest being regenerated between the update check and the download.","solutions":["Fix the manifest/release so every asset carries a real file name","Re-fetch the release info before downloading if the manifest may have changed","Pre-validate with the public safe_asset_name() before calling download_asset_to"],"exampleFix":"// before\nlet path = download_asset_to(&release, &bytes, &dir)?;\n\n// after\nif let Some(name) = release.asset_name.as_deref() {\n    safe_asset_name(name)?; // fail fast with the real cause\n}\nlet path = download_asset_to(&release, &bytes, &dir)?;","handlingStrategy":"validation","validationCode":"if let Some(name) = release.asset_name.as_deref() {\n    codex_plus_core::update::safe_asset_name(name)?; // rejects blank names up front\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Validate asset names with safe_asset_name at manifest-ingest time, not at download time","Fail manifest generation when the artifact filename variable is empty","Re-fetch the manifest if it may have been regenerated since the last check"],"tags":["filename","release-asset","validation","download"],"backgroundTag":"unsafe-filename-validation","analyzedSha":"1f431ae49b57b3055e0e6845ba6156c6b4232b4d","analyzedAt":"2026-08-16T20:54:18.598Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}