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 64aa8b6793
commit 4bbe0d7fe9
12 files changed
+215 -89

No files matched your search

+36
View File
@@ -122,6 +122,27 @@ impl Args {
})
}
/// Take an enum value (`parseIntoField` on an enum): every occurrence is
/// validated against `variants` in order (a bad value is an error even if
/// a later one is fine); an empty value resets to `default`.
pub fn take_enum<T: Copy>(
&mut self,
key: &str,
default: T,
variants: &[(&str, T)],
) -> Result<T, String> {
self.apply(key, default, |v| {
let v = v.ok_or_else(|| format!("option --{key} requires a value"))?;
let bad = || format!("invalid value for --{key}: {}", v.to_string_lossy());
let v = v.to_str().ok_or_else(bad)?;
variants
.iter()
.find(|(n, _)| *n == v)
.map(|(_, t)| *t)
.ok_or_else(bad)
})
}
/// Take a file path value (any bytes are allowed).
pub fn take_opt_path(&mut self, key: &str) -> Result<Option<PathBuf>, String> {
self.apply(key, None, |v| {
@@ -197,6 +218,21 @@ mod tests {
assert!(a.take("n", 0u16).is_err());
}
#[test]
fn take_enum_validates_every_occurrence() {
let t = |v: &[&str]| {
Args::parse(v.iter().map(OsString::from))
.unwrap()
.take_enum("mode", 0u8, &[("noop", 0), ("table", 1)])
};
assert!(t(&["--mode=bad", "--mode=noop"]).is_err());
assert!(t(&["--mode=noop", "--mode=bad"]).is_err());
assert!(t(&["--mode=bad", "--mode="]).is_err());
assert_eq!(t(&["--mode=table", "--mode="]).unwrap(), 0);
assert_eq!(t(&["--mode=noop", "--mode=table"]).unwrap(), 1);
assert_eq!(t(&[]).unwrap(), 0);
}
#[test]
fn duplicates_apply_in_order_and_first_error_wins() {
let a = |v: &[&str]| Args::parse(v.iter().map(OsString::from)).unwrap();
+40 -28
View File
@@ -104,11 +104,7 @@ impl TerminalStream {
let rows = args.take("terminal-rows", 80u16)?;
let cols = args.take("terminal-cols", 120u16)?;
let data = args.take_opt_path("data")?;
let full = match args.take_opt_string("handler")?.as_deref() {
None | Some("print") => false,
Some("full") => true,
Some(m) => return Err(format!("invalid value for --handler: {m}")),
};
let full = args.take_enum("handler", false, &[("print", false), ("full", true)])?;
let t = Terminal::new(TermOptions::new(cols, rows)).map_err(|e| format!("{e:?}"))?;
Ok(TerminalStream {
data: Data::new(data),
@@ -259,13 +255,16 @@ pub struct CodepointWidth {
impl CodepointWidth {
pub fn create(args: &mut Args) -> std::result::Result<Self, String> {
let mode = match args.take_opt_string("mode")?.as_deref() {
None | Some("noop") => WidthMode::Noop,
Some("wcwidth") => WidthMode::Wcwidth,
Some("simd") => WidthMode::Simd,
Some("table") => WidthMode::Table,
Some(m) => return Err(format!("invalid value for --mode: {m}")),
};
let mode = args.take_enum(
"mode",
WidthMode::Noop,
&[
("noop", WidthMode::Noop),
("wcwidth", WidthMode::Wcwidth),
("simd", WidthMode::Simd),
("table", WidthMode::Table),
],
)?;
Ok(CodepointWidth {
data: Data::new(args.take_opt_path("data")?),
mode,
@@ -384,11 +383,7 @@ pub struct GraphemeBreak {
impl GraphemeBreak {
pub fn create(args: &mut Args) -> std::result::Result<Self, String> {
let noop = match args.take_opt_string("mode")?.as_deref() {
None | Some("table") => false,
Some("noop") => true,
Some(m) => return Err(format!("invalid value for --mode: {m}")),
};
let noop = args.take_enum("mode", false, &[("noop", true), ("table", false)])?;
Ok(GraphemeBreak {
data: Data::new(args.take_opt_path("data")?),
noop,
@@ -473,11 +468,11 @@ pub fn is_symbol_computed(cp: u32) -> bool {
impl IsSymbol {
pub fn create(args: &mut Args) -> std::result::Result<Self, String> {
let mode = match args.take_opt_string("mode")?.as_deref() {
None | Some("uucode") => SymbolMode::Uucode,
Some("table") => SymbolMode::Table,
Some(m) => return Err(format!("invalid value for --mode: {m}")),
};
let mode = args.take_enum(
"mode",
SymbolMode::Uucode,
&[("uucode", SymbolMode::Uucode), ("table", SymbolMode::Table)],
)?;
Ok(IsSymbol {
data: Data::new(args.take_opt_path("data")?),
mode,
@@ -536,12 +531,15 @@ pub struct ScreenClone {
impl ScreenClone {
pub fn create(args: &mut Args) -> std::result::Result<Self, String> {
let mode = match args.take_opt_string("mode")?.as_deref() {
None | Some("clone") => CloneMode::Clone,
Some("noop") => CloneMode::Noop,
Some("render") => CloneMode::Render,
Some(m) => return Err(format!("invalid value for --mode: {m}")),
};
let mode = args.take_enum(
"mode",
CloneMode::Clone,
&[
("clone", CloneMode::Clone),
("noop", CloneMode::Noop),
("render", CloneMode::Render),
],
)?;
let rows = args.take("terminal-rows", 80u16)?;
let cols = args.take("terminal-cols", 120u16)?;
let data = args.take_opt_path("data")?;
@@ -718,6 +716,20 @@ mod tests {
let _ = std::fs::remove_file(p);
}
#[test]
fn duplicate_bad_enum_modes_fail() {
let mk = |v: &[&str]| Args::parse(v.iter().map(|s| (*s).to_owned())).unwrap();
for v in [
&["--mode=bad", "--mode=noop"][..],
&["--mode=bad", "--mode="][..],
] {
assert!(CodepointWidth::create(&mut mk(v)).is_err());
assert!(GraphemeBreak::create(&mut mk(v)).is_err());
assert!(IsSymbol::create(&mut mk(v)).is_err());
assert!(ScreenClone::create(&mut mk(v)).is_err());
}
}
#[test]
fn is_symbol_modes_run() {
let p = sample_file("sym");
@@ -40,11 +40,14 @@ fn run() -> Result<(), String> {
}
"osc" => {
let p_valid = args.take("p-valid", 0.5f64)?;
let style = match args.take_opt_string("style")?.as_deref() {
None | Some("streaming") => OscStyle::Streaming,
Some("parser") => OscStyle::Parser,
Some(s) => return Err(format!("invalid value for --style: {s}")),
};
let style = args.take_enum(
"style",
OscStyle::Streaming,
&[
("streaming", OscStyle::Streaming),
("parser", OscStyle::Parser),
],
)?;
args.finish()?;
run_osc(&mut out, &mut rng, p_valid, style)
}