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 ecd67c8f3e
commit 16eb1fba61
22 files changed
+290 -186

No files matched your search

+23 -15
View File
@@ -89,9 +89,10 @@ fn percent_decode(s: &str) -> Cow<'_, [u8]> {
while i < b.len() {
if b[i] == b'%'
&& i + 2 < b.len()
&& let (Some(h), Some(l)) = (hexval(b[i + 1]), hexval(b[i + 2]))
// `std.fmt.parseInt(u8, "XX", 16)`: accepts a sign and `_`.
&& let Ok(v) = crate::zig_fmt::parse_int::<u8>(&b[i + 1..i + 3], 16)
{
out.push((h << 4) | l);
out.push(v);
i += 3;
continue;
}
@@ -101,15 +102,6 @@ fn percent_decode(s: &str) -> Cow<'_, [u8]> {
Cow::Owned(out)
}
fn hexval(c: u8) -> Option<u8> {
match c {
b'0'..=b'9' => Some(c - b'0'),
b'a'..=b'f' => Some(c - b'a' + 10),
b'A'..=b'F' => Some(c - b'A' + 10),
_ => None,
}
}
/// `std.Uri`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Uri<'a> {
@@ -217,8 +209,7 @@ fn parse_authority<'a>(authority: &'a str, uri: &mut Uri<'a>) -> Result<(), Pars
// if not part of the V6 address field
end_of_host = end_of_host.min(index);
uri.port = Some(
authority[index + 1..]
.parse::<u16>()
crate::zig_fmt::parse_int::<u16>(&authority.as_bytes()[index + 1..], 10)
.map_err(|_| ParseError::InvalidPort)?,
);
}
@@ -228,8 +219,7 @@ fn parse_authority<'a>(authority: &'a str, uri: &mut Uri<'a>) -> Result<(), Pars
// if not part of the userinfo field
end_of_host = end_of_host.min(index);
uri.port = Some(
authority[index + 1..]
.parse::<u16>()
crate::zig_fmt::parse_int::<u16>(&authority.as_bytes()[index + 1..], 10)
.map_err(|_| ParseError::InvalidPort)?,
);
}
@@ -411,6 +401,24 @@ fn is_valid_mac_address(s: &str) -> bool {
mod tests {
use super::*;
#[test]
fn zig_parse_int_port_and_percent() {
let u = parse("http://host:8_0/p", ParseOptions::default()).unwrap();
assert_eq!(u.port, Some(80));
let u = parse("http://host:+80/p", ParseOptions::default()).unwrap();
assert_eq!(u.port, Some(80));
assert!(parse("http://host:0x50/p", ParseOptions::default()).is_err());
assert!(parse("http://host:65536/p", ParseOptions::default()).is_err());
assert!(parse("http://host:-1/p", ParseOptions::default()).is_err());
// `%XX` is decoded with `parseInt(u8, XX, 16)`: sign and nothing else.
let u = parse(
"file://localhost/a%+Ab%-0c%_1d%1_e%41",
ParseOptions::default(),
)
.unwrap();
assert_eq!(&*u.path.to_raw_bytes(), b"/a\x0ab\x00c%_1d%1_eA");
}
const MAC: ParseOptions = ParseOptions {
mac_address: true,
raw_path: false,