{"record":{"id":"1a88d8ede291cf97","repo":"unicity-aos/aos-ce","slug":"embedded-capsule-source-is-not-canonical-source","errorCode":null,"errorMessage":"embedded capsule source is not canonical: {source}","messagePattern":"embedded capsule source is not canonical: (.+?)","errorType":"validation","errorClass":"io::Error","httpStatus":null,"severity":"error","filePath":"crates/unicity-aos-bootstrap/src/lib.rs","lineNumber":684,"sourceCode":"            .get(\"source\")\n            .and_then(toml::Value::as_str)\n            .ok_or_else(|| {\n                io::Error::new(io::ErrorKind::InvalidData, \"embedded capsule has no source\")\n            })?;\n        let relative = Path::new(source);\n        let mut components = relative.components();\n        if components.next() != Some(std::path::Component::Normal(OsStr::new(\"capsules\")))\n            || components\n                .next()\n                .and_then(|component| match component {\n                    std::path::Component::Normal(name) => Some(name),\n                    _ => None,\n                })\n                .is_none()\n            || components.next().is_some()\n            || relative.extension() != Some(OsStr::new(\"capsule\"))\n        {\n            return Err(io::Error::new(\n                io::ErrorKind::InvalidData,\n                format!(\"embedded capsule source is not canonical: {source}\"),\n            ));\n        }\n        let asset = relative\n            .file_name()\n            .expect(\"validated capsule source has a filename\")\n            .to_str()\n            .ok_or_else(|| {\n                io::Error::new(io::ErrorKind::InvalidData, \"capsule asset is not UTF-8\")\n            })?\n            .to_owned();\n        if asset != format!(\"{package}.capsule\") {\n            return Err(io::Error::new(\n                io::ErrorKind::InvalidData,\n                format!(\"embedded capsule source does not match package {package}\"),\n            ));\n        }","sourceCodeStart":666,"sourceCodeEnd":702,"githubUrl":"https://github.com/unicity-aos/aos-ce/blob/f6f22024fb1e8d122f28a1b4a9f75aee448ae839/crates/unicity-aos-bootstrap/src/lib.rs#L666-L702","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\n[[capsule]]\nname = \"foo\"\nsource = \"./capsules/../capsules/foo.tar\"\n\n// after\n[[capsule]]\nname = \"foo\"\nsource = \"capsules/foo.capsule\"","handlingStrategy":"validation","validationCode":"// Rust: pre-validate source paths are canonical capsules/<name>.capsule\nfn ensure_canonical_sources(manifest_toml: &str) -> Result<(), String> {\n    let v: toml::Value = toml::from_str(manifest_toml).map_err(|e| e.to_string())?;\n    for c in v.get(\"capsule\").and_then(toml::Value::as_array).unwrap_or(&vec![]) {\n        if let Some(src) = c.get(\"source\").and_then(toml::Value::as_str) {\n            let p = std::path::Path::new(src);\n            let ok = p.components().count() == 2\n                && src.starts_with(\"capsules/\")\n                && !src.contains('\\\\')\n                && p.extension().map(|e| e == \"capsule\").unwrap_or(false);\n            if !ok {\n                return Err(format!(\"source must be exactly capsules/<file>.capsule, got {src}\"));\n            }\n        }\n    }\n    Ok(())\n}","typeGuard":"fn is_canonical_capsule_source(src: &str) -> bool {\n    let p = std::path::Path::new(src);\n    p.components().count() == 2\n        && src.starts_with(\"capsules/\")\n        && p.extension().map(|e| e == \"capsule\").unwrap_or(false)\n        && p.components().all(|c| matches!(c, std::path::Component::Normal(_)))\n}","tryCatchPattern":"match install_capsule_fixtures(...) {\n    Err(e) if e.to_string().starts_with(\"embedded capsule source is not canonical:\") => {\n        eprintln!(\"{e}; rewrite source as `capsules/<name>.capsule` (relative, forward slashes, no ./ or ..)\");\n    }\n    other => other,\n}","preventionTips":["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."],"tags":["path","validation","manifest","path-traversal"],"backgroundTag":"path-traversal-blocked","analyzedSha":"f6f22024fb1e8d122f28a1b4a9f75aee448ae839","analyzedAt":"2026-09-13T03:04:44.565Z","contentChangedAt":"2026-09-13T03:04:44.565Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}