astrid-runtime/astrid · error

capsule archive contains duplicate path {name}

Error message

capsule archive contains duplicate path {name}

What it means

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.

Source

Thrown at crates/astrid-capsule-install/src/source_digest.rs:54

    let decoder = flate2::read::GzDecoder::new(file);
    let mut archive = tar::Archive::new(decoder);
    let mut names = BTreeSet::new();
    for entry in archive.entries().context("read capsule archive entries")? {
        let mut entry = entry.context("read capsule archive entry")?;
        let path = entry.path().context("read capsule archive path")?;
        if path.is_absolute()
            || path
                .components()
                .any(|component| matches!(component, std::path::Component::ParentDir))
        {
            bail!("capsule archive contains an unsafe path {}", path.display());
        }
        let name = path
            .to_str()
            .ok_or_else(|| anyhow::anyhow!("capsule archive path is not UTF-8"))?
            .replace('\\', "/");
        if !names.insert(name.clone()) {
            bail!("capsule archive contains duplicate path {name}");
        }
        let entry_type = entry.header().entry_type();
        if !entry_type.is_dir() && !entry_type.is_file() {
            bail!("capsule archive contains a link or special file {name}");
        }
        let destination = staging.path().join(&path);
        if entry_type.is_dir() {
            fs::create_dir_all(&destination)
                .with_context(|| format!("create capsule archive directory {name}"))?;
            continue;
        }
        if let Some(parent) = destination.parent() {
            fs::create_dir_all(parent)
                .with_context(|| format!("create capsule archive parent for {name}"))?;
        }
        entry
            .unpack(&destination)
            .with_context(|| format!("unpack capsule archive file {name}"))?;

View on GitHub (pinned to affd8760f4)

Solutions

  1. List duplicates: `tar -tzf capsule.tgz | sort | uniq -d`.
  2. Recreate the archive from a clean tree instead of appending: `rm capsule.tgz && tar -czf capsule.tgz -C <root> .`.
  3. Fix the packaging script so it always builds a fresh archive per run.

Example fix

// before: append to existing archive, duplicating entries
tar -rf capsule.tar Capsule.toml   # run twice -> duplicates
// after: always rebuild from scratch
tar -czf capsule.tgz -C capsule-root .
Defensive patterns

Strategy: validation

Validate before calling

let out = std::process::Command::new("sh")
    .arg("-c")
    .arg(format!("tar -tzf {} | sort | uniq -d", archive_path))
    .output()?;
if !out.stdout.is_empty() {
    eprintln!("duplicate archive entries: {}", String::from_utf8_lossy(&out.stdout));
}

Type guard

fn has_no_duplicates(names: &[String]) -> bool {
    let mut seen = std::collections::HashSet::new();
    names.iter().all(|n| seen.insert(n.replace('\\', "/")))
}

Try / catch

match archive_digest_for_source(archive) {
    Err(e) if e.to_string().contains("duplicate path") => {
        anyhow::bail!("recreate the archive from a clean tree; do not append to an existing tar");
    }
    other => other,
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/cb8f1f554f6a5d9f. Report an issue: GitHub.