unicity-aos/aos-ce · error · io::Error

embedded capsule source is not canonical

Error message

embedded capsule source is not canonical: {source}

What it means

The library enforces that each capsule `source` is a canonical relative path of exactly two Normal components: `capsules/<file>.capsule` — no `./`, `..`, absolute paths, extra nesting, symlinks-in-path components, or wrong extension. This error is thrown when any of those path-component or extension checks fail. It exists to keep embedded asset references deterministic and to block path traversal.

Solutions

  1. Rewrite source as exactly `capsules/<file>.capsule` — two path components, forward slashes, relative.
  2. Remove any `.`/`..` segments, leading slashes, or backslashes from the source path.
  3. Move the capsule asset so it sits directly under the distro's `capsules/` directory if it currently lives deeper.
  4. Rename the asset so its extension is `.capsule`.
  5. Note this can also indicate a path-traversal attempt — validate manifest inputs at build time if they come from untrusted sources.

Example fix

// before
[[capsule]]
name = "foo"
source = "./capsules/../capsules/foo.tar"

// after
[[capsule]]
name = "foo"
source = "capsules/foo.capsule"
Defensive patterns

Strategy: validation

Validate before calling

// Rust: pre-validate source paths are canonical capsules/<name>.capsule
fn ensure_canonical_sources(manifest_toml: &str) -> Result<(), String> {
    let v: toml::Value = toml::from_str(manifest_toml).map_err(|e| e.to_string())?;
    for c in v.get("capsule").and_then(toml::Value::as_array).unwrap_or(&vec![]) {
        if let Some(src) = c.get("source").and_then(toml::Value::as_str) {
            let p = std::path::Path::new(src);
            let ok = p.components().count() == 2
                && src.starts_with("capsules/")
                && !src.contains('\\')
                && p.extension().map(|e| e == "capsule").unwrap_or(false);
            if !ok {
                return Err(format!("source must be exactly capsules/<file>.capsule, got {src}"));
            }
        }
    }
    Ok(())
}

Type guard

fn is_canonical_capsule_source(src: &str) -> bool {
    let p = std::path::Path::new(src);
    p.components().count() == 2
        && src.starts_with("capsules/")
        && p.extension().map(|e| e == "capsule").unwrap_or(false)
        && p.components().all(|c| matches!(c, std::path::Component::Normal(_)))
}

Try / catch

match install_capsule_fixtures(...) {
    Err(e) if e.to_string().starts_with("embedded capsule source is not canonical:") => {
        eprintln!("{e}; rewrite source as `capsules/<name>.capsule` (relative, forward slashes, no ./ or ..)");
    }
    other => other,
}

Prevention

When it happens

Trigger: A `[[capsule]]` source value that is absolute (`/x/capsules/a.capsule`), contains parent refs (`capsules/../capsules/a.capsule`), starts with `./`, is deeper than one directory level (`assets/capsules/a.capsule`), omits the `capsules/` prefix, lacks the `.capsule` extension, or has extra components after the filename. Raised from capsule_assets_from_manifest during capsule_dir_with / prepare_unicity_ce_init / install_capsule_fixtures.

Common situations: Copy-pasting an absolute build path from a local machine into the manifest; using `./capsules/foo.capsule`; referencing capsules in nested vendor directories; renaming the extension to `.tar` or forgetting it; Windows backslash separators in the source string.

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 unicity-aos/aos-ce@f6f22024fb (2026-09-13). Data as JSON: /api/errors/1a88d8ede291cf97. Report an issue: GitHub.

Appendix: source

Thrown at crates/unicity-aos-bootstrap/src/lib.rs:684

            .get("source")
            .and_then(toml::Value::as_str)
            .ok_or_else(|| {
                io::Error::new(io::ErrorKind::InvalidData, "embedded capsule has no source")
            })?;
        let relative = Path::new(source);
        let mut components = relative.components();
        if components.next() != Some(std::path::Component::Normal(OsStr::new("capsules")))
            || components
                .next()
                .and_then(|component| match component {
                    std::path::Component::Normal(name) => Some(name),
                    _ => None,
                })
                .is_none()
            || components.next().is_some()
            || relative.extension() != Some(OsStr::new("capsule"))
        {
            return Err(io::Error::new(
                io::ErrorKind::InvalidData,
                format!("embedded capsule source is not canonical: {source}"),
            ));
        }
        let asset = relative
            .file_name()
            .expect("validated capsule source has a filename")
            .to_str()
            .ok_or_else(|| {
                io::Error::new(io::ErrorKind::InvalidData, "capsule asset is not UTF-8")
            })?
            .to_owned();
        if asset != format!("{package}.capsule") {
            return Err(io::Error::new(
                io::ErrorKind::InvalidData,
                format!("embedded capsule source does not match package {package}"),
            ));
        }

View on GitHub (pinned to f6f22024fb)