{"record":{"id":"cb8f1f554f6a5d9f","repo":"astrid-runtime/astrid","slug":"capsule-archive-contains-duplicate-path-name","errorCode":null,"errorMessage":"capsule archive contains duplicate path {name}","messagePattern":"capsule archive contains duplicate path (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-capsule-install/src/source_digest.rs","lineNumber":54,"sourceCode":"    let decoder = flate2::read::GzDecoder::new(file);\n    let mut archive = tar::Archive::new(decoder);\n    let mut names = BTreeSet::new();\n    for entry in archive.entries().context(\"read capsule archive entries\")? {\n        let mut entry = entry.context(\"read capsule archive entry\")?;\n        let path = entry.path().context(\"read capsule archive path\")?;\n        if path.is_absolute()\n            || path\n                .components()\n                .any(|component| matches!(component, std::path::Component::ParentDir))\n        {\n            bail!(\"capsule archive contains an unsafe path {}\", path.display());\n        }\n        let name = path\n            .to_str()\n            .ok_or_else(|| anyhow::anyhow!(\"capsule archive path is not UTF-8\"))?\n            .replace('\\\\', \"/\");\n        if !names.insert(name.clone()) {\n            bail!(\"capsule archive contains duplicate path {name}\");\n        }\n        let entry_type = entry.header().entry_type();\n        if !entry_type.is_dir() && !entry_type.is_file() {\n            bail!(\"capsule archive contains a link or special file {name}\");\n        }\n        let destination = staging.path().join(&path);\n        if entry_type.is_dir() {\n            fs::create_dir_all(&destination)\n                .with_context(|| format!(\"create capsule archive directory {name}\"))?;\n            continue;\n        }\n        if let Some(parent) = destination.parent() {\n            fs::create_dir_all(parent)\n                .with_context(|| format!(\"create capsule archive parent for {name}\"))?;\n        }\n        entry\n            .unpack(&destination)\n            .with_context(|| format!(\"unpack capsule archive file {name}\"))?;","sourceCodeStart":36,"sourceCodeEnd":72,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-capsule-install/src/source_digest.rs#L36-L72","documentation":"canonical_archive_for_source collects archive entry names into a BTreeSet to guarantee a one-to-one mapping between paths and content. If the same normalized path (backslashes normalized to slashes) appears more than once in the archive, digesting would be ambiguous — the canonical builder cannot decide which copy wins — so it bails.","triggerScenarios":"Calling archive_digest_for_source on a .tar.gz that contains two entries with the identical path, e.g. from appending to an existing archive (`tar -rf` twice) or concatenating two archives.","commonSituations":"Incremental repacking scripts that append instead of recreating the archive; archives merged by naive `cat a.tgz b.tgz`-style tooling; CI caches that reuse a growing archive file.","solutions":["List duplicates: `tar -tzf capsule.tgz | sort | uniq -d`.","Recreate the archive from a clean tree instead of appending: `rm capsule.tgz && tar -czf capsule.tgz -C <root> .`.","Fix the packaging script so it always builds a fresh archive per run."],"exampleFix":"// before: append to existing archive, duplicating entries\ntar -rf capsule.tar Capsule.toml   # run twice -> duplicates\n// after: always rebuild from scratch\ntar -czf capsule.tgz -C capsule-root .","handlingStrategy":"validation","validationCode":"let out = std::process::Command::new(\"sh\")\n    .arg(\"-c\")\n    .arg(format!(\"tar -tzf {} | sort | uniq -d\", archive_path))\n    .output()?;\nif !out.stdout.is_empty() {\n    eprintln!(\"duplicate archive entries: {}\", String::from_utf8_lossy(&out.stdout));\n}","typeGuard":"fn has_no_duplicates(names: &[String]) -> bool {\n    let mut seen = std::collections::HashSet::new();\n    names.iter().all(|n| seen.insert(n.replace('\\\\', \"/\")))\n}","tryCatchPattern":"match archive_digest_for_source(archive) {\n    Err(e) if e.to_string().contains(\"duplicate path\") => {\n        anyhow::bail!(\"recreate the archive from a clean tree; do not append to an existing tar\");\n    }\n    other => other,\n}","preventionTips":["Always create archives fresh (rm + tar -czf) instead of appending with tar -rf","Never concatenate compressed archives to merge them","Add a CI step that fails on duplicate `tar -t` entries","Keep packaging scripts idempotent"],"tags":["archive","tar","validation","duplicates"],"backgroundTag":"invalid-argument-format","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-14T11:17:12.474Z"}