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:
1 parent
c8a85b8806
commit
a1ed0cd776
22 files changed
+290
-186
No files matched your search
@@ -11,6 +11,7 @@ flate2 = "1.1.10"
|
||||
ghostty-datastruct.workspace = true
|
||||
ghostty-unicode.workspace = true
|
||||
ghostty-vt.workspace = true
|
||||
ghostty-zigfmt.workspace = true
|
||||
log = "0.4.34"
|
||||
png = "0.18.1"
|
||||
thiserror = "2.0.21"
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user