fix: last RV11 gaps - bench enum args, theme NUL paths, Zig error names

Resolves the 3 remaining gaps in docs/rewrite/notes/review-rv11-codex.md
(final-Z14.md):
- ghostty-bench validates every occurrence of enum options in order
  (Args::take_enum; empty value resets) like cli/args.zig, so
  `--mode=bad --mode=noop` fails as in Zig.
- Theme paths with an interior NUL open the prefix like Zig's C-string
  path handling (verified against /usr/bin/ghostty).
- Filesystem diagnostics print Zig's error names (error.FileNotFound,
  NotDir, AccessDenied, IsDir, NameTooLong, SymLinkLoop, ...) via a
  shared ghostty_os::zig_error mapping used by theme/path/config-file/
  file_load/edit-config/list-themes; messages match the oracle.

Test Plan:
- cargo test --workspace --no-fail-fast
- just check-macos (both), just check-windows, just check-wasm
This commit is contained in:
ddidderr committed 2026-10-12 13:54:15 +02:00
1 parent ce9373aa7a
commit f9b333545a
12 files changed
+215 -89

No files matched your search

+63
View File
@@ -0,0 +1,63 @@
//! `std::io::Error` -> Zig error name (`{}` of an error value, e.g.
//! `error.FileNotFound`), following Zig std's posix errno mapping for the
//! calls Ghostty makes (`openat`, `fstat`, `realpath`, `execve`, ...).
/// Zig error name (without the `error.` prefix) for a Linux errno.
#[must_use]
pub fn errno_name(errno: i32) -> &'static str {
match errno {
1 => "PermissionDenied", // EPERM
2 => "FileNotFound", // ENOENT
3 => "ProcessNotFound", // ESRCH
6 | 19 => "NoDevice", // ENXIO, ENODEV
11 => "WouldBlock", // EAGAIN
12 => "SystemResources", // ENOMEM
13 => "AccessDenied", // EACCES
16 => "DeviceBusy", // EBUSY
17 => "PathAlreadyExists", // EEXIST
20 => "NotDir", // ENOTDIR
21 => "IsDir", // EISDIR
23 => "SystemFdQuotaExceeded", // ENFILE
24 => "ProcessFdQuotaExceeded", // EMFILE
26 => "FileBusy", // ETXTBSY
27 | 75 => "FileTooBig", // EFBIG, EOVERFLOW
28 => "NoSpaceLeft", // ENOSPC
30 => "ReadOnlyFileSystem", // EROFS
36 => "NameTooLong", // ENAMETOOLONG
40 => "SymLinkLoop", // ELOOP
84 => "InvalidWtf8", // EILSEQ
95 => "FileLocksNotSupported", // EOPNOTSUPP
_ => "Unexpected",
}
}
/// `error.Name` text for an I/O error.
#[must_use]
pub fn io_error_name(err: &std::io::Error) -> String {
if let Some(errno) = err.raw_os_error() {
return format!("error.{}", errno_name(errno));
}
match err.kind() {
std::io::ErrorKind::NotFound => "error.FileNotFound".into(),
std::io::ErrorKind::PermissionDenied => "error.AccessDenied".into(),
std::io::ErrorKind::InvalidInput => "error.BadPathName".into(),
_ => "error.Unexpected".into(),
}
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
#[test]
fn names() {
let n = |e| io_error_name(&std::io::Error::from_raw_os_error(e));
assert_eq!(n(2), "error.FileNotFound");
assert_eq!(n(20), "error.NotDir");
assert_eq!(n(13), "error.AccessDenied");
assert_eq!(n(21), "error.IsDir");
assert_eq!(n(36), "error.NameTooLong");
assert_eq!(n(40), "error.SymLinkLoop");
assert_eq!(n(9999), "error.Unexpected");
}
}