{"record":{"id":"10b3b706c29c32b8","repo":"Hmbown/CodeWhale","slug":"artifact-name-is-not-portable-utf-8","errorCode":null,"errorMessage":"artifact name is not portable UTF-8","messagePattern":"artifact name is not portable UTF-8","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/session_export.rs","lineNumber":362,"sourceCode":"    for entry in fs::read_dir(dir)? {\n        let entry = entry?;\n        *entries += 1;\n        if *entries > MAX_ARTIFACT_ENTRIES {\n            return Err(io::Error::new(\n                io::ErrorKind::InvalidData,\n                \"artifact tree exceeds export entry limit\",\n            ));\n        }\n        let file_type = entry.file_type()?;\n        if file_type.is_symlink() {\n            continue;\n        }\n        let child = entry.file_name();\n        let child = child\n            .to_str()\n            .filter(|name| !name.contains(['\\\\', ':']))\n            .ok_or_else(|| {\n                io::Error::new(\n                    io::ErrorKind::InvalidData,\n                    \"artifact name is not portable UTF-8\",\n                )\n            })?;\n        let member = format!(\"{prefix}/{child}\");\n        let path = entry.path();\n        let relative = path.strip_prefix(sessions_dir).map_err(io::Error::other)?;\n        if file_type.is_dir() {\n            // Reject substituted parent directories before descending. The file\n            // read repeats confinement checks, so directory races cannot leak bytes.\n            WorkspaceFile::open(sessions_dir, &relative.join(\".export-root-check\"), false)?;\n            collect_artifact_files_recursive(\n                sessions_dir,\n                &path,\n                &member,\n                depth + 1,\n                entries,\n                files,","sourceCodeStart":344,"sourceCodeEnd":380,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/session_export.rs#L344-L380","documentation":"Artifact file names must be portable UTF-8 and must not contain backslash or colon characters. When a directory entry's name is not valid UTF-8 or contains those characters, the walk aborts with InvalidData so archives stay portable across filesystems and platforms.","triggerScenarios":"A file or directory under artifacts/ has a non-UTF-8 (raw byte) name, or its name contains '\\\\' or ':' — often from files created on another OS or by tools writing raw bytes.","commonSituations":"Files copied from Windows with names like 'C:\\temp\\log.txt' baked in; files created on Linux with invalid UTF-8 byte sequences; NFC/NFD or encoding issues from network transfers.","solutions":["Rename the offending artifact file to a portable UTF-8 name without '\\\\' or ':' and re-export.","Identify offenders with: find <artifacts_dir> -name '*[\\\\:]*' and with ls | grep -P '[^\\x00-\\x7F]'.","If files came from another platform, normalize names during copy-in rather than at export time.","Fix the tool that created non-portable names so future sessions export cleanly."],"exampleFix":"// before\nartifacts/logs/'C:\\temp\\build.log'\n// after\n$ mv 'artifacts/logs/C:\\temp\\build.log' 'artifacts/logs/C-temp-build.log'","handlingStrategy":"validation","validationCode":"fn portable_name(name: &std::ffi::OsStr) -> bool {\n    name.to_str()\n        .map(|s| !s.contains('\\\\') && !s.contains(':'))\n        .unwrap_or(false)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Sanitize file names to ASCII/UTF-8 at write time, not at export time.","Strip drive letters and separators when ingesting files from Windows.","Enforce a name allowlist (alnum, '-', '_', '.') for artifact writers."],"tags":["filesystem","encoding","utf-8"],"backgroundTag":"invalid-identifier-format","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}