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

capsule asset is not UTF-8

Error message

capsule asset is not UTF-8

What it means

After path validation, the library converts the capsule asset's filename to a Rust str to use as an embedded asset key. This error is thrown when that filename is not valid UTF-8 (Rust OsStr/Path are not guaranteed UTF-8). Practically it means the capsule file on disk has a non-UTF-8 name, which cannot be used as an embedded asset identifier.

Solutions

  1. Rename the capsule file so its name is valid UTF-8, ideally plain ASCII matching `<package>.capsule`.
  2. Regenerate the capsule file with build tooling that uses ASCII identifiers for package names.
  3. Check that the manifest's source string itself is clean UTF-8 TOML (a valid TOML string already is; the corruption is typically on the filesystem side or from escaped bytes).
  4. Audit manifest generation scripts for byte-level filename handling (e.g. shelling out with raw locale-dependent output).

Example fix

// before (filename with non-UTF-8 bytes)
$ mv capsules/foo.capsule $'capsules/fo\xffo.capsule'

// after
$ mv capsules/foo.capsule capsules/foo.capsule   # plain ASCII, matches package name "foo"
Defensive patterns

Strategy: validation

Validate before calling

// Rust: verify asset filenames are valid UTF-8 before invoking the API
fn ensure_utf8_asset_names(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 name = std::path::Path::new(src).file_name().unwrap_or_default();
            if name.to_str().is_none() {
                return Err(format!("asset filename for {src} is not UTF-8"));
            }
        }
    }
    Ok(())
}

Type guard

fn is_utf8_file_name(src: &str) -> bool {
    std::path::Path::new(src).file_name().map(|n| n.to_str().is_some()).unwrap_or(false)
}

Try / catch

match capsule_assets_from_manifest() {
    Err(e) if e.to_string().contains("capsule asset is not UTF-8") => {
        eprintln!("{e}; rename the capsule file to a UTF-8 (ASCII) name matching <package>.capsule");
    }
    other => other,
}

Prevention

When it happens

Trigger: A `[[capsule]]` source resolving to a filename containing raw non-UTF-8 bytes (e.g. Latin-1 encoded characters or invalid byte sequences created on some filesystems), passed through capsule_assets_from_manifest via capsule_dir_with, prepare_unicity_ce_init, or install_capsule_fixtures.

Common situations: A capsule file renamed on a filesystem with a non-UTF-8 locale/encoding; a script writing filenames from unvalidated bytes; files transferred from systems using legacy codepages; accidental binary corruption of a filename.

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

Appendix: source

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

                .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}"),
            ));
        }
        if assets.contains(&asset) {
            return Err(io::Error::new(
                io::ErrorKind::InvalidData,
                format!("embedded distro selects duplicate capsule asset {asset}"),
            ));
        }
        assets.push(asset);
    }
    Ok(assets)
}

View on GitHub (pinned to f6f22024fb)