astrid-runtime/astrid · error · io::Error

layout destination path is not portable UTF-8: {error}

Error message

layout destination path is not portable UTF-8: {error}

What it means

On non-Unix platforms, encoded_bytes_to_os_string converts receipt-encoded path bytes back into an OsString by requiring the bytes to be valid UTF-8. If the stored path bytes are not valid UTF-8 (e.g. written on a Unix system with raw non-UTF-8 bytes), the conversion fails with this InvalidData error including the underlying Utf8Error. This enforces portability of layout destination paths across platforms.

Source

Thrown at crates/astrid-core/src/dirs_layout_records.rs:242

}

#[cfg(unix)]
#[expect(
    clippy::unnecessary_wraps,
    reason = "the cross-platform receipt decoder has one fallible signature"
)]
pub(super) fn encoded_bytes_to_os_string(bytes: Vec<u8>) -> io::Result<OsString> {
    use std::os::unix::ffi::OsStringExt as _;

    // Unix receipts commit to the raw OsStr byte sequence, including paths
    // that are not UTF-8. Decoding must therefore preserve every byte.
    Ok(OsString::from_vec(bytes))
}

#[cfg(not(unix))]
pub(super) fn encoded_bytes_to_os_string(bytes: Vec<u8>) -> io::Result<OsString> {
    let text = String::from_utf8(bytes).map_err(|error| {
        io::Error::new(
            io::ErrorKind::InvalidData,
            format!("layout destination path is not portable UTF-8: {error}"),
        )
    })?;
    let path = OsString::from(&text);
    if path.as_encoded_bytes() != text.as_bytes() {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            "layout destination path cannot be represented losslessly on this platform",
        ));
    }
    Ok(path)
}

pub(super) fn inventory_tree(path: &Path) -> io::Result<LayoutTreeIdentityV1> {
    let mut hasher = blake3::Hasher::new_derive_key("astrid layout source inventory v1");
    let mut entries = 0_u64;
    let mut bytes = 0_u64;

View on GitHub (pinned to affd8760f4)

Solutions

  1. Regenerate the layout records on a system where the path is valid UTF-8.
  2. Rename the destination path to use only UTF-8/ASCII characters and rewrite the receipt.
  3. Inspect the encoded bytes with a hex dump to find and repair the invalid sequence in the source record.

Example fix

// before: non-UTF-8 filename on Linux
mv $'caf\xe9' café
// after: rewrite receipt against the clean path
let dest = Path::new("/data/café/volume.bin"); // valid UTF-8
Defensive patterns

Strategy: validation

Validate before calling

if std::str::from_utf8(&encoded_bytes).is_err() {
    return Err("recorded path bytes are not valid UTF-8; regenerate on a UTF-8 filesystem");
}

Try / catch

match physical_path(&destination) {
    Err(e) if e.to_string().contains("not portable UTF-8") => regenerate_records_with_utf8_paths(),
    other => other,
}

Prevention

When it happens

Trigger: Calling physical_path (or any receipt decoding that reaches encoded_bytes_to_os_string) on a non-Unix platform where the encoded destination path bytes contain invalid UTF-8 sequences.

Common situations: Receipts or records produced on Linux with non-UTF-8 file names (e.g. Latin-1 encoded bytes) then consumed on Windows/macOS; corrupted receipt bytes; hand-edited binary records.

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/cacc5929497ebba1. Report an issue: GitHub.