ogham/exa · error · io::Error
Error: path somehow contained a NUL?
Error message
Error: path somehow contained a NUL?
What it means
This io::Error comes from exa's extended-attribute module (src/fs/feature/xattr.rs). Before calling the OS listxattr functions, exa converts the file path to a C string; the conversion is rejected when path.to_str() returns None (the path is not valid UTF-8) or when CString::new finds an interior NUL byte. The message names only the NUL case, so it is misleading: on Unix a NUL byte cannot appear in a real filename, and in practice this error almost always means the filename contained non-UTF-8 bytes. It is returned as an Err of kind ErrorKind::Other, not a panic, so the caller sees it as a failed attributes() / list_attrs() call.
Source
Thrown at src/fs/feature/xattr.rs:64
No,
}
/// Extended attribute
#[derive(Debug, Clone)]
pub struct Attribute {
pub name: String,
pub size: usize,
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
pub fn list_attrs(lister: &lister::Lister, path: &Path) -> io::Result<Vec<Attribute>> {
use std::ffi::CString;
let c_path = match path.to_str().and_then(|s| CString::new(s).ok()) {
Some(cstring) => cstring,
None => {
return Err(io::Error::new(io::ErrorKind::Other, "Error: path somehow contained a NUL?"));
}
};
let bufsize = lister.listxattr_first(&c_path);
match bufsize.cmp(&0) {
Ordering::Less => return Err(io::Error::last_os_error()),
Ordering::Equal => return Ok(Vec::new()),
Ordering::Greater => {},
}
let mut buf = vec![0_u8; bufsize as usize];
let err = lister.listxattr_second(&c_path, &mut buf, bufsize);
match err.cmp(&0) {
Ordering::Less => return Err(io::Error::last_os_error()),
Ordering::Equal => return Ok(Vec::new()),
Ordering::Greater => {},
}View on GitHub (pinned to 3d1edbb470)
Solutions
- If you are the library consumer: validate or fix the filename first. Rename the offending file to a valid UTF-8 name, or run exa in a locale/terminal that handles the bytes, e.g. with LC_ALL=C or a UTF-8 locale so the name round-trips.
- If you hit this as a user of the exa binary: drop --extended for that directory, or rename the file (find it with: find . -name '*[! -~]*' or ls -b to show escapes).
- If you maintain this code: build the CString from the raw OS bytes instead of a &str, so only real NUL bytes fail. Use std::os::unix::ffi::OsStrExt::as_bytes, and return io::ErrorKind::InvalidInput with an accurate message distinguishing the non-UTF-8 and NUL cases.
- As a defensive change, map the failure to the OS convention: io::Error::from_raw_os_error(libc::EINVAL) so callers can match on the error kind.
Example fix
// before (src/fs/feature/xattr.rs)
let c_path = match path.to_str().and_then(|s| CString::new(s).ok()) {
Some(cstring) => cstring,
None => return Err(io::Error::new(io::ErrorKind::Other, "Error: path somehow contained a NUL?")),
};
// after: use the raw OS bytes; only a real NUL byte can fail now
#[cfg(unix)]
let c_path = {
use std::os::unix::ffi::OsStrExt;
match CString::new(path.as_os_str().as_bytes()) {
Ok(cstring) => cstring,
Err(_) => return Err(io::Error::new(io::ErrorKind::InvalidInput,
"path contains an interior NUL byte")),
}
}; Defensive patterns
Strategy: validation
Validate before calling
// Before calling attributes()/symlink_attributes() on a user-supplied path:
use std::ffi::OsStr;
use std::path::Path;
fn check_path_for_xattrs(path: &Path) -> Result<(), std::io::Error> {
// Case 1: not valid UTF-8 (the common real-world trigger for this message)
if path.to_str().is_none() {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput,
format!("path is not valid UTF-8: {:?}", path.as_os_str())));
}
// Case 2: an actual interior NUL byte (only possible in synthetic paths)
if path.to_str().map_or(false, |s| s.contains('\0')) {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput,
"path contains an interior NUL byte"));
}
Ok(())
} Type guard
fn is_xattr_listable(path: &Path) -> bool {
// Mirror of the check in src/fs/feature/xattr.rs list_attrs():
// the CString conversion must succeed.
path.to_str().map_or(false, |s| !s.contains('\0'))
} Try / catch
// Treat attribute listing as optional metadata, not a fatal error:
match path.attributes() {
Ok(attrs) => { /* render xattrs */ }
Err(e) if e.kind() == std::io::ErrorKind::Other
&& e.to_string().contains("NUL") => {
// non-UTF-8 or NUL-containing path: skip extended attributes for this file
}
Err(e) => { /* report or skip */ }
} Prevention
- Normalize or reject non-UTF-8 filenames at the boundary where your program accepts paths (argument parsing, directory iteration).
- When iterating directories, work with OsString/Path end to end; never round-trip through String, which is what makes this conversion fail.
- If you list arbitrary user files, wrap each file's attribute lookup in its own error handling so one odd filename cannot stop the whole listing.
- In your own wrappers, convert paths with std::os::unix::ffi::OsStrExt::as_bytes instead of to_str(); that removes the non-UTF-8 failure mode entirely.
When it happens
Trigger: Calling Path::attributes() or Path::symlink_attributes() (the FileAttributes trait), or running exa with --extended/--all with xattrs enabled, on a Linux/macOS system where a listed filename has bytes that are not valid UTF-8 (for example latin-1 names or a byte sequence that is invalid UTF-8). The same match arm also fires for a path that truly contains an embedded NUL, which essentially only happens for programmatically built paths, not real directory entries.
Common situations: Listing old archives, media directories, or files created by non-UTF-8 programs (Windows latin-1 names copied via SMB, misconfigured locale). Also triggered when a wrapper script builds paths from unvalidated input. Note the source loses the original bytes: it goes through path.to_str(), so any non-UTF-8 path is reported with the wrong 'NUL' message.
Related errors
AI-assisted analysis of ogham/exa@3d1edbb470 (2026-08-16).
Data as JSON: /api/errors/02280ecba9c28e30.
Report an issue: GitHub.