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
- Rewrite source as exactly `capsules/<file>.capsule` — two path components, forward slashes, relative.
- Remove any `.`/`..` segments, leading slashes, or backslashes from the source path.
- Move the capsule asset so it sits directly under the distro's `capsules/` directory if it currently lives deeper.
- Rename the asset so its extension is `.capsule`.
- 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
- Generate source paths programmatically as format!("capsules/{name}.capsule") instead of writing them by hand.
- Never copy absolute or OS-specific paths into the manifest; reject any source not starting with `capsules/` in CI.
- Treat non-canonical paths from untrusted manifests as path-traversal input and reject them upstream.
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
- must be an absolute path
- bundled executable must have a parent directory
- cannot contain a platform PATH separator
- embedded capsule source has no asset
- Subscribe ` ` has priority outside the u32 range.
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)