{"record":{"id":"c474bbaa456bc42f","repo":"Hmbown/CodeWhale","slug":"artifact-id-and-extension-must-contain-safe-ascii-characters","errorCode":null,"errorMessage":"artifact id and extension must contain safe ASCII characters","messagePattern":"artifact id and extension must contain safe ASCII characters","errorType":"validation","errorClass":"io::Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/artifacts.rs","lineNumber":84,"sourceCode":"\n#[must_use]\npub fn session_artifact_relative_path(artifact_id: &str) -> PathBuf {\n    PathBuf::from(ARTIFACTS_DIR_NAME).join(format!(\"{artifact_id}.txt\"))\n}\n\nfn session_artifact_relative_path_with_extension(\n    artifact_id: &str,\n    extension: &str,\n) -> io::Result<PathBuf> {\n    let artifact_id = sanitize_id_component(artifact_id);\n    let extension = extension.trim_start_matches('.').to_ascii_lowercase();\n    if artifact_id.is_empty()\n        || extension.is_empty()\n        || !extension\n            .chars()\n            .all(|character| character.is_ascii_alphanumeric())\n    {\n        return Err(io::Error::new(\n            io::ErrorKind::InvalidInput,\n            \"artifact id and extension must contain safe ASCII characters\",\n        ));\n    }\n    Ok(PathBuf::from(ARTIFACTS_DIR_NAME).join(format!(\"{artifact_id}.{extension}\")))\n}\n\nfn artifact_sessions_root() -> Option<PathBuf> {\n    #[cfg(test)]\n    if let Some(root) = TEST_ARTIFACT_SESSIONS_ROOT\n        .lock()\n        .unwrap_or_else(|err| err.into_inner())\n        .clone()\n    {\n        return Some(root);\n    }\n\n    // Use the same state-root authority as saved sessions, including an explicit","sourceCodeStart":66,"sourceCodeEnd":102,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/artifacts.rs#L66-L102","documentation":"Session artifacts are written under a path built from the artifact id and extension, so both must be safe ASCII. This error is returned when the artifact id is empty, the extension is empty, or the extension contains a non-alphanumeric-ASCII character, blocking path traversal and unsafe filenames.","triggerScenarios":"Calling `write_session_artifact_bytes` (via `session_artifact_relative_path_with_extension`) with an empty artifact_id, an empty extension, or an extension containing characters like `.`, `/`, spaces, or unicode (e.g. passing \".png\" with the dot included).","commonSituations":"Deriving the extension from a filename with `path.extension()` on a file with no extension; including the leading dot ('.tar.gz'); empty id from an uninitialized record; user-supplied names with special characters.","solutions":["Pass the extension without the leading dot and using only ASCII letters/digits (e.g. \"png\", \"json\").","Ensure the artifact id is a non-empty safe-ASCII identifier before calling.","Validate/normalize inputs at the call site: strip dots, reject or slugify non-alphanumeric characters.","If the source file has no extension, choose a default extension rather than passing an empty string."],"exampleFix":"// before\nlet ext = path.extension().and_then(|e| e.to_str()).unwrap_or(\"\"); // may be empty\nwrite_session_artifact_bytes(session, id, ext, bytes)?;\n\n// after\nlet ext = path.extension().and_then(|e| e.to_str()).unwrap_or(\"txt\");\nlet ext: String = ext.chars().filter(|c| c.is_ascii_alphanumeric()).collect();\nwrite_session_artifact_bytes(session, id, &ext, bytes)?;","handlingStrategy":"validation","validationCode":"fn safe_extension(ext: &str) -> bool {\n    !ext.is_empty() && ext.chars().all(|c| c.is_ascii_alphanumeric())\n}","typeGuard":"fn normalize_ext(ext: &str) -> Option<&str> {\n    let e = ext.trim_start_matches('.');\n    (!e.is_empty() && e.chars().all(|c| c.is_ascii_alphanumeric())).then_some(e)\n}","tryCatchPattern":"if let Err(e) = write_session_artifact_bytes(session, id, ext, bytes) {\n    if e.kind() == std::io::ErrorKind::InvalidInput { /* sanitize id/ext and retry */ }\n}","preventionTips":["Strip the leading dot from filename extensions before passing.","Default to a safe extension (e.g. \"txt\") when the source has none.","Filter extensions to ASCII alphanumerics at the boundary."],"tags":["validation","path-safety","artifacts","input-validation"],"backgroundTag":"invalid-argument-format","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-22T10:30:35.592Z"}