Hmbown/CodeWhale · error · io::Error (InvalidData)

artifact name is not portable UTF-8

Error message

artifact name is not portable UTF-8

What it means

Every artifact entry name must be valid UTF-8 and free of Windows-hostile characters (backslash and colon) so that names are portable in the archive and safe as archive member paths. A file whose name is non-UTF-8 (common on Linux with arbitrary bytes in filenames) or contains `\` / `:` is rejected with InvalidData rather than producing a corrupt or non-portable archive.

Solutions

  1. Rename the offending files to portable ASCII names (`mv` the files detected with `convmv` or a find command).
  2. Fix the tool writing artifacts to use UTF-8 names without `:` or `\` (e.g. use `-` instead of `:` in timestamps).
  3. Remove the offending files if they are disposable.
  4. On macOS/Windows this is rare; on Linux, run `find <dir> -name *\:* -o -name *\\*` and also check for invalid-UTF-8 names with `find <dir> | grep -P '[^\x00-\x7F]'` before exporting.

Example fix

// before
// artifacts/log:2024-01-01.txt (colon in name) -> export fails
// after
fs::rename("artifacts/log:2024-01-01.txt", "artifacts/log-2024-01-01.txt")?;
Defensive patterns

Strategy: validation

Validate before calling

fn portable_name(name: &std::ffi::OsStr) -> bool {
    name.to_str().map(|s| !s.contains(['\\', ':'])).unwrap_or(false)
}
// scan artifacts: reject any entry whose file_name() fails portable_name

Type guard

fn as_portable_name(name: &std::ffi::OsStr) -> Option<&str> {
    name.to_str().filter(|s| !s.contains(['\\', ':']))
}

Try / catch

match write_session_archive(&session, dir, out, opts) {
    Err(e) if e.to_string().contains("not portable UTF-8") => {
        eprintln!("rename non-portable artifact filenames before export");
    }
    other => other?,
}

Prevention

When it happens

Trigger: The artifacts directory contains a file created with non-UTF-8 raw bytes in its name (e.g. from a tool that wrote locale-raw names on Linux), or a name containing `\` or `:`.

Common situations: Files copied from a FAT/NTFS volume or Windows machine onto a Linux artifacts dir; scripts generating names with timestamps containing `:`; archives extracted with encoding-raw filenames.

Understand the failure class

Background: "invalid id" errors: invalid identifier format — why libraries reject IDs before lookup, and how to fix them — this error's family across 37 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15). Data as JSON: /api/errors/10b3b706c29c32b8. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/session_export.rs:362

    for entry in fs::read_dir(dir)? {
        let entry = entry?;
        *entries += 1;
        if *entries > MAX_ARTIFACT_ENTRIES {
            return Err(io::Error::new(
                io::ErrorKind::InvalidData,
                "artifact tree exceeds export entry limit",
            ));
        }
        let file_type = entry.file_type()?;
        if file_type.is_symlink() {
            continue;
        }
        let child = entry.file_name();
        let child = child
            .to_str()
            .filter(|name| !name.contains(['\\', ':']))
            .ok_or_else(|| {
                io::Error::new(
                    io::ErrorKind::InvalidData,
                    "artifact name is not portable UTF-8",
                )
            })?;
        let member = format!("{prefix}/{child}");
        let path = entry.path();
        let relative = path.strip_prefix(sessions_dir).map_err(io::Error::other)?;
        if file_type.is_dir() {
            // Reject substituted parent directories before descending. The file
            // read repeats confinement checks, so directory races cannot leak bytes.
            WorkspaceFile::open(sessions_dir, &relative.join(".export-root-check"), false)?;
            collect_artifact_files_recursive(
                sessions_dir,
                &path,
                &member,
                depth + 1,
                entries,
                files,

View on GitHub (pinned to 433685b202)