fix: use Zig's numeric parsing in protocol and escape-sequence parsers

Rust's str::parse accepts and rejects different inputs than Zig's
std.fmt.parseInt/parseFloat (underscores, base prefixes, signs). The
config layer already used the Zig ports; now the protocol parsers do
too (call site -> Zig reference -> status in final-Z12.md):
- zig_fmt moves into the leaf crate ghostty-zigfmt (ghostty-vt cannot
  depend on ghostty-os); ghostty-os re-exports it unchanged.
- ghostty-vt: kitty graphics keys, kitty text sizing, OSC 4/5/10-19
  color indices, hex color components and intensities, OSC 9 sleep and
  progress, OSC 133 exit codes, OSC 3008 fields, kitty color palette.
- ghostty-terminal: tmux layout, format variables and pane tabs.
- ghostty-os: URI ports and %XX decoding; ghostty-bench CLI arguments.
Expected values were cross-checked against the real Zig toolchain.

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 12:45:55 +02:00
1 parent c8a85b8806
commit a1ed0cd776
22 files changed
+290 -186

No files matched your search

+19 -16
View File
@@ -41,22 +41,8 @@ pub enum ParseError {
}
fn parse_usize(s: &[u8]) -> Result<usize, ParseError> {
// std.fmt.parseInt(usize, s, 10): optional '+', digits only.
let s = s.strip_prefix(b"+").unwrap_or(s);
if s.is_empty() {
return Err(ParseError::SyntaxError);
}
let mut v: usize = 0;
for &c in s {
if !c.is_ascii_digit() {
return Err(ParseError::SyntaxError);
}
v = v
.checked_mul(10)
.and_then(|v| v.checked_add(usize::from(c - b'0')))
.ok_or(ParseError::SyntaxError)?;
}
Ok(v)
// std.fmt.parseInt(usize, s, 10) (underscores, sign rules).
ghostty_zigfmt::parse_int::<usize>(s, 10).map_err(|_| ParseError::SyntaxError)
}
fn index_of_any(s: &[u8], set: &[u8]) -> Option<usize> {
@@ -191,6 +177,23 @@ impl Checksum {
mod tests {
use super::*;
#[test]
fn zig_parse_int_differences() {
// `std.fmt.parseInt(usize, _, 10)`: `_` separators, `+`/`-0` signs.
let l = p("1_0x2_0,+3,-0,4").unwrap();
assert_eq!((l.width, l.height, l.x, l.y), (10, 20, 3, 0));
assert_eq!(pane(&l), 4);
assert_eq!(p("1_x2,3,4,5").err(), Some(ParseError::SyntaxError));
assert_eq!(p("0x10x2,3,4,5").err(), Some(ParseError::SyntaxError));
assert_eq!(p("10x2,-1,4,5").err(), Some(ParseError::SyntaxError));
assert_eq!(parse_usize(b"1__0"), Ok(10));
assert_eq!(parse_usize(b"_1"), Err(ParseError::SyntaxError));
assert_eq!(
parse_usize(b"99999999999999999999999"),
Err(ParseError::SyntaxError)
);
}
fn p(s: &str) -> Result<Layout, ParseError> {
Layout::parse(s.as_bytes())
}
+22 -29
View File
@@ -55,36 +55,11 @@ pub enum Value {
Str(String),
}
/// Equivalent of `std.fmt.parseInt(usize, s, 10)`.
fn parse_int(s: &str) -> Result<usize, VariableError> {
let b = s.as_bytes();
let (neg, digits) = match b.first() {
Some(b'-') => (true, &b[1..]),
Some(b'+') => (false, &b[1..]),
_ => (false, b),
};
if digits.is_empty() {
return Err(VariableError::InvalidCharacter);
}
let mut v: usize = 0;
let mut overflow = false;
for &c in digits {
if !c.is_ascii_digit() {
return Err(VariableError::InvalidCharacter);
}
match v
.checked_mul(10)
.and_then(|v| v.checked_add(usize::from(c - b'0')))
{
Some(n) => v = n,
None => overflow = true,
}
}
// Negative values (other than zero) overflow an unsigned type.
if overflow || (neg && v != 0) {
return Err(VariableError::Overflow);
}
Ok(v)
ghostty_zigfmt::parse_int::<usize>(s.as_bytes(), 10).map_err(|e| match e {
ghostty_zigfmt::ParseIntError::Overflow => VariableError::Overflow,
ghostty_zigfmt::ParseIntError::InvalidCharacter => VariableError::InvalidCharacter,
})
}
fn parse_prefixed(prefix: u8, value: &str) -> Result<usize, VariableError> {
@@ -320,6 +295,24 @@ mod tests {
use super::*;
#[test]
fn zig_parse_int_differences() {
assert_eq!(parse_int("1_0"), Ok(10));
assert_eq!(parse_int("+5"), Ok(5));
assert_eq!(parse_int("-0"), Ok(0));
assert_eq!(parse_int("-1"), Err(VariableError::Overflow));
assert_eq!(parse_int("0x10"), Err(VariableError::InvalidCharacter));
assert_eq!(parse_int("1_"), Err(VariableError::InvalidCharacter));
assert_eq!(parse_int(""), Err(VariableError::InvalidCharacter));
assert_eq!(
parse_int("99999999999999999999999"),
Err(VariableError::Overflow)
);
assert_eq!(PaneId.parse("%1_2"), Ok(Value::Usize(12)));
assert_eq!(WindowId.parse("@+7"), Ok(Value::Usize(7)));
assert_eq!(WindowWidth.parse("8_0"), Ok(Value::Usize(80)));
}
fn flag(v: Variable) {
assert_eq!(v.parse("1"), Ok(Value::Bool(true)));
assert_eq!(v.parse("0"), Ok(Value::Bool(false)));
+1 -1
View File
@@ -976,7 +976,7 @@ impl Viewer {
let tabs = data.str(Variable::PaneTabs);
if !tabs.is_empty() {
for tab_str in tabs.split(',') {
let Ok(col) = tab_str.parse::<usize>() else {
let Ok(col) = ghostty_zigfmt::parse_int::<usize>(tab_str.as_bytes(), 10) else {
continue;
};
let Some(col_cell) = to_cell(col) else {