fix(config): match Zig's numeric grammar, NUL handling and diagnostics
Resolves the 6 differential gap groups in docs/rewrite/notes/review- rv9-codex.md (details in final-Z11.md): - ghostty_os::zig_fmt ports std.fmt.parseInt/parseUnsigned/parseFloat (underscores, 0x/0o/0b prefixes, hex floats, inf/nan) with the Zig std tests and replaces every int/float parse in config, keybind parameters, ssh-cache and desktop detection. - Interior NUL no longer panics in font names and ends C strings like Zig's backing bytes (fontconfig, C config getters, title actions, clipboard callback, relative paths, config-file, shaders, background image, GTK CSS). - GTK new-window arguments are trimmed of ASCII whitespace only. - Command tokenization uses a port of std.process.ArgIteratorGeneral (incl. CR/LF separators) with its std tests. - UTF-8 validation happens where Zig's field parsers do it, so error kinds match (metric modifiers, quick-terminal size, link, ...). A scripted differential run of 202 fields x ~220 hostile/valid values against /usr/bin/ghostty (+validate-config and +show-config: exit code, stdout, stderr) now has zero mismatches besides build-specific defaults (theme resources path, auto-update-channel, gtk-opengl-debug); it also surfaced and fixed background-blur failure state, NaN formatting and NaN/-0 finalize clamps. Regression tests: ghostty-config/tests/ zig_parity.rs. Test Plan: - cargo test --workspace --no-fail-fast (3456 passed, exit 0) - just check-macos (both), just check-windows, just check-wasm
This commit is contained in:
1 parent
61e7a680ac
commit
a57ff41d34
26 files changed
+1835
-228
No files matched your search
@@ -1138,12 +1138,13 @@ impl Application {
|
|||||||
}
|
}
|
||||||
if let Some(v) = s.strip_prefix("--working-directory=") {
|
if let Some(v) = s.strip_prefix("--working-directory=") {
|
||||||
// D-Bus strings are plain UTF-8, including literal U+FDD0.
|
// D-Bus strings are plain UTF-8, including literal U+FDD0.
|
||||||
working_directory =
|
working_directory = Some(ghostty_os::osbytes::EncodedOs::from_plain_text(
|
||||||
Some(ghostty_os::osbytes::EncodedOs::from_plain_text(v.trim()));
|
trim_ascii_whitespace(v),
|
||||||
|
));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Some(v) = s.strip_prefix("--title=") {
|
if let Some(v) = s.strip_prefix("--title=") {
|
||||||
title = Some(v.trim().to_owned());
|
title = Some(trim_ascii_whitespace(v).to_owned());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1869,3 +1870,30 @@ impl Drop for ReadyGuard {
|
|||||||
fn scopeguard_ready() -> ReadyGuard {
|
fn scopeguard_ready() -> ReadyGuard {
|
||||||
ReadyGuard
|
ReadyGuard
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `std.mem.trim(u8, v, &std.ascii.whitespace)`: only ASCII whitespace
|
||||||
|
/// (space, `\t`, `\n`, VT, FF, `\r`) is trimmed, never Unicode whitespace
|
||||||
|
/// such as U+00A0.
|
||||||
|
fn trim_ascii_whitespace(v: &str) -> &str {
|
||||||
|
v.trim_matches(|c: char| matches!(c, ' ' | '\t' | '\n' | '\r' | '\x0b' | '\x0c'))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod trim_tests {
|
||||||
|
use super::trim_ascii_whitespace;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn new_window_arguments_trim_only_ascii_whitespace() {
|
||||||
|
assert_eq!(
|
||||||
|
trim_ascii_whitespace(" \t\n\r\x0b\x0c/tmp/a b \n"),
|
||||||
|
"/tmp/a b"
|
||||||
|
);
|
||||||
|
assert_eq!(trim_ascii_whitespace("/tmp/name\u{a0}"), "/tmp/name\u{a0}");
|
||||||
|
assert_eq!(
|
||||||
|
trim_ascii_whitespace("\u{a0}title\u{2003} "),
|
||||||
|
"\u{a0}title\u{2003}"
|
||||||
|
);
|
||||||
|
assert_eq!(trim_ascii_whitespace("\x0btitle\x0b"), "title");
|
||||||
|
assert_eq!(trim_ascii_whitespace(" \t "), "");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -110,8 +110,8 @@ impl CssProviders {
|
|||||||
|
|
||||||
for p in &config.gtk_custom_css.value {
|
for p in &config.gtk_custom_css.value {
|
||||||
let (path, optional) = match p {
|
let (path, optional) = match p {
|
||||||
Path::Optional(path) => (path.to_path(), true),
|
Path::Optional(path) => (path.to_syscall_path(), true),
|
||||||
Path::Required(path) => (path.to_path(), false),
|
Path::Required(path) => (path.to_syscall_path(), false),
|
||||||
};
|
};
|
||||||
let path = match path {
|
let path = match path {
|
||||||
Ok(path) => path,
|
Ok(path) => path,
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ impl Entry {
|
|||||||
let hostname = iter.next()?;
|
let hostname = iter.next()?;
|
||||||
let timestamp_str = iter.next()?;
|
let timestamp_str = iter.next()?;
|
||||||
let terminfo_version = iter.next().unwrap_or("xterm-ghostty");
|
let terminfo_version = iter.next().unwrap_or("xterm-ghostty");
|
||||||
let timestamp = match timestamp_str.parse::<i64>() {
|
let timestamp = match ghostty_os::zig_fmt::parse_int::<i64>(timestamp_str.as_bytes(), 10) {
|
||||||
Ok(t) => t,
|
Ok(t) => t,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!("Invalid timestamp in cache entry: {timestamp_str} err={err}");
|
log::warn!("Invalid timestamp in cache entry: {timestamp_str} err={err}");
|
||||||
|
|||||||
@@ -506,6 +506,42 @@ pub fn parse_packed_struct_with(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Zig's `@max` on floats: a NaN operand yields the other operand and
|
||||||
|
/// `max(+0, -0)` is `+0`.
|
||||||
|
#[must_use]
|
||||||
|
pub fn zig_max(a: f64, b: f64) -> f64 {
|
||||||
|
if a.is_nan() {
|
||||||
|
b
|
||||||
|
} else if b.is_nan() {
|
||||||
|
a
|
||||||
|
} else if a == b {
|
||||||
|
if a.is_sign_negative() { b } else { a }
|
||||||
|
} else {
|
||||||
|
a.max(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Zig's `@min` on floats: a NaN operand yields the other operand and
|
||||||
|
/// `min(+0, -0)` is `-0`.
|
||||||
|
#[must_use]
|
||||||
|
pub fn zig_min(a: f64, b: f64) -> f64 {
|
||||||
|
if a.is_nan() {
|
||||||
|
b
|
||||||
|
} else if b.is_nan() {
|
||||||
|
a
|
||||||
|
} else if a == b {
|
||||||
|
if a.is_sign_negative() { a } else { b }
|
||||||
|
} else {
|
||||||
|
a.min(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `std.math.clamp` on floats (`@max(lower, @min(val, upper))`).
|
||||||
|
#[must_use]
|
||||||
|
pub fn zig_clamp(val: f64, lower: f64, upper: f64) -> f64 {
|
||||||
|
zig_max(lower, zig_min(val, upper))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn parse_bool(v: &str) -> Result<bool, ArgError> {
|
pub fn parse_bool(v: &str) -> Result<bool, ArgError> {
|
||||||
match v {
|
match v {
|
||||||
"1" | "t" | "T" | "true" => Ok(true),
|
"1" | "t" | "T" | "true" => Ok(true),
|
||||||
@@ -514,42 +550,6 @@ pub fn parse_bool(v: &str) -> Result<bool, ArgError> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse an integer like Zig's `std.fmt.parseInt(T, s, 0)`: optional sign,
|
|
||||||
/// optional `0x`/`0o`/`0b` prefix, underscores between digits allowed.
|
|
||||||
pub fn parse_int_base0(s: &str) -> Option<i128> {
|
|
||||||
let (neg, rest) = match s.as_bytes().first()? {
|
|
||||||
b'-' => (true, &s[1..]),
|
|
||||||
b'+' => (false, &s[1..]),
|
|
||||||
_ => (false, s),
|
|
||||||
};
|
|
||||||
let (base, digits) = if rest.len() >= 2 && rest.as_bytes()[0] == b'0' {
|
|
||||||
match rest.as_bytes()[1] {
|
|
||||||
b'x' => (16, &rest[2..]),
|
|
||||||
b'o' => (8, &rest[2..]),
|
|
||||||
b'b' => (2, &rest[2..]),
|
|
||||||
_ => (10, rest),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(10, rest)
|
|
||||||
};
|
|
||||||
if digits.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let db = digits.as_bytes();
|
|
||||||
if db[0] == b'_' || db[db.len() - 1] == b'_' || digits.contains("__") {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let mut acc: i128 = 0;
|
|
||||||
for &c in db {
|
|
||||||
if c == b'_' {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let d = i128::from((c as char).to_digit(base)?);
|
|
||||||
acc = acc.checked_mul(i128::from(base))?.checked_add(d)?;
|
|
||||||
}
|
|
||||||
Some(if neg { -acc } else { acc })
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
// ConfigField impls for primitives
|
// ConfigField impls for primitives
|
||||||
|
|
||||||
@@ -580,10 +580,9 @@ macro_rules! impl_int_field {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn parse_bytes(&mut self, value: Option<&[u8]>) -> Result<(), ArgError> {
|
fn parse_bytes(&mut self, value: Option<&[u8]>) -> Result<(), ArgError> {
|
||||||
let value = crate::cli_args::text_value(value)?;
|
|
||||||
let v = value.ok_or(ArgError::ValueRequired)?;
|
let v = value.ok_or(ArgError::ValueRequired)?;
|
||||||
let n = parse_int_base0(v).ok_or(ArgError::InvalidValue)?;
|
*self = ghostty_os::zig_fmt::parse_int::<$t>(v, 0)
|
||||||
*self = <$t>::try_from(n).map_err(|_| ArgError::InvalidValue)?;
|
.map_err(|_| ArgError::InvalidValue)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -615,14 +614,14 @@ macro_rules! impl_float_field {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn parse_bytes(&mut self, value: Option<&[u8]>) -> Result<(), ArgError> {
|
fn parse_bytes(&mut self, value: Option<&[u8]>) -> Result<(), ArgError> {
|
||||||
let value = crate::cli_args::text_value(value)?;
|
|
||||||
let v = value.ok_or(ArgError::ValueRequired)?;
|
let v = value.ok_or(ArgError::ValueRequired)?;
|
||||||
*self = v.parse::<$t>().map_err(|_| ArgError::InvalidValue)?;
|
*self = ghostty_os::zig_fmt::parse_float::<$t>(v)
|
||||||
|
.map_err(|_| ArgError::InvalidValue)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_entry(&self, f: &mut EntryFormatter<'_>) -> fmt::Result {
|
fn format_entry(&self, f: &mut EntryFormatter<'_>) -> fmt::Result {
|
||||||
writeln!(f.writer, "{} = {}", f.name, self)
|
writeln!(f.writer, "{} = {}", f.name, ghostty_os::zig_fmt::format_float_d(*self))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)*};
|
)*};
|
||||||
@@ -1642,12 +1641,45 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_int_base0_forms() {
|
fn numeric_fields_use_zig_parse_int_and_parse_float_grammars() {
|
||||||
assert_eq!(parse_int_base0("0x_1"), None);
|
let mut n = 0u32;
|
||||||
assert_eq!(parse_int_base0("1_000"), Some(1000));
|
n.parse_bytes(Some(b"0X10")).unwrap();
|
||||||
assert_eq!(parse_int_base0("-0b11"), Some(-3));
|
assert_eq!(n, 16);
|
||||||
assert_eq!(parse_int_base0("0o17"), Some(15));
|
n.parse_bytes(Some(b"1__0")).unwrap();
|
||||||
assert_eq!(parse_int_base0(""), None);
|
assert_eq!(n, 10);
|
||||||
|
n.parse_bytes(Some(b"+0b1_1")).unwrap();
|
||||||
|
assert_eq!(n, 3);
|
||||||
|
for bad in ["0x_1", "_1", "1_", "-1", "0x", "", " 1", "1 ", "\u{e9}"] {
|
||||||
|
assert_eq!(
|
||||||
|
n.parse_bytes(Some(bad.as_bytes())),
|
||||||
|
Err(ArgError::InvalidValue),
|
||||||
|
"{bad:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert_eq!(n.parse_bytes(None), Err(ArgError::ValueRequired));
|
||||||
|
let mut i = 0i16;
|
||||||
|
i.parse_bytes(Some(b"-0O17")).unwrap();
|
||||||
|
assert_eq!(i, -15);
|
||||||
|
assert_eq!(i.parse_bytes(Some(b"40000")), Err(ArgError::InvalidValue));
|
||||||
|
// Invalid UTF-8 is just an invalid character for numbers.
|
||||||
|
assert_eq!(n.parse_bytes(Some(b"\xff")), Err(ArgError::InvalidValue));
|
||||||
|
|
||||||
|
let mut f = 0f32;
|
||||||
|
f.parse_bytes(Some(b"1_4")).unwrap();
|
||||||
|
assert_eq!(f, 14.0);
|
||||||
|
f.parse_bytes(Some(b"0x1.cp+3")).unwrap();
|
||||||
|
assert_eq!(f, 14.0);
|
||||||
|
f.parse_bytes(Some(b"-INF")).unwrap();
|
||||||
|
assert_eq!(f, f32::NEG_INFINITY);
|
||||||
|
f.parse_bytes(Some(b"nan")).unwrap();
|
||||||
|
assert!(f.is_nan());
|
||||||
|
for bad in ["1__4", "infx", ".", "1e", " 1", "0x"] {
|
||||||
|
assert_eq!(
|
||||||
|
f.parse_bytes(Some(bad.as_bytes())),
|
||||||
|
Err(ArgError::InvalidValue),
|
||||||
|
"{bad:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -63,55 +63,14 @@ impl Iterator for ArgIterator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Best-effort shell string splitter matching Zig's
|
/// Zig's `std.process.ArgIteratorGeneral(.{})`.
|
||||||
/// `std.process.ArgIteratorGeneral(.{})` (whitespace separated, double
|
|
||||||
/// quotes group, backslashes only special before a double quote).
|
|
||||||
fn split_general(b: &[u8]) -> Vec<EncodedOs> {
|
fn split_general(b: &[u8]) -> Vec<EncodedOs> {
|
||||||
let mut out = vec![];
|
ghostty_os::arg_iterator::ArgIteratorGeneral::new(
|
||||||
let mut i = 0;
|
ghostty_os::arg_iterator::Options::default(),
|
||||||
loop {
|
b,
|
||||||
// skip whitespace
|
)
|
||||||
while i < b.len() && (b[i] == b' ' || b[i] == b'\t') {
|
.map(|arg| EncodedOs::from_bytes(&arg))
|
||||||
i += 1;
|
.collect()
|
||||||
}
|
|
||||||
if i >= b.len() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut cur: Vec<u8> = vec![];
|
|
||||||
let mut quoted = false;
|
|
||||||
while i < b.len() {
|
|
||||||
match b[i] {
|
|
||||||
b' ' | b'\t' if !quoted => break,
|
|
||||||
b'\\' => {
|
|
||||||
let mut n = 0;
|
|
||||||
while i < b.len() && b[i] == b'\\' {
|
|
||||||
n += 1;
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
if i < b.len() && b[i] == b'"' {
|
|
||||||
cur.extend(std::iter::repeat_n(b'\\', n / 2));
|
|
||||||
if n % 2 == 1 {
|
|
||||||
cur.push(b'"');
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cur.extend(std::iter::repeat_n(b'\\', n));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b'"' => {
|
|
||||||
quoted = !quoted;
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
c => {
|
|
||||||
cur.push(c);
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out.push(EncodedOs::from_bytes(&cur));
|
|
||||||
}
|
|
||||||
out
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Command {
|
impl Command {
|
||||||
@@ -259,6 +218,13 @@ mod tests {
|
|||||||
assert_eq!(args, ["echo", "hello world", "a\"b"]);
|
assert_eq!(args, ["echo", "hello world", "a\"b"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn command_arg_iterator_shell_splits_cr_lf_tab_like_zig() {
|
||||||
|
let v = Command::Shell(EncodedOs::from_bytes(b"bash\n-i\r\tx \"a\nb\"\0ignored"));
|
||||||
|
let args: Vec<EncodedOs> = v.arg_iterator().collect();
|
||||||
|
assert_eq!(args, ["bash", "-i", "x", "a\nb"]);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn command_arg_iterator_direct() {
|
fn command_arg_iterator_direct() {
|
||||||
let v = Command::Direct(vec!["echo".into(), "hello world".into()]);
|
let v = Command::Direct(vec!["echo".into(), "hello world".into()]);
|
||||||
|
|||||||
@@ -4224,7 +4224,7 @@ impl Config {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let file = match path.to_path().and_then(std::fs::File::open) {
|
let file = match path.to_syscall_path().and_then(std::fs::File::open) {
|
||||||
Ok(f) => f,
|
Ok(f) => f,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
if err.kind() != std::io::ErrorKind::NotFound || !optional {
|
if err.kind() != std::io::ErrorKind::NotFound || !optional {
|
||||||
@@ -4564,16 +4564,19 @@ impl Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clamp our mouse scroll multiplier
|
// Clamp our mouse scroll multiplier
|
||||||
|
// (`@min(10_000, @max(0.01, v))`: NaN becomes the minimum.)
|
||||||
|
let zmin = cli_args::zig_min;
|
||||||
|
let zmax = cli_args::zig_max;
|
||||||
self.mouse_scroll_multiplier.precision =
|
self.mouse_scroll_multiplier.precision =
|
||||||
self.mouse_scroll_multiplier.precision.clamp(0.01, 10_000.0);
|
zmin(10_000.0, zmax(0.01, self.mouse_scroll_multiplier.precision));
|
||||||
self.mouse_scroll_multiplier.discrete =
|
self.mouse_scroll_multiplier.discrete =
|
||||||
self.mouse_scroll_multiplier.discrete.clamp(0.01, 10_000.0);
|
zmin(10_000.0, zmax(0.01, self.mouse_scroll_multiplier.discrete));
|
||||||
|
|
||||||
// Clamp our split opacity
|
// Clamp our split opacity
|
||||||
self.unfocused_split_opacity = self.unfocused_split_opacity.clamp(0.15, 1.0);
|
self.unfocused_split_opacity = zmin(1.0, zmax(0.15, self.unfocused_split_opacity));
|
||||||
|
|
||||||
// Clamp our contrast
|
// Clamp our contrast
|
||||||
self.minimum_contrast = self.minimum_contrast.clamp(1.0, 21.0);
|
self.minimum_contrast = zmin(21.0, zmax(1.0, self.minimum_contrast));
|
||||||
|
|
||||||
// Minimum window size
|
// Minimum window size
|
||||||
if self.window_width > 0 {
|
if self.window_width > 0 {
|
||||||
@@ -4606,7 +4609,7 @@ impl Config {
|
|||||||
self.auto_update_channel = Some(ReleaseChannel::Stable);
|
self.auto_update_channel = Some(ReleaseChannel::Stable);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.faint_opacity = self.faint_opacity.clamp(0.0, 1.0);
|
self.faint_opacity = cli_args::zig_clamp(self.faint_opacity, 0.0, 1.0);
|
||||||
|
|
||||||
// Finalize key remapping set for efficient lookups
|
// Finalize key remapping set for efficient lookups
|
||||||
self.key_remap.finalize();
|
self.key_remap.finalize();
|
||||||
@@ -4753,14 +4756,37 @@ impl Iterator for ChangeIterator<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Name of an IO error like Zig's error names (best effort), used in
|
/// Name of an IO error like Zig's `{}` formatting of an error value
|
||||||
/// user-visible diagnostics.
|
/// (`error.FileNotFound`), used in user-visible diagnostics.
|
||||||
fn zig_io_error_name(err: &std::io::Error) -> String {
|
fn zig_io_error_name(err: &std::io::Error) -> String {
|
||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
|
#[cfg(unix)]
|
||||||
|
if let Some(errno) = err.raw_os_error() {
|
||||||
|
let name = match errno {
|
||||||
|
2 => Some("FileNotFound"),
|
||||||
|
13 => Some("AccessDenied"),
|
||||||
|
1 => Some("PermissionDenied"),
|
||||||
|
21 => Some("IsDir"),
|
||||||
|
20 => Some("NotDir"),
|
||||||
|
40 => Some("SymLinkLoop"),
|
||||||
|
36 => Some("NameTooLong"),
|
||||||
|
24 => Some("ProcessFdQuotaExceeded"),
|
||||||
|
23 => Some("SystemFdQuotaExceeded"),
|
||||||
|
12 => Some("SystemResources"),
|
||||||
|
19 => Some("NoDevice"),
|
||||||
|
16 => Some("DeviceBusy"),
|
||||||
|
26 => Some("FileBusy"),
|
||||||
|
30 => Some("ReadOnlyFileSystem"),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
if let Some(name) = name {
|
||||||
|
return format!("error.{name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
match err.kind() {
|
match err.kind() {
|
||||||
ErrorKind::NotFound => "FileNotFound".to_string(),
|
ErrorKind::NotFound => "error.FileNotFound".to_string(),
|
||||||
ErrorKind::PermissionDenied => "AccessDenied".to_string(),
|
ErrorKind::PermissionDenied => "error.AccessDenied".to_string(),
|
||||||
_ => err.to_string(),
|
_ => "error.Unexpected".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,13 +30,18 @@ impl MetricModifier {
|
|||||||
/// Parses the modifier value. If the value ends in "%" it is assumed
|
/// Parses the modifier value. If the value ends in "%" it is assumed
|
||||||
/// to be a percent, otherwise the value is parsed as an integer.
|
/// to be a percent, otherwise the value is parsed as an integer.
|
||||||
pub fn parse(input: &str) -> Result<MetricModifier, ArgError> {
|
pub fn parse(input: &str) -> Result<MetricModifier, ArgError> {
|
||||||
|
Self::parse_bytes(input.as_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`MetricModifier::parse`] over the original bytes (Zig never
|
||||||
|
/// validates UTF-8; every failure is `InvalidFormat`).
|
||||||
|
pub fn parse_bytes(input: &[u8]) -> Result<MetricModifier, ArgError> {
|
||||||
if input.is_empty() {
|
if input.is_empty() {
|
||||||
return Err(ArgError::other("InvalidFormat"));
|
return Err(ArgError::other("InvalidFormat"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(num) = input.strip_suffix('%') {
|
if let Some(num) = input.strip_suffix(b"%") {
|
||||||
let mut percent = num
|
let mut percent = ghostty_os::zig_fmt::parse_float::<f64>(num)
|
||||||
.parse::<f64>()
|
|
||||||
.map_err(|_| ArgError::other("InvalidFormat"))?;
|
.map_err(|_| ArgError::other("InvalidFormat"))?;
|
||||||
percent /= 100.0;
|
percent /= 100.0;
|
||||||
|
|
||||||
@@ -46,15 +51,14 @@ impl MetricModifier {
|
|||||||
return Ok(MetricModifier::Percent(1.0 + percent));
|
return Ok(MetricModifier::Percent(1.0 + percent));
|
||||||
}
|
}
|
||||||
|
|
||||||
input
|
ghostty_os::zig_fmt::parse_int::<i32>(input, 10)
|
||||||
.parse::<i32>()
|
|
||||||
.map(MetricModifier::Absolute)
|
.map(MetricModifier::Absolute)
|
||||||
.map_err(|_| ArgError::other("InvalidFormat"))
|
.map_err(|_| ArgError::other("InvalidFormat"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// So it works with the config framework.
|
/// So it works with the config framework.
|
||||||
pub fn parse_cli_value(input: Option<&str>) -> Result<MetricModifier, ArgError> {
|
pub fn parse_cli_value(input: Option<&[u8]>) -> Result<MetricModifier, ArgError> {
|
||||||
Self::parse(input.ok_or(ArgError::ValueRequired)?)
|
Self::parse_bytes(input.ok_or(ArgError::ValueRequired)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply a modifier to an integer value (saturating, clamped at zero for
|
/// Apply a modifier to an integer value (saturating, clamped at zero for
|
||||||
@@ -100,14 +104,16 @@ impl MetricModifier {
|
|||||||
|
|
||||||
impl ConfigField for MetricModifier {
|
impl ConfigField for MetricModifier {
|
||||||
fn parse_bytes(&mut self, value: Option<&[u8]>) -> Result<(), ArgError> {
|
fn parse_bytes(&mut self, value: Option<&[u8]>) -> Result<(), ArgError> {
|
||||||
let value = crate::cli_args::text_value(value)?;
|
|
||||||
*self = Self::parse_cli_value(value)?;
|
*self = Self::parse_cli_value(value)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_entry(&self, f: &mut EntryFormatter<'_>) -> fmt::Result {
|
fn format_entry(&self, f: &mut EntryFormatter<'_>) -> fmt::Result {
|
||||||
let s = match self {
|
let s = match self {
|
||||||
MetricModifier::Percent(v) => format!("{}%", (v - 1.0) * 100.0),
|
MetricModifier::Percent(v) => format!(
|
||||||
|
"{}%",
|
||||||
|
ghostty_os::zig_fmt::format_float_d((v - 1.0) * 100.0)
|
||||||
|
),
|
||||||
MetricModifier::Absolute(v) => format!("{v}"),
|
MetricModifier::Absolute(v) => format!("{v}"),
|
||||||
};
|
};
|
||||||
f.format_entry(&s)
|
f.format_entry(&s)
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ impl Path {
|
|||||||
// Make sure the base directory can be opened.
|
// Make sure the base directory can be opened.
|
||||||
std::fs::read_dir(&base_path)?;
|
std::fs::read_dir(&base_path)?;
|
||||||
|
|
||||||
let full = base_path.join(path.to_path()?);
|
let full = base_path.join(path.to_syscall_path()?);
|
||||||
let abs = match std::fs::canonicalize(&full) {
|
let abs = match std::fs::canonicalize(&full) {
|
||||||
Ok(p) => EncodedOs::from_path(&p),
|
Ok(p) => EncodedOs::from_path(&p),
|
||||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||||
@@ -392,6 +392,31 @@ mod tests {
|
|||||||
assert!(diags.is_empty());
|
assert!(diags.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn expand_interior_nul_resolves_the_c_string_prefix_like_zig() {
|
||||||
|
let tmp = ghostty_os::TempDir::init().unwrap();
|
||||||
|
let base = std::fs::canonicalize(tmp.path()).unwrap();
|
||||||
|
std::fs::write(base.join("exists"), b"x").unwrap();
|
||||||
|
|
||||||
|
// The syscall sees only the prefix, which exists: the real path of
|
||||||
|
// the prefix replaces the value.
|
||||||
|
let mut p = Path::Required(EncodedOs::from_bytes(b"exists\0tail"));
|
||||||
|
let mut diags = DiagnosticList::default();
|
||||||
|
p.expand(&EncodedOs::from_path(&base), &mut diags).unwrap();
|
||||||
|
assert_eq!(diags.items().len(), 0);
|
||||||
|
assert_eq!(p.path().to_path().unwrap(), base.join("exists"));
|
||||||
|
|
||||||
|
// The prefix is missing: the whole value is resolved lexically.
|
||||||
|
let mut p = Path::Required(EncodedOs::from_bytes(b"missing\0tail"));
|
||||||
|
p.expand(&EncodedOs::from_path(&base), &mut diags).unwrap();
|
||||||
|
assert_eq!(diags.items().len(), 0);
|
||||||
|
assert_eq!(
|
||||||
|
p.path().to_bytes(),
|
||||||
|
[base.as_os_str().as_encoded_bytes(), b"/missing\0tail"].concat()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
#[test]
|
#[test]
|
||||||
fn expand_symlink_to_non_utf8_target_round_trips() {
|
fn expand_symlink_to_non_utf8_target_round_trips() {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use ghostty_vt::color::{DEFAULT as DEFAULT_PALETTE, Palette as TermPalette, Pale
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
c_value::CValue,
|
c_value::CValue,
|
||||||
cli_args::{ArgError, ConfigField, parse_int_base0, trim_ws},
|
cli_args::{ArgError, ConfigField, trim_ws},
|
||||||
clipboard_codepoint_map::{ClipboardCodepointMap, Entry as ClipboardEntry, Replacement},
|
clipboard_codepoint_map::{ClipboardCodepointMap, Entry as ClipboardEntry, Replacement},
|
||||||
config_enum,
|
config_enum,
|
||||||
config_packed,
|
config_packed,
|
||||||
@@ -242,11 +242,18 @@ impl Color {
|
|||||||
|
|
||||||
/// Zig: `Color.parseCLI(input)` (static form returning a new value).
|
/// Zig: `Color.parseCLI(input)` (static form returning a new value).
|
||||||
pub fn from_cli(input: Option<&str>) -> Result<Color, ArgError> {
|
pub fn from_cli(input: Option<&str>) -> Result<Color, ArgError> {
|
||||||
|
Color::from_cli_bytes(input.map(str::as_bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`Color::from_cli`] over the original bytes: Zig never validates
|
||||||
|
/// UTF-8 here, so a stray byte is an `InvalidCharacter` (or
|
||||||
|
/// `InvalidValue` for a wrong length), not a generic invalid value.
|
||||||
|
pub fn from_cli_bytes(input: Option<&[u8]>) -> Result<Color, ArgError> {
|
||||||
let input = input.ok_or(ArgError::ValueRequired)?;
|
let input = input.ok_or(ArgError::ValueRequired)?;
|
||||||
// Trim any whitespace before processing
|
// Trim any whitespace before processing
|
||||||
let trimmed = trim_ws(input);
|
let trimmed = crate::cli_args::trim_bytes(input);
|
||||||
|
|
||||||
if let Some(rgb) = ghostty_vt::x11_color::get(trimmed) {
|
if let Some(rgb) = ghostty_vt::x11_color::get_bytes(trimmed) {
|
||||||
return Ok(Color {
|
return Ok(Color {
|
||||||
r: rgb.r,
|
r: rgb.r,
|
||||||
g: rgb.g,
|
g: rgb.g,
|
||||||
@@ -254,7 +261,7 @@ impl Color {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Color::from_hex(trimmed)
|
Color::from_hex_bytes(trimmed)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Format the color as a string.
|
/// Format the color as a string.
|
||||||
@@ -266,8 +273,13 @@ impl Color {
|
|||||||
/// fromHex parses a color from a hex value such as #RRGGBB. The "#"
|
/// fromHex parses a color from a hex value such as #RRGGBB. The "#"
|
||||||
/// is optional.
|
/// is optional.
|
||||||
pub fn from_hex(input: &str) -> Result<Color, ArgError> {
|
pub fn from_hex(input: &str) -> Result<Color, ArgError> {
|
||||||
|
Color::from_hex_bytes(input.as_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`Color::from_hex`] over bytes.
|
||||||
|
pub fn from_hex_bytes(input: &[u8]) -> Result<Color, ArgError> {
|
||||||
// Trim the beginning '#' if it exists
|
// Trim the beginning '#' if it exists
|
||||||
let trimmed = input.strip_prefix('#').unwrap_or(input).as_bytes();
|
let trimmed = input.strip_prefix(b"#").unwrap_or(input);
|
||||||
if trimmed.len() != 6 && trimmed.len() != 3 {
|
if trimmed.len() != 6 && trimmed.len() != 3 {
|
||||||
return Err(ArgError::InvalidValue);
|
return Err(ArgError::InvalidValue);
|
||||||
}
|
}
|
||||||
@@ -282,7 +294,7 @@ impl Color {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let digit = |c: u8| -> Result<u8, ArgError> {
|
let digit = |c: u8| -> Result<u8, ArgError> {
|
||||||
(c as char)
|
char::from(c)
|
||||||
.to_digit(16)
|
.to_digit(16)
|
||||||
.map(|d| d as u8)
|
.map(|d| d as u8)
|
||||||
.ok_or_else(|| ArgError::other("InvalidCharacter"))
|
.ok_or_else(|| ArgError::other("InvalidCharacter"))
|
||||||
@@ -308,8 +320,7 @@ impl ConfigField for Color {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn parse_bytes(&mut self, input: Option<&[u8]>) -> Result<(), ArgError> {
|
fn parse_bytes(&mut self, input: Option<&[u8]>) -> Result<(), ArgError> {
|
||||||
let input = crate::cli_args::text_value(input)?;
|
*self = Color::from_cli_bytes(input)?;
|
||||||
*self = Color::from_cli(input)?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,11 +347,16 @@ impl Default for TerminalColor {
|
|||||||
impl TerminalColor {
|
impl TerminalColor {
|
||||||
/// Zig: `TerminalColor.parseCLI(input)`.
|
/// Zig: `TerminalColor.parseCLI(input)`.
|
||||||
pub fn from_cli(input: Option<&str>) -> Result<TerminalColor, ArgError> {
|
pub fn from_cli(input: Option<&str>) -> Result<TerminalColor, ArgError> {
|
||||||
|
TerminalColor::from_cli_bytes(input.map(str::as_bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`TerminalColor::from_cli`] over bytes.
|
||||||
|
pub fn from_cli_bytes(input: Option<&[u8]>) -> Result<TerminalColor, ArgError> {
|
||||||
let input = input.ok_or(ArgError::ValueRequired)?;
|
let input = input.ok_or(ArgError::ValueRequired)?;
|
||||||
match input {
|
match input {
|
||||||
"cell-foreground" => Ok(TerminalColor::CellForeground),
|
b"cell-foreground" => Ok(TerminalColor::CellForeground),
|
||||||
"cell-background" => Ok(TerminalColor::CellBackground),
|
b"cell-background" => Ok(TerminalColor::CellBackground),
|
||||||
_ => Ok(TerminalColor::Color(Color::from_cli(Some(input))?)),
|
_ => Ok(TerminalColor::Color(Color::from_cli_bytes(Some(input))?)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,8 +371,7 @@ impl TerminalColor {
|
|||||||
|
|
||||||
impl ConfigField for TerminalColor {
|
impl ConfigField for TerminalColor {
|
||||||
fn parse_bytes(&mut self, input: Option<&[u8]>) -> Result<(), ArgError> {
|
fn parse_bytes(&mut self, input: Option<&[u8]>) -> Result<(), ArgError> {
|
||||||
let input = crate::cli_args::text_value(input)?;
|
*self = TerminalColor::from_cli_bytes(input)?;
|
||||||
*self = TerminalColor::from_cli(input)?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,18 +400,22 @@ impl Default for BoldColor {
|
|||||||
impl BoldColor {
|
impl BoldColor {
|
||||||
/// Zig: `BoldColor.parseCLI(input)`.
|
/// Zig: `BoldColor.parseCLI(input)`.
|
||||||
pub fn from_cli(input: Option<&str>) -> Result<BoldColor, ArgError> {
|
pub fn from_cli(input: Option<&str>) -> Result<BoldColor, ArgError> {
|
||||||
|
BoldColor::from_cli_bytes(input.map(str::as_bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`BoldColor::from_cli`] over bytes.
|
||||||
|
pub fn from_cli_bytes(input: Option<&[u8]>) -> Result<BoldColor, ArgError> {
|
||||||
let input = input.ok_or(ArgError::ValueRequired)?;
|
let input = input.ok_or(ArgError::ValueRequired)?;
|
||||||
if input == "bright" {
|
if input == b"bright" {
|
||||||
return Ok(BoldColor::Bright);
|
return Ok(BoldColor::Bright);
|
||||||
}
|
}
|
||||||
Ok(BoldColor::Color(Color::from_cli(Some(input))?))
|
Ok(BoldColor::Color(Color::from_cli_bytes(Some(input))?))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigField for BoldColor {
|
impl ConfigField for BoldColor {
|
||||||
fn parse_bytes(&mut self, input: Option<&[u8]>) -> Result<(), ArgError> {
|
fn parse_bytes(&mut self, input: Option<&[u8]>) -> Result<(), ArgError> {
|
||||||
let input = crate::cli_args::text_value(input)?;
|
*self = BoldColor::from_cli_bytes(input)?;
|
||||||
*self = BoldColor::from_cli(input)?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,7 +438,6 @@ impl ConfigField for ColorList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn parse_bytes(&mut self, input: Option<&[u8]>) -> Result<(), ArgError> {
|
fn parse_bytes(&mut self, input: Option<&[u8]>) -> Result<(), ArgError> {
|
||||||
let input = crate::cli_args::text_value(input)?;
|
|
||||||
let input = input.ok_or(ArgError::ValueRequired)?;
|
let input = input.ok_or(ArgError::ValueRequired)?;
|
||||||
if input.is_empty() {
|
if input.is_empty() {
|
||||||
return Err(ArgError::ValueRequired);
|
return Err(ArgError::ValueRequired);
|
||||||
@@ -430,15 +448,15 @@ impl ConfigField for ColorList {
|
|||||||
|
|
||||||
// Split the input by commas and parse each color
|
// Split the input by commas and parse each color
|
||||||
let mut count = 0usize;
|
let mut count = 0usize;
|
||||||
for raw in input.split(',').filter(|s| !s.is_empty()) {
|
for raw in input.split(|b| *b == b',').filter(|s| !s.is_empty()) {
|
||||||
count += 1;
|
count += 1;
|
||||||
if count > 64 {
|
if count > 64 {
|
||||||
return Err(ArgError::InvalidValue);
|
return Err(ArgError::InvalidValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trim whitespace from each color value
|
// Trim whitespace from each color value
|
||||||
let trimmed = trim_ws(raw);
|
let trimmed = crate::cli_args::trim_bytes(raw);
|
||||||
let color = Color::from_cli(Some(trimmed))?;
|
let color = Color::from_cli_bytes(Some(trimmed))?;
|
||||||
self.colors.push(color);
|
self.colors.push(color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -488,21 +506,26 @@ impl ConfigField for Palette {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn parse_bytes(&mut self, input: Option<&[u8]>) -> Result<(), ArgError> {
|
fn parse_bytes(&mut self, input: Option<&[u8]>) -> Result<(), ArgError> {
|
||||||
let input = crate::cli_args::text_value(input)?;
|
|
||||||
let value = input.ok_or(ArgError::ValueRequired)?;
|
let value = input.ok_or(ArgError::ValueRequired)?;
|
||||||
let eql_idx = value.find('=').ok_or(ArgError::InvalidValue)?;
|
let eql_idx = value
|
||||||
|
.iter()
|
||||||
|
.position(|b| *b == b'=')
|
||||||
|
.ok_or(ArgError::InvalidValue)?;
|
||||||
|
|
||||||
// Parse the key part (trim whitespace)
|
// Parse the key part (trim whitespace)
|
||||||
let key_str = trim_ws(&value[..eql_idx]);
|
let key_str = crate::cli_args::trim_bytes(&value[..eql_idx]);
|
||||||
let key = match parse_int_base0(key_str) {
|
let key = match ghostty_os::zig_fmt::parse_int::<u8>(key_str, 0) {
|
||||||
None => return Err(ArgError::other("InvalidCharacter")),
|
Ok(n) => usize::from(n),
|
||||||
Some(n) if n < 0 => return Err(ArgError::other("InvalidCharacter")),
|
Err(ghostty_os::zig_fmt::ParseIntError::InvalidCharacter) => {
|
||||||
Some(n) if n > 255 => return Err(ArgError::other("Overflow")),
|
return Err(ArgError::other("InvalidCharacter"));
|
||||||
Some(n) => n as usize,
|
}
|
||||||
|
Err(ghostty_os::zig_fmt::ParseIntError::Overflow) => {
|
||||||
|
return Err(ArgError::other("Overflow"));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Parse the color part (Color parsing will handle whitespace)
|
// Parse the color part (Color parsing will handle whitespace)
|
||||||
let rgb = Color::from_cli(Some(&value[eql_idx + 1..]))?;
|
let rgb = Color::from_cli_bytes(Some(&value[eql_idx + 1..]))?;
|
||||||
self.value[key] = rgb.to_terminal_rgb();
|
self.value[key] = rgb.to_terminal_rgb();
|
||||||
self.mask.set(key);
|
self.mask.set(key);
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -698,7 +721,8 @@ impl ConfigField for RepeatableFontVariation {
|
|||||||
let key: &[u8; 4] = key.try_into().map_err(|_| ArgError::InvalidValue)?;
|
let key: &[u8; 4] = key.try_into().map_err(|_| ArgError::InvalidValue)?;
|
||||||
self.list.push(Variation {
|
self.list.push(Variation {
|
||||||
id: VariationId::init(key),
|
id: VariationId::init(key),
|
||||||
value: value.parse::<f64>().map_err(|_| ArgError::InvalidValue)?,
|
value: ghostty_os::zig_fmt::parse_float::<f64>(value.as_bytes())
|
||||||
|
.map_err(|_| ArgError::InvalidValue)?,
|
||||||
});
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -861,8 +885,7 @@ impl<'a> UnicodeRangeParser<'a> {
|
|||||||
|
|
||||||
/// `std.fmt.parseInt(u21, s, 16)` for plain hex digit strings.
|
/// `std.fmt.parseInt(u21, s, 16)` for plain hex digit strings.
|
||||||
fn parse_u21_hex(s: &[u8]) -> Option<u32> {
|
fn parse_u21_hex(s: &[u8]) -> Option<u32> {
|
||||||
let s = std::str::from_utf8(s).ok()?;
|
let v = ghostty_os::zig_fmt::parse_int::<u32>(s, 16).ok()?;
|
||||||
let v = u32::from_str_radix(s, 16).ok()?;
|
|
||||||
(v <= 0x1f_ffff).then_some(v)
|
(v <= 0x1f_ffff).then_some(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1054,7 +1077,6 @@ pub struct RepeatableLink {
|
|||||||
|
|
||||||
impl ConfigField for RepeatableLink {
|
impl ConfigField for RepeatableLink {
|
||||||
fn parse_bytes(&mut self, _input: Option<&[u8]>) -> Result<(), ArgError> {
|
fn parse_bytes(&mut self, _input: Option<&[u8]>) -> Result<(), ArgError> {
|
||||||
let _input = crate::cli_args::text_value(_input)?;
|
|
||||||
Err(ArgError::other("NotImplemented"))
|
Err(ArgError::other("NotImplemented"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -413,7 +413,8 @@ impl ConfigField for MouseScrollMultiplier {
|
|||||||
match r {
|
match r {
|
||||||
Ok(()) => *self = result,
|
Ok(()) => *self = result,
|
||||||
Err(ArgError::InvalidValue) => {
|
Err(ArgError::InvalidValue) => {
|
||||||
let v: f64 = input.parse().map_err(|_| ArgError::InvalidValue)?;
|
let v = ghostty_os::zig_fmt::parse_float::<f64>(input.as_bytes())
|
||||||
|
.map_err(|_| ArgError::InvalidValue)?;
|
||||||
*self = MouseScrollMultiplier {
|
*self = MouseScrollMultiplier {
|
||||||
precision: v,
|
precision: v,
|
||||||
discrete: v,
|
discrete: v,
|
||||||
@@ -427,7 +428,11 @@ impl ConfigField for MouseScrollMultiplier {
|
|||||||
fn format_entry(&self, f: &mut EntryFormatter<'_>) -> fmt::Result {
|
fn format_entry(&self, f: &mut EntryFormatter<'_>) -> fmt::Result {
|
||||||
fmt_str(
|
fmt_str(
|
||||||
f,
|
f,
|
||||||
&format!("precision:{},discrete:{}", self.precision, self.discrete),
|
&format!(
|
||||||
|
"precision:{},discrete:{}",
|
||||||
|
ghostty_os::zig_fmt::format_float_d(self.precision),
|
||||||
|
ghostty_os::zig_fmt::format_float_d(self.discrete)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -537,19 +542,21 @@ impl QuickTerminalSizeValue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse(input: &str) -> Result<Self, ArgError> {
|
pub fn parse(input: &[u8]) -> Result<Self, ArgError> {
|
||||||
if input.is_empty() {
|
if input.is_empty() {
|
||||||
return Err(ArgError::ValueRequired);
|
return Err(ArgError::ValueRequired);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(num) = input.strip_suffix("px") {
|
if let Some(num) = input.strip_suffix(b"px") {
|
||||||
return Ok(Self::Pixels(
|
return Ok(Self::Pixels(
|
||||||
num.parse::<u32>().map_err(|_| ArgError::InvalidValue)?,
|
ghostty_os::zig_fmt::parse_int::<u32>(num, 10)
|
||||||
|
.map_err(|_| ArgError::InvalidValue)?,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(num) = input.strip_suffix('%') {
|
if let Some(num) = input.strip_suffix(b"%") {
|
||||||
let percentage = num.parse::<f32>().map_err(|_| ArgError::InvalidValue)?;
|
let percentage =
|
||||||
|
ghostty_os::zig_fmt::parse_float::<f32>(num).map_err(|_| ArgError::InvalidValue)?;
|
||||||
if percentage < 0.0 {
|
if percentage < 0.0 {
|
||||||
return Err(ArgError::InvalidValue);
|
return Err(ArgError::InvalidValue);
|
||||||
}
|
}
|
||||||
@@ -630,15 +637,16 @@ impl ConfigField for QuickTerminalSize {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn parse_bytes(&mut self, input: Option<&[u8]>) -> Result<(), ArgError> {
|
fn parse_bytes(&mut self, input: Option<&[u8]>) -> Result<(), ArgError> {
|
||||||
let input = crate::cli_args::text_value(input)?;
|
|
||||||
let input = input.ok_or(ArgError::ValueRequired)?;
|
let input = input.ok_or(ArgError::ValueRequired)?;
|
||||||
let mut it = input.split(',');
|
let mut it = input.split(|b| *b == b',');
|
||||||
|
|
||||||
let primary = trim_ws(it.next().ok_or(ArgError::ValueRequired)?);
|
let primary = crate::cli_args::trim_bytes(it.next().ok_or(ArgError::ValueRequired)?);
|
||||||
self.primary = Some(QuickTerminalSizeValue::parse(primary)?);
|
self.primary = Some(QuickTerminalSizeValue::parse(primary)?);
|
||||||
|
|
||||||
self.secondary = match it.next() {
|
self.secondary = match it.next() {
|
||||||
Some(s) => Some(QuickTerminalSizeValue::parse(trim_ws(s))?),
|
Some(s) => Some(QuickTerminalSizeValue::parse(crate::cli_args::trim_bytes(
|
||||||
|
s,
|
||||||
|
))?),
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -824,15 +832,17 @@ impl ConfigField for BackgroundBlur {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn parse_bytes(&mut self, input: Option<&[u8]>) -> Result<(), ArgError> {
|
fn parse_bytes(&mut self, input: Option<&[u8]>) -> Result<(), ArgError> {
|
||||||
let input = crate::cli_args::text_value(input)?;
|
|
||||||
let Some(input) = input else {
|
let Some(input) = input else {
|
||||||
// Emulate behavior for bools
|
// Emulate behavior for bools
|
||||||
*self = BackgroundBlur::True;
|
*self = BackgroundBlur::True;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
// Only the bool/enum spellings need text; everything else is a
|
||||||
|
// `parseInt` over the raw bytes.
|
||||||
|
let text = std::str::from_utf8(input).unwrap_or("\u{fffd}");
|
||||||
|
|
||||||
// Try to parse normal bools
|
// Try to parse normal bools
|
||||||
if let Ok(b) = parse_bool(input) {
|
if let Ok(b) = parse_bool(text) {
|
||||||
*self = if b {
|
*self = if b {
|
||||||
BackgroundBlur::True
|
BackgroundBlur::True
|
||||||
} else {
|
} else {
|
||||||
@@ -842,7 +852,7 @@ impl ConfigField for BackgroundBlur {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Try to parse enums (only void variants can be parsed this way)
|
// Try to parse enums (only void variants can be parsed this way)
|
||||||
match input {
|
match text {
|
||||||
"macos-glass-regular" => {
|
"macos-glass-regular" => {
|
||||||
*self = BackgroundBlur::MacosGlassRegular;
|
*self = BackgroundBlur::MacosGlassRegular;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -854,11 +864,20 @@ impl ConfigField for BackgroundBlur {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut r = 0u8;
|
// Zig writes `self.* = .{ .radius = parseInt(...) catch return ... }`
|
||||||
r.parse_cli(Some(input))
|
// in place: the union tag is switched to `radius` before the payload
|
||||||
.map_err(|_| ArgError::InvalidValue)?;
|
// is computed, so a failed parse leaves a `radius` variant behind
|
||||||
|
// (observable in the oracle: an invalid value formats as `0`).
|
||||||
|
let mut r = match *self {
|
||||||
|
BackgroundBlur::Radius(v) => v,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
let parsed = ghostty_os::zig_fmt::parse_int::<u8>(input, 0);
|
||||||
|
if let Ok(v) = parsed {
|
||||||
|
r = v;
|
||||||
|
}
|
||||||
*self = BackgroundBlur::Radius(r);
|
*self = BackgroundBlur::Radius(r);
|
||||||
Ok(())
|
parsed.map(|_| ()).map_err(|_| ArgError::InvalidValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_entry(&self, f: &mut EntryFormatter<'_>) -> fmt::Result {
|
fn format_entry(&self, f: &mut EntryFormatter<'_>) -> fmt::Result {
|
||||||
@@ -1028,14 +1047,7 @@ pub const DURATION_UNITS: &[(&str, u64)] = &[
|
|||||||
|
|
||||||
/// `std.fmt.parseUnsigned(u64, s, 10)`.
|
/// `std.fmt.parseUnsigned(u64, s, 10)`.
|
||||||
fn parse_unsigned_u64(s: &[u8]) -> Option<u64> {
|
fn parse_unsigned_u64(s: &[u8]) -> Option<u64> {
|
||||||
if s.is_empty() || !s.iter().all(u8::is_ascii_digit) {
|
ghostty_os::zig_fmt::parse_unsigned::<u64>(s, 10).ok()
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let mut acc: u64 = 0;
|
|
||||||
for &c in s {
|
|
||||||
acc = acc.checked_mul(10)?.checked_add(u64::from(c - b'0'))?;
|
|
||||||
}
|
|
||||||
Some(acc)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Duration {
|
impl Duration {
|
||||||
@@ -1201,7 +1213,10 @@ pub struct WindowPadding {
|
|||||||
impl WindowPadding {
|
impl WindowPadding {
|
||||||
pub fn parse(input: Option<&str>) -> Result<WindowPadding, ArgError> {
|
pub fn parse(input: Option<&str>) -> Result<WindowPadding, ArgError> {
|
||||||
let input = input.ok_or(ArgError::ValueRequired)?;
|
let input = input.ok_or(ArgError::ValueRequired)?;
|
||||||
let p = |s: &str| s.parse::<u32>().map_err(|_| ArgError::InvalidValue);
|
let p = |s: &str| {
|
||||||
|
ghostty_os::zig_fmt::parse_int::<u32>(s.as_bytes(), 10)
|
||||||
|
.map_err(|_| ArgError::InvalidValue)
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(idx) = input.find(',') {
|
if let Some(idx) = input.find(',') {
|
||||||
let left = p(trim_ws(&input[..idx]))?;
|
let left = p(trim_ws(&input[..idx]))?;
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
//! Parity regressions found by differential runs against Ghostty 1.3.1:
|
||||||
|
//! numeric grammars, field-specific error kinds for non-UTF-8 input and
|
||||||
|
//! float formatting.
|
||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
use ghostty_config::{cli_args::ArgError, config::Config};
|
||||||
|
|
||||||
|
fn parse(key: &str, value: &[u8]) -> Result<Config, ArgError> {
|
||||||
|
let mut config = Config::default();
|
||||||
|
config.parse_field_bytes(key, Some(value))?;
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kind(key: &str, value: &[u8]) -> String {
|
||||||
|
parse(key, value).map_or_else(|e| e.to_string(), |_| "ok".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn numeric_grammars_accept_zig_spellings() {
|
||||||
|
assert_eq!(parse("window-height", b"0X10").unwrap().window_height, 16);
|
||||||
|
assert_eq!(parse("window-height", b"1__0").unwrap().window_height, 10);
|
||||||
|
assert_eq!(parse("window-height", b"0b1_0").unwrap().window_height, 2);
|
||||||
|
assert_eq!(parse("font-size", b"1_4").unwrap().font_size, 14.0);
|
||||||
|
assert_eq!(parse("font-size", b"0x1.cp+3").unwrap().font_size, 14.0);
|
||||||
|
assert_eq!(
|
||||||
|
parse("window-padding-x", b"1_0")
|
||||||
|
.unwrap()
|
||||||
|
.window_padding_x
|
||||||
|
.top_left,
|
||||||
|
10
|
||||||
|
);
|
||||||
|
assert_eq!(kind("window-padding-x", b"0x10"), "InvalidValue");
|
||||||
|
assert_eq!(kind("window-height", b"1_"), "InvalidValue");
|
||||||
|
assert_eq!(kind("font-size", b"1__0"), "InvalidValue");
|
||||||
|
// Decimal-only places keep rejecting prefixes.
|
||||||
|
assert_eq!(kind("adjust-cell-width", b"0x10"), "InvalidFormat");
|
||||||
|
assert_eq!(kind("adjust-cell-width", b"1_0"), "ok");
|
||||||
|
assert_eq!(kind("adjust-cell-width", b"0x1p3%"), "ok");
|
||||||
|
assert_eq!(kind("quick-terminal-size", b"1_0px"), "ok");
|
||||||
|
assert_eq!(kind("quick-terminal-size", b"0x1p3%,1_0px"), "ok");
|
||||||
|
assert_eq!(kind("quick-terminal-size", b"0x10px"), "InvalidValue");
|
||||||
|
assert_eq!(kind("font-variation", b"wght=1_0.5"), "ok");
|
||||||
|
assert_eq!(kind("font-variation", b"wght=0x1p4"), "ok");
|
||||||
|
assert_eq!(kind("mouse-scroll-multiplier", b"1_0"), "ok");
|
||||||
|
assert_eq!(kind("palette", b"0x10=#fff"), "ok");
|
||||||
|
assert_eq!(kind("palette", b"256=#fff"), "Overflow");
|
||||||
|
assert_eq!(kind("palette", b"-1=#fff"), "Overflow");
|
||||||
|
assert_eq!(kind("palette", b"-0=#fff"), "ok");
|
||||||
|
assert_eq!(kind("palette", b"1x=#fff"), "InvalidCharacter");
|
||||||
|
assert_eq!(kind("background-blur", b"0b11"), "ok");
|
||||||
|
assert_eq!(kind("keybind", b"a=scroll_page_lines:1_0"), "ok");
|
||||||
|
assert_eq!(
|
||||||
|
kind("keybind", b"a=scroll_page_lines:0x10"),
|
||||||
|
"InvalidFormat"
|
||||||
|
);
|
||||||
|
assert_eq!(kind("keybind", b"a=set_font_size:1_4"), "ok");
|
||||||
|
assert_eq!(kind("keybind", b"a=set_font_size:0x1.cp+3"), "ok");
|
||||||
|
assert_eq!(kind("keybind", b"a=resize_split:up,1_0"), "ok");
|
||||||
|
assert_eq!(kind("keybind", b"a=goto_tab:1__0"), "ok");
|
||||||
|
assert_eq!(kind("keybind", b"a=goto_tab:_1"), "InvalidFormat");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_bytes_keep_field_specific_error_kinds() {
|
||||||
|
for key in [
|
||||||
|
"adjust-cell-width",
|
||||||
|
"adjust-cursor-height",
|
||||||
|
"adjust-icon-height",
|
||||||
|
] {
|
||||||
|
assert_eq!(kind(key, b"\xff"), "InvalidFormat", "{key}");
|
||||||
|
assert_eq!(kind(key, b"10px\xff"), "InvalidFormat", "{key}");
|
||||||
|
assert_eq!(kind(key, b"\xff%"), "InvalidFormat", "{key}");
|
||||||
|
}
|
||||||
|
assert_eq!(kind("quick-terminal-size", b"\xff"), "MissingUnit");
|
||||||
|
assert_eq!(kind("quick-terminal-size", b"a\xffb"), "MissingUnit");
|
||||||
|
assert_eq!(kind("quick-terminal-size", b"\xff%"), "InvalidValue");
|
||||||
|
assert_eq!(kind("quick-terminal-size", b"1px,\xff"), "MissingUnit");
|
||||||
|
assert_eq!(kind("link", b"\xff"), "NotImplemented");
|
||||||
|
assert_eq!(kind("link", b"x"), "NotImplemented");
|
||||||
|
for key in [
|
||||||
|
"background",
|
||||||
|
"foreground",
|
||||||
|
"cursor-color",
|
||||||
|
"bold-color",
|
||||||
|
"split-divider-color",
|
||||||
|
] {
|
||||||
|
assert_eq!(kind(key, b"a\xffb"), "InvalidCharacter", "{key}");
|
||||||
|
assert_eq!(kind(key, b"\xff"), "InvalidValue", "{key}");
|
||||||
|
}
|
||||||
|
assert_eq!(kind("palette", b"1=a\xffb"), "InvalidCharacter");
|
||||||
|
assert_eq!(kind("palette", b"1=\xff"), "InvalidValue");
|
||||||
|
assert_eq!(kind("keybind", b"\xff=a"), "InvalidAction");
|
||||||
|
assert_eq!(kind("keybind", b"\xff=ignore"), "InvalidFormat");
|
||||||
|
assert_eq!(kind("keybind", b"ctrl+a=goto_tab:\xff"), "InvalidFormat");
|
||||||
|
assert_eq!(kind("keybind", b"ctrl+a=nope:\xff"), "InvalidAction");
|
||||||
|
assert_eq!(kind("keybind", b"ctrl+a=text:\xff"), "ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn failed_background_blur_parse_leaves_a_zero_radius_like_zig() {
|
||||||
|
use ghostty_config::types_b::BackgroundBlur;
|
||||||
|
let mut config = Config::default();
|
||||||
|
assert!(
|
||||||
|
config
|
||||||
|
.parse_field_bytes("background-blur", Some(b"bad"))
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
assert_eq!(config.background_blur, BackgroundBlur::Radius(0));
|
||||||
|
config
|
||||||
|
.parse_field_bytes("background-blur", Some(b"7"))
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
config
|
||||||
|
.parse_field_bytes("background-blur", Some(b"\xff"))
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
assert_eq!(config.background_blur, BackgroundBlur::Radius(7));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn float_nan_formats_like_zig_and_finalize_clamps_like_zig() {
|
||||||
|
let mut config = parse("font-size", b"nan").unwrap();
|
||||||
|
let mut out = String::new();
|
||||||
|
ghostty_config::formatter::format_entry("font-size", &config.font_size, &mut out).unwrap();
|
||||||
|
assert_eq!(out, "font-size = nan\n");
|
||||||
|
|
||||||
|
config
|
||||||
|
.parse_field_bytes("faint-opacity", Some(b"nan"))
|
||||||
|
.unwrap();
|
||||||
|
config
|
||||||
|
.parse_field_bytes("unfocused-split-opacity", Some(b"nan"))
|
||||||
|
.unwrap();
|
||||||
|
config
|
||||||
|
.parse_field_bytes("minimum-contrast", Some(b"nan"))
|
||||||
|
.unwrap();
|
||||||
|
config
|
||||||
|
.parse_field_bytes("mouse-scroll-multiplier", Some(b"nan"))
|
||||||
|
.unwrap();
|
||||||
|
config.finalize().unwrap();
|
||||||
|
assert_eq!(config.faint_opacity, 1.0);
|
||||||
|
assert_eq!(config.unfocused_split_opacity, 0.15);
|
||||||
|
assert_eq!(config.minimum_contrast, 1.0);
|
||||||
|
assert_eq!(config.mouse_scroll_multiplier.precision, 0.01);
|
||||||
|
|
||||||
|
let mut config = parse("faint-opacity", b"-0").unwrap();
|
||||||
|
config.finalize().unwrap();
|
||||||
|
assert!(config.faint_opacity.is_sign_positive());
|
||||||
|
}
|
||||||
@@ -162,6 +162,43 @@ mod fc_impl {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[cfg(font_fontconfig)]
|
||||||
|
#[test]
|
||||||
|
fn interior_nul_in_font_names_ends_the_c_string() {
|
||||||
|
use ghostty_config::config::Config;
|
||||||
|
use ghostty_os::osbytes::os_bytes;
|
||||||
|
|
||||||
|
use crate::fontconfig::Property;
|
||||||
|
let mut config = Config::default();
|
||||||
|
config
|
||||||
|
.parse_field_bytes("font-family", Some(b"monospace\0suffix"))
|
||||||
|
.unwrap();
|
||||||
|
config
|
||||||
|
.parse_field_bytes("font-style", Some(b"Regular\0suffix"))
|
||||||
|
.unwrap();
|
||||||
|
config
|
||||||
|
.parse_field_bytes("font-codepoint-map", Some(b"U+0041=Fam\0ily"))
|
||||||
|
.unwrap();
|
||||||
|
let pat = to_fc_pattern(&Descriptor {
|
||||||
|
family: Some(config.font_family.list[0].clone()),
|
||||||
|
style: config.font_style.name_value().cloned(),
|
||||||
|
..Descriptor::default()
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
os_bytes(&pat.get_os_string(Property::Family, 0).unwrap()),
|
||||||
|
b"monospace"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
os_bytes(&pat.get_os_string(Property::Style, 0).unwrap()),
|
||||||
|
b"Regular"
|
||||||
|
);
|
||||||
|
let pat = to_fc_pattern(&config.font_codepoint_map.map.list[0].descriptor);
|
||||||
|
assert_eq!(
|
||||||
|
os_bytes(&pat.get_os_string(Property::Family, 0).unwrap()),
|
||||||
|
b"Fam"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(font_fontconfig)]
|
#[cfg(font_fontconfig)]
|
||||||
#[test]
|
#[test]
|
||||||
fn configured_font_names_reach_fontconfig_as_original_bytes() {
|
fn configured_font_names_reach_fontconfig_as_original_bytes() {
|
||||||
|
|||||||
@@ -189,8 +189,10 @@ impl Pattern {
|
|||||||
self.add_string_bytes(prop, v.as_bytes())
|
self.add_string_bytes(prop, v.as_bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [`Pattern::add_string`] for arbitrary bytes (file names).
|
/// [`Pattern::add_string`] for arbitrary bytes (file names). Like Zig's
|
||||||
|
/// sentinel-terminated C string handoff, the value ends at the first NUL.
|
||||||
pub fn add_string_bytes(&mut self, prop: Property, v: &[u8]) -> bool {
|
pub fn add_string_bytes(&mut self, prop: Property, v: &[u8]) -> bool {
|
||||||
|
let v = v.split(|&b| b == 0).next().unwrap_or(v);
|
||||||
let Ok(c) = CString::new(v) else {
|
let Ok(c) = CString::new(v) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -177,10 +177,8 @@ impl SplitResizeParameter {
|
|||||||
pub(super) fn parse_param(param: &str) -> Result<SplitResizeParameter, Error> {
|
pub(super) fn parse_param(param: &str) -> Result<SplitResizeParameter, Error> {
|
||||||
let mut it = param.split(',');
|
let mut it = param.split(',');
|
||||||
let dir = SplitResizeDirection::parse_param(it.next().ok_or(Error::InvalidFormat)?)?;
|
let dir = SplitResizeDirection::parse_param(it.next().ok_or(Error::InvalidFormat)?)?;
|
||||||
let amt = it
|
let amt = it.next().ok_or(Error::InvalidFormat)?;
|
||||||
.next()
|
let amt = ghostty_os::zig_fmt::parse_int::<u16>(amt.as_bytes(), 10)
|
||||||
.ok_or(Error::InvalidFormat)?
|
|
||||||
.parse::<u16>()
|
|
||||||
.map_err(|_| Error::InvalidFormat)?;
|
.map_err(|_| Error::InvalidFormat)?;
|
||||||
// If we have extra parameters it is an error
|
// If we have extra parameters it is an error
|
||||||
if it.next().is_some() {
|
if it.next().is_some() {
|
||||||
@@ -304,16 +302,23 @@ pub(super) trait ParseNum: Sized {
|
|||||||
fn parse_param(s: &str) -> Result<Self, Error>;
|
fn parse_param(s: &str) -> Result<Self, Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! num_param {
|
macro_rules! int_param {
|
||||||
($($t:ty),*) => {$(
|
($($t:ty),*) => {$(
|
||||||
impl ParseNum for $t {
|
impl ParseNum for $t {
|
||||||
fn parse_param(s: &str) -> Result<$t, Error> {
|
fn parse_param(s: &str) -> Result<$t, Error> {
|
||||||
s.parse::<$t>().map_err(|_| Error::InvalidFormat)
|
ghostty_os::zig_fmt::parse_int::<$t>(s.as_bytes(), 10)
|
||||||
|
.map_err(|_| Error::InvalidFormat)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)*};
|
)*};
|
||||||
}
|
}
|
||||||
num_param!(f32, usize, isize, i16);
|
int_param!(usize, isize, i16);
|
||||||
|
|
||||||
|
impl ParseNum for f32 {
|
||||||
|
fn parse_param(s: &str) -> Result<f32, Error> {
|
||||||
|
ghostty_os::zig_fmt::parse_float::<f32>(s.as_bytes()).map_err(|_| Error::InvalidFormat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Escape in the style of Zig's `std.zig.stringEscape`.
|
/// Escape in the style of Zig's `std.zig.stringEscape`.
|
||||||
pub(super) fn string_escape(s: &[u8], out: &mut String) {
|
pub(super) fn string_escape(s: &[u8], out: &mut String) {
|
||||||
@@ -378,12 +383,14 @@ impl Action {
|
|||||||
"set_tab_title" => Ok(Self::SetTabTitle(bytes()?)),
|
"set_tab_title" => Ok(Self::SetTabTitle(bytes()?)),
|
||||||
"activate_key_table" => Ok(Self::ActivateKeyTable(bytes()?)),
|
"activate_key_table" => Ok(Self::ActivateKeyTable(bytes()?)),
|
||||||
"activate_key_table_once" => Ok(Self::ActivateKeyTableOnce(bytes()?)),
|
"activate_key_table_once" => Ok(Self::ActivateKeyTableOnce(bytes()?)),
|
||||||
|
// Zig never validates UTF-8 for enum/numeric parameters: the
|
||||||
|
// bytes just fail the field's own grammar (`InvalidFormat`) for
|
||||||
|
// a known action and `InvalidAction` for an unknown name. U+FFFD
|
||||||
|
// is accepted by none of those grammars, so it stands in for any
|
||||||
|
// invalid parameter.
|
||||||
_ => Self::parse_named(
|
_ => Self::parse_named(
|
||||||
name,
|
name,
|
||||||
param
|
param.map(|p| std::str::from_utf8(p).unwrap_or("\u{fffd}")),
|
||||||
.map(std::str::from_utf8)
|
|
||||||
.transpose()
|
|
||||||
.map_err(|_| Error::InvalidAction)?,
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1930,15 +1937,15 @@ impl Action {
|
|||||||
}
|
}
|
||||||
Action::IncreaseFontSize(v) => {
|
Action::IncreaseFontSize(v) => {
|
||||||
out.push(':');
|
out.push(':');
|
||||||
out.push_str(&v.to_string());
|
out.push_str(&ghostty_os::zig_fmt::format_float_d(*v));
|
||||||
}
|
}
|
||||||
Action::DecreaseFontSize(v) => {
|
Action::DecreaseFontSize(v) => {
|
||||||
out.push(':');
|
out.push(':');
|
||||||
out.push_str(&v.to_string());
|
out.push_str(&ghostty_os::zig_fmt::format_float_d(*v));
|
||||||
}
|
}
|
||||||
Action::SetFontSize(v) => {
|
Action::SetFontSize(v) => {
|
||||||
out.push(':');
|
out.push(':');
|
||||||
out.push_str(&v.to_string());
|
out.push_str(&ghostty_os::zig_fmt::format_float_d(*v));
|
||||||
}
|
}
|
||||||
Action::Search(v) => {
|
Action::Search(v) => {
|
||||||
out.push(':');
|
out.push(':');
|
||||||
@@ -1954,7 +1961,7 @@ impl Action {
|
|||||||
}
|
}
|
||||||
Action::ScrollPageFractional(v) => {
|
Action::ScrollPageFractional(v) => {
|
||||||
out.push(':');
|
out.push(':');
|
||||||
out.push_str(&v.to_string());
|
out.push_str(&ghostty_os::zig_fmt::format_float_d(*v));
|
||||||
}
|
}
|
||||||
Action::ScrollPageLines(v) => {
|
Action::ScrollPageLines(v) => {
|
||||||
out.push(':');
|
out.push(':');
|
||||||
|
|||||||
@@ -173,11 +173,7 @@ impl<'a> Parser<'a> {
|
|||||||
// This is kind of hacky but we put a dummy trigger
|
// This is kind of hacky but we put a dummy trigger
|
||||||
// for chained inputs. The `next` will never yield this
|
// for chained inputs. The `next` will never yield this
|
||||||
// because we have chain set.
|
// because we have chain set.
|
||||||
input: if chain {
|
input: if chain { b"a" } else { &input[..eql_idx] },
|
||||||
"a"
|
|
||||||
} else {
|
|
||||||
std::str::from_utf8(&input[..eql_idx]).map_err(|_| Error::InvalidFormat)?
|
|
||||||
},
|
|
||||||
i: 0,
|
i: 0,
|
||||||
},
|
},
|
||||||
action: Action::parse_bytes(&input[eql_idx + 1..])?,
|
action: Action::parse_bytes(&input[eql_idx + 1..])?,
|
||||||
@@ -279,13 +275,16 @@ pub struct SequenceIterator<'a> {
|
|||||||
/// The input of triggers. This is expected to be ONLY triggers. Things
|
/// The input of triggers. This is expected to be ONLY triggers. Things
|
||||||
/// like the "unconsumed:" prefix or action must be stripped before
|
/// like the "unconsumed:" prefix or action must be stripped before
|
||||||
/// passing to this iterator.
|
/// passing to this iterator.
|
||||||
pub input: &'a str,
|
pub input: &'a [u8],
|
||||||
pub i: usize,
|
pub i: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> SequenceIterator<'a> {
|
impl<'a> SequenceIterator<'a> {
|
||||||
pub fn new(input: &'a str) -> Self {
|
pub fn new(input: &'a str) -> Self {
|
||||||
SequenceIterator { input, i: 0 }
|
SequenceIterator {
|
||||||
|
input: input.as_bytes(),
|
||||||
|
i: 0,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the next trigger in the sequence if there is no parsing error.
|
/// Returns the next trigger in the sequence if there is no parsing error.
|
||||||
@@ -294,9 +293,9 @@ impl<'a> SequenceIterator<'a> {
|
|||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
let rem = &self.input[self.i..];
|
let rem = &self.input[self.i..];
|
||||||
let idx = rem.find('>').unwrap_or(rem.len());
|
let idx = rem.iter().position(|b| *b == b'>').unwrap_or(rem.len());
|
||||||
self.i += idx + 1;
|
self.i += idx + 1;
|
||||||
Ok(Some(Trigger::parse(&rem[..idx])?))
|
Ok(Some(Trigger::parse_bytes(&rem[..idx])?))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if there are no more triggers to parse.
|
/// Returns true if there are no more triggers to parse.
|
||||||
@@ -545,19 +544,28 @@ impl Trigger {
|
|||||||
/// not be part of a sequence (i.e. `a>b`). This parses exactly a single
|
/// not be part of a sequence (i.e. `a>b`). This parses exactly a single
|
||||||
/// trigger.
|
/// trigger.
|
||||||
pub fn parse(input: &str) -> Result<Trigger, Error> {
|
pub fn parse(input: &str) -> Result<Trigger, Error> {
|
||||||
|
Trigger::parse_bytes(input.as_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`Trigger::parse`] over the original bytes. Invalid UTF-8 can only
|
||||||
|
/// reach (and fail) the key lookups, which all end in `InvalidFormat`.
|
||||||
|
pub fn parse_bytes(input: &[u8]) -> Result<Trigger, Error> {
|
||||||
if input.is_empty() {
|
if input.is_empty() {
|
||||||
return Err(Error::InvalidFormat);
|
return Err(Error::InvalidFormat);
|
||||||
}
|
}
|
||||||
let mut result = Trigger::default();
|
let mut result = Trigger::default();
|
||||||
let mut rem: &str = input;
|
let mut rem: &[u8] = input;
|
||||||
'outer: while !rem.is_empty() {
|
'outer: while !rem.is_empty() {
|
||||||
let idx = rem.find('+').unwrap_or(rem.len());
|
let idx = rem.iter().position(|b| *b == b'+').unwrap_or(rem.len());
|
||||||
let part = &rem[..idx];
|
let part = &rem[..idx];
|
||||||
rem = if idx >= rem.len() {
|
rem = if idx >= rem.len() {
|
||||||
""
|
b""
|
||||||
} else {
|
} else {
|
||||||
&rem[idx + 1..]
|
&rem[idx + 1..]
|
||||||
};
|
};
|
||||||
|
let Ok(part) = std::str::from_utf8(part) else {
|
||||||
|
return Err(Error::InvalidFormat);
|
||||||
|
};
|
||||||
|
|
||||||
// Check if its a modifier
|
// Check if its a modifier
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -34,16 +34,15 @@ pub struct Keep {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Keep {
|
impl Keep {
|
||||||
/// A NUL terminated copy of `s` (interior NULs are dropped).
|
/// A NUL terminated copy of `s` (it ends at the first interior NUL).
|
||||||
fn cstr(&mut self, s: &str) -> *const c_char {
|
fn cstr(&mut self, s: &str) -> *const c_char {
|
||||||
self.cstr_bytes(s.as_bytes())
|
self.cstr_bytes(s.as_bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [`Keep::cstr`] for raw bytes.
|
/// [`Keep::cstr`] for raw bytes.
|
||||||
fn cstr_bytes(&mut self, s: &[u8]) -> *const c_char {
|
fn cstr_bytes(&mut self, s: &[u8]) -> *const c_char {
|
||||||
let mut s = s.to_vec();
|
let end = s.iter().position(|&b| b == 0).unwrap_or(s.len());
|
||||||
s.retain(|&b| b != 0);
|
let c = CString::new(&s[..end]).unwrap_or_default();
|
||||||
let c = CString::new(s).unwrap_or_default();
|
|
||||||
let ptr = c.as_ptr();
|
let ptr = c.as_ptr();
|
||||||
// The heap buffer of a CString doesn't move when the Vec grows.
|
// The heap buffer of a CString doesn't move when the Vec grows.
|
||||||
self.strings.push(c);
|
self.strings.push(c);
|
||||||
@@ -241,6 +240,26 @@ mod tests {
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn c_title_actions_end_at_the_first_interior_nul() {
|
||||||
|
use ghostty_os::osbytes::EncodedOs;
|
||||||
|
let mut keep = Keep::default();
|
||||||
|
for tab in [false, true] {
|
||||||
|
let title = SetTitle {
|
||||||
|
title: EncodedOs::from_bytes(b"a\0b"),
|
||||||
|
};
|
||||||
|
let action = if tab {
|
||||||
|
Action::SetTabTitle(title)
|
||||||
|
} else {
|
||||||
|
Action::SetTitle(title)
|
||||||
|
};
|
||||||
|
let c = action_to_c(&action, &mut keep);
|
||||||
|
// SAFETY: both title union members have identical layout, pointer owned by keep.
|
||||||
|
let title = unsafe { std::ffi::CStr::from_ptr(c.action.set_title.title) };
|
||||||
|
assert_eq!(title.to_bytes(), b"a");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn c_title_actions_and_key_table_names_forward_original_bytes() {
|
fn c_title_actions_and_key_table_names_forward_original_bytes() {
|
||||||
use ghostty_os::osbytes::EncodedOs;
|
use ghostty_os::osbytes::EncodedOs;
|
||||||
|
|||||||
@@ -98,18 +98,23 @@ impl CConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A C string without interior NULs.
|
/// A C string over `bytes` that ends at the first NUL, like a C consumer
|
||||||
|
/// reading Zig's backing bytes (the rest is never visible through the pointer).
|
||||||
|
fn cstring_until_nul(bytes: &[u8]) -> CString {
|
||||||
|
let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
|
||||||
|
CString::new(&bytes[..end]).unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A C string for a UTF-8 string, ending at the first NUL.
|
||||||
fn cstring(s: &str) -> CString {
|
fn cstring(s: &str) -> CString {
|
||||||
CString::new(s.replace('\0', "")).unwrap_or_default()
|
cstring_until_nul(s.as_bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A C string for a config *value*: config strings carry non-UTF-8 bytes in
|
/// A C string for a config *value*: config strings carry non-UTF-8 bytes in
|
||||||
/// the reversible [`ghostty_os::osbytes`] encoding, so the embedder gets the
|
/// the reversible [`ghostty_os::osbytes`] encoding, so the embedder gets the
|
||||||
/// decoded bytes (Zig hands out the original byte slice).
|
/// decoded bytes (Zig hands out the original byte slice).
|
||||||
fn cstring_value(s: &str) -> CString {
|
fn cstring_value(s: &str) -> CString {
|
||||||
let mut bytes = ghostty_os::osbytes::config_str_to_bytes(s);
|
cstring_until_nul(&ghostty_os::osbytes::config_str_to_bytes(s))
|
||||||
bytes.retain(|b| *b != 0);
|
|
||||||
CString::new(bytes).unwrap_or_default()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Intern a string for the lifetime of the process (enum tag names).
|
/// Intern a string for the lifetime of the process (enum tag names).
|
||||||
@@ -614,6 +619,23 @@ mod tests {
|
|||||||
assert_eq!(b.strings[0].to_str().unwrap(), "h\u{e9}llo");
|
assert_eq!(b.strings[0].to_str().unwrap(), "h\u{e9}llo");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn config_string_values_end_at_the_first_interior_nul() {
|
||||||
|
use ghostty_os::osbytes::EncodedOs;
|
||||||
|
let enc = EncodedOs::from_bytes(b"a\0b");
|
||||||
|
let b = build_backing(&CValue::String(enc.into_encoded()));
|
||||||
|
assert_eq!(b.strings[0].as_bytes(), b"a");
|
||||||
|
let b = build_backing(&CValue::String("x\0y".to_owned()));
|
||||||
|
assert_eq!(b.strings[0].as_bytes(), b"x");
|
||||||
|
let enc = EncodedOs::from_bytes(b"\0lead");
|
||||||
|
let b = build_backing(&CValue::Path {
|
||||||
|
path: enc.into_encoded(),
|
||||||
|
optional: false,
|
||||||
|
});
|
||||||
|
assert_eq!(b.strings[0].as_bytes(), b"");
|
||||||
|
assert_eq!(intern("ab\0c"), intern("ab"));
|
||||||
|
}
|
||||||
|
|
||||||
fn get<T: Copy>(cfg: ghostty_config_t, key: &str, init: T) -> (bool, T) {
|
fn get<T: Copy>(cfg: ghostty_config_t, key: &str, init: T) -> (bool, T) {
|
||||||
let mut out = init;
|
let mut out = init;
|
||||||
// SAFETY: `out` is a valid destination of the key's C type.
|
// SAFETY: `out` is a valid destination of the key's C type.
|
||||||
|
|||||||
@@ -626,9 +626,8 @@ impl RtSurface for SurfaceRt {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|(m, d)| {
|
.map(|(m, d)| {
|
||||||
(
|
(
|
||||||
CString::new(m.replace('\0', "")).unwrap_or_default(),
|
nul_terminated_prefix(m.as_bytes()),
|
||||||
CString::new(d.iter().copied().filter(|b| *b != 0).collect::<Vec<u8>>())
|
nul_terminated_prefix(d),
|
||||||
.unwrap_or_default(),
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@@ -2157,6 +2156,13 @@ mod darwin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A C string that ends at the first interior NUL, like a C consumer of a
|
||||||
|
/// Zig sentinel-terminated slice.
|
||||||
|
fn nul_terminated_prefix(bytes: &[u8]) -> CString {
|
||||||
|
let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
|
||||||
|
CString::new(&bytes[..end]).unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::os::unix::ffi::OsStrExt;
|
use std::os::unix::ffi::OsStrExt;
|
||||||
|
|||||||
@@ -0,0 +1,254 @@
|
|||||||
|
//! Port of Zig 0.16 `std.process.ArgIteratorGeneral` (`process/Args.zig`).
|
||||||
|
//!
|
||||||
|
//! Splits a command line on space, tab, CR and LF. Double quotes group;
|
||||||
|
//! backslashes are only special before a double quote (or a single quote
|
||||||
|
//! when `single_quotes` is enabled). A NUL byte terminates the command line,
|
||||||
|
//! exactly like Zig's sentinel handling.
|
||||||
|
|
||||||
|
/// `ArgIteratorGeneral` options.
|
||||||
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||||
|
pub struct Options {
|
||||||
|
/// Treat `#` at a token start as a comment up to end of line.
|
||||||
|
pub comments: bool,
|
||||||
|
/// Treat `'` as a quote character.
|
||||||
|
pub single_quotes: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterator over the arguments of a command line.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ArgIteratorGeneral<'a> {
|
||||||
|
cmd_line: &'a [u8],
|
||||||
|
index: usize,
|
||||||
|
options: Options,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ArgIteratorGeneral<'a> {
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(options: Options, cmd_line: &'a [u8]) -> Self {
|
||||||
|
ArgIteratorGeneral {
|
||||||
|
cmd_line,
|
||||||
|
index: 0,
|
||||||
|
options,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The byte at the cursor, or 0 at the end (the sentinel).
|
||||||
|
fn character(&self) -> u8 {
|
||||||
|
self.cmd_line.get(self.index).copied().unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Skips whitespace (and comments). Returns false at the terminator.
|
||||||
|
fn skip_whitespace(&mut self) -> bool {
|
||||||
|
loop {
|
||||||
|
match self.character() {
|
||||||
|
0 => return false,
|
||||||
|
b' ' | b'\t' | b'\r' | b'\n' => {}
|
||||||
|
b'#' => {
|
||||||
|
if self.options.comments {
|
||||||
|
loop {
|
||||||
|
match self.cmd_line.get(self.index).copied().unwrap_or(0) {
|
||||||
|
b'\n' => break,
|
||||||
|
0 => return false,
|
||||||
|
_ => self.index += 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The loop `continue`s, which advances past the newline.
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => break,
|
||||||
|
}
|
||||||
|
self.index += 1;
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Skips the next argument. Returns false at the end.
|
||||||
|
pub fn skip_arg(&mut self) -> bool {
|
||||||
|
if !self.skip_whitespace() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut backslash_count: usize = 0;
|
||||||
|
let mut in_quote = false;
|
||||||
|
loop {
|
||||||
|
let c = self.character();
|
||||||
|
match c {
|
||||||
|
0 => return true,
|
||||||
|
b'"' | b'\'' => {
|
||||||
|
if !self.options.single_quotes && c == b'\'' {
|
||||||
|
backslash_count = 0;
|
||||||
|
} else {
|
||||||
|
let quote_is_real = backslash_count.is_multiple_of(2);
|
||||||
|
if quote_is_real {
|
||||||
|
in_quote = !in_quote;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b'\\' => backslash_count += 1,
|
||||||
|
b' ' | b'\t' | b'\r' | b'\n' => {
|
||||||
|
if !in_quote {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
backslash_count = 0;
|
||||||
|
}
|
||||||
|
_ => backslash_count = 0,
|
||||||
|
}
|
||||||
|
self.index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The next argument, or `None` at the end.
|
||||||
|
pub fn next_arg(&mut self) -> Option<Vec<u8>> {
|
||||||
|
if !self.skip_whitespace() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut out: Vec<u8> = Vec::new();
|
||||||
|
let mut backslash_count: usize = 0;
|
||||||
|
let mut in_quote = false;
|
||||||
|
let emit_backslashes =
|
||||||
|
|out: &mut Vec<u8>, n: usize| out.extend(std::iter::repeat_n(b'\\', n));
|
||||||
|
loop {
|
||||||
|
let c = self.character();
|
||||||
|
match c {
|
||||||
|
0 => {
|
||||||
|
emit_backslashes(&mut out, backslash_count);
|
||||||
|
return Some(out);
|
||||||
|
}
|
||||||
|
b'"' | b'\'' => {
|
||||||
|
if !self.options.single_quotes && c == b'\'' {
|
||||||
|
emit_backslashes(&mut out, backslash_count);
|
||||||
|
backslash_count = 0;
|
||||||
|
out.push(c);
|
||||||
|
} else {
|
||||||
|
let quote_is_real = backslash_count.is_multiple_of(2);
|
||||||
|
emit_backslashes(&mut out, backslash_count / 2);
|
||||||
|
backslash_count = 0;
|
||||||
|
if quote_is_real {
|
||||||
|
in_quote = !in_quote;
|
||||||
|
} else {
|
||||||
|
out.push(b'"');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b'\\' => backslash_count += 1,
|
||||||
|
b' ' | b'\t' | b'\r' | b'\n' => {
|
||||||
|
emit_backslashes(&mut out, backslash_count);
|
||||||
|
backslash_count = 0;
|
||||||
|
if in_quote {
|
||||||
|
out.push(c);
|
||||||
|
} else {
|
||||||
|
return Some(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
emit_backslashes(&mut out, backslash_count);
|
||||||
|
backslash_count = 0;
|
||||||
|
out.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iterator for ArgIteratorGeneral<'_> {
|
||||||
|
type Item = Vec<u8>;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Vec<u8>> {
|
||||||
|
self.next_arg()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[allow(clippy::byte_char_slices)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn response_file(input: &str, expected: &[&str]) {
|
||||||
|
let it = ArgIteratorGeneral::new(
|
||||||
|
Options {
|
||||||
|
comments: true,
|
||||||
|
single_quotes: true,
|
||||||
|
},
|
||||||
|
input.as_bytes(),
|
||||||
|
);
|
||||||
|
let got: Vec<Vec<u8>> = it.collect();
|
||||||
|
let want: Vec<Vec<u8>> = expected.iter().map(|s| s.as_bytes().to_vec()).collect();
|
||||||
|
assert_eq!(got, want, "{input:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn response_file_arg_parsing() {
|
||||||
|
response_file("a b\nc d\\", &["a", "b", "c", "d\\"]);
|
||||||
|
response_file("a b c d\\", &["a", "b", "c", "d\\"]);
|
||||||
|
response_file(
|
||||||
|
"j\n k l # this is a comment \\\\ \\\\\\ \\\\\\\\ \"none\" \"\\\\\" \"\\\\\\\"\n \"m\" #another comment\n",
|
||||||
|
&["j", "k", "l", "m"],
|
||||||
|
);
|
||||||
|
response_file(
|
||||||
|
" \"\" q \"\"\n \"r s # t\" \"u\\\" v\" #another comment\n",
|
||||||
|
&["", "q", "", "r s # t", "u\" v"],
|
||||||
|
);
|
||||||
|
response_file(
|
||||||
|
" -l\"advapi32\" a# b#c d#\ne\\\\\\",
|
||||||
|
&["-ladvapi32", "a#", "b#c", "d#", "e\\\\\\"],
|
||||||
|
);
|
||||||
|
response_file(" 'foo' \"bar\"", &["foo", "bar"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn general(input: &[u8]) -> Vec<Vec<u8>> {
|
||||||
|
ArgIteratorGeneral::new(Options::default(), input).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn all_zig_separators_split() {
|
||||||
|
for sep in [b' ', b'\t', b'\r', b'\n'] {
|
||||||
|
assert_eq!(
|
||||||
|
general(&[b'a', sep, b'b']),
|
||||||
|
vec![b"a".to_vec(), b"b".to_vec()]
|
||||||
|
);
|
||||||
|
assert_eq!(general(&[sep, sep, b'a', sep, sep]), vec![b"a".to_vec()]);
|
||||||
|
}
|
||||||
|
// VT and FF are not separators.
|
||||||
|
assert_eq!(general(b"a\x0bb\x0cc"), vec![b"a\x0bb\x0cc".to_vec()]);
|
||||||
|
// Quoted separators are kept.
|
||||||
|
assert_eq!(
|
||||||
|
general(b"\"a\nb\r\tc\" d"),
|
||||||
|
vec![b"a\nb\r\tc".to_vec(), b"d".to_vec()]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn quoting_and_backslash_rules() {
|
||||||
|
assert_eq!(general(b"a\\b"), vec![b"a\\b".to_vec()]);
|
||||||
|
assert_eq!(general(b"\\\\\\\"x"), vec![b"\\\"x".to_vec()]);
|
||||||
|
assert_eq!(general(b"\\\\\"x y\""), vec![b"\\x y".to_vec()]);
|
||||||
|
// Single quotes are ordinary characters by default.
|
||||||
|
assert_eq!(general(b"'a b'"), vec![b"'a".to_vec(), b"b'".to_vec()]);
|
||||||
|
// Unterminated quote runs to the end.
|
||||||
|
assert_eq!(general(b"\"a b"), vec![b"a b".to_vec()]);
|
||||||
|
// Trailing backslashes are emitted verbatim.
|
||||||
|
assert_eq!(general(b"a\\\\"), vec![b"a\\\\".to_vec()]);
|
||||||
|
// Empty quoted argument.
|
||||||
|
assert_eq!(general(b"\"\" x"), vec![b"".to_vec(), b"x".to_vec()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nul_terminates_the_command_line() {
|
||||||
|
assert_eq!(general(b"a b\0c d"), vec![b"a".to_vec(), b"b".to_vec()]);
|
||||||
|
assert_eq!(general(b"\0a"), Vec::<Vec<u8>>::new());
|
||||||
|
assert_eq!(general(b"\"a\0b\" c"), vec![b"a".to_vec()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn skip_matches_next() {
|
||||||
|
let mut it = ArgIteratorGeneral::new(Options::default(), b"a \"b c\"\nd");
|
||||||
|
assert!(it.skip_arg());
|
||||||
|
assert_eq!(it.next_arg(), Some(b"b c".to_vec()));
|
||||||
|
assert!(it.skip_arg());
|
||||||
|
assert!(!it.skip_arg());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,11 +33,12 @@ pub fn launched_from_desktop() -> bool {
|
|||||||
|
|
||||||
// (`std::process::id` is `getpid` / `GetCurrentProcessId`.)
|
// (`std::process::id` is `getpid` / `GetCurrentProcessId`.)
|
||||||
let pid = std::process::id();
|
let pid = std::process::id();
|
||||||
let Ok(gio_pid) = gio_pid_str.to_string_lossy().parse::<u32>() else {
|
// (`parseInt(c_int, ..., 10)`.)
|
||||||
|
let Ok(gio_pid) = crate::zig_fmt::parse_int::<i32>(gio_pid_str.as_encoded_bytes(), 10) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
gio_pid == pid
|
i64::from(gio_pid) == i64::from(pid)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Environment variable set by the macOS app to tell libghostty how it was
|
/// Environment variable set by the macOS app to tell libghostty how it was
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
)
|
)
|
||||||
)]
|
)]
|
||||||
|
|
||||||
|
pub mod arg_iterator;
|
||||||
pub mod args;
|
pub mod args;
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub mod cf_release_thread;
|
pub mod cf_release_thread;
|
||||||
@@ -31,6 +32,7 @@ pub mod file_type;
|
|||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
pub mod flatpak;
|
pub mod flatpak;
|
||||||
pub mod osbytes;
|
pub mod osbytes;
|
||||||
|
pub mod zig_fmt;
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
pub mod flatpak {
|
pub mod flatpak {
|
||||||
//! Flatpak does not exist on Windows.
|
//! Flatpak does not exist on Windows.
|
||||||
|
|||||||
@@ -431,6 +431,17 @@ impl EncodedOs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// [`EncodedOs::to_path`] as a syscall sees it: Zig hands paths to the
|
||||||
|
/// OS as NUL-terminated C strings, so an interior NUL ends the path
|
||||||
|
/// (Rust's `std::fs` would refuse the whole path instead).
|
||||||
|
pub fn to_syscall_path(&self) -> std::io::Result<std::path::PathBuf> {
|
||||||
|
let bytes = self.to_bytes();
|
||||||
|
match bytes.iter().position(|&b| b == 0) {
|
||||||
|
Some(end) => EncodedOs::from_bytes(&bytes[..end]).to_path(),
|
||||||
|
None => self.to_path(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether the decoded value starts with the given plain prefix.
|
/// Whether the decoded value starts with the given plain prefix.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn starts_with_plain(&self, prefix: &str) -> bool {
|
pub fn starts_with_plain(&self, prefix: &str) -> bool {
|
||||||
@@ -526,6 +537,22 @@ pub fn config_str_to_path(s: &str) -> std::io::Result<std::path::PathBuf> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn syscall_path_ends_at_the_first_nul() {
|
||||||
|
let e = EncodedOs::from_bytes(b"/a/b\0c");
|
||||||
|
assert_eq!(
|
||||||
|
e.to_syscall_path().unwrap(),
|
||||||
|
std::path::PathBuf::from("/a/b")
|
||||||
|
);
|
||||||
|
let e = EncodedOs::from_bytes(b"/a/\xffb");
|
||||||
|
assert_eq!(e.to_syscall_path().unwrap(), e.to_path().unwrap());
|
||||||
|
assert_eq!(
|
||||||
|
EncodedOs::from_bytes(b"\0x").to_syscall_path().unwrap(),
|
||||||
|
std::path::PathBuf::new()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn windows_paths_fail_with_bad_path_name_instead_of_replacement() {
|
fn windows_paths_fail_with_bad_path_name_instead_of_replacement() {
|
||||||
let error = windows_path_wide(b"C:\\bad\xff").unwrap_err();
|
let error = windows_path_wide(b"C:\\bad\xff").unwrap_err();
|
||||||
|
|||||||
@@ -0,0 +1,872 @@
|
|||||||
|
//! Port of Zig 0.16 `std.fmt.parseInt`, `std.fmt.parseUnsigned` and
|
||||||
|
//! `std.fmt.parseFloat` (`fmt.zig`, `fmt/parse_float.zig` and
|
||||||
|
//! `fmt/parse_float/{parse,convert_hex,FloatStream}.zig`).
|
||||||
|
//!
|
||||||
|
//! The grammars are reproduced exactly: underscores, `0b`/`0o`/`0x`
|
||||||
|
//! prefixes (both cases, base 0 only), sign rules, hex floats, `inf` /
|
||||||
|
//! `infinity` / `nan`. Decimal float values are rounded correctly by the
|
||||||
|
//! standard library once the Zig grammar has accepted the input (Zig's
|
||||||
|
//! algorithm is also correctly rounded); hex floats follow `convertHex`.
|
||||||
|
|
||||||
|
/// `std.fmt.ParseIntError`.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ParseIntError {
|
||||||
|
/// The result cannot fit in the type specified.
|
||||||
|
Overflow,
|
||||||
|
/// The input was empty or contained an invalid character.
|
||||||
|
InvalidCharacter,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ParseIntError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(match self {
|
||||||
|
ParseIntError::Overflow => "Overflow",
|
||||||
|
ParseIntError::InvalidCharacter => "InvalidCharacter",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for ParseIntError {}
|
||||||
|
|
||||||
|
/// `std.fmt.ParseFloatError`.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct ParseFloatError;
|
||||||
|
|
||||||
|
/// Integer types that can be targeted by [`parse_int`].
|
||||||
|
pub trait ZigInt: Sized + Copy {
|
||||||
|
const MIN_I128: i128;
|
||||||
|
const MAX_I128: i128;
|
||||||
|
fn from_i128(v: i128) -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! zig_int {
|
||||||
|
($($t:ty),*) => {$(
|
||||||
|
impl ZigInt for $t {
|
||||||
|
const MIN_I128: i128 = <$t>::MIN as i128;
|
||||||
|
const MAX_I128: i128 = <$t>::MAX as i128;
|
||||||
|
fn from_i128(v: i128) -> Self {
|
||||||
|
v as $t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)*};
|
||||||
|
}
|
||||||
|
zig_int!(u8, u16, u32, u64, usize, i8, i16, i32, i64, isize);
|
||||||
|
|
||||||
|
/// `std.fmt.charToDigit`.
|
||||||
|
fn char_to_digit(c: u8, base: u8) -> Result<u8, ParseIntError> {
|
||||||
|
let value = match c {
|
||||||
|
b'0'..=b'9' => c - b'0',
|
||||||
|
b'A'..=b'Z' => c - b'A' + 10,
|
||||||
|
b'a'..=b'z' => c - b'a' + 10,
|
||||||
|
_ => return Err(ParseIntError::InvalidCharacter),
|
||||||
|
};
|
||||||
|
if value >= base {
|
||||||
|
return Err(ParseIntError::InvalidCharacter);
|
||||||
|
}
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `std.fmt.parseInt(T, buf, base)`.
|
||||||
|
pub fn parse_int<T: ZigInt>(buf: &[u8], base: u8) -> Result<T, ParseIntError> {
|
||||||
|
match buf.first() {
|
||||||
|
None => Err(ParseIntError::InvalidCharacter),
|
||||||
|
Some(b'+') => parse_int_with_sign(&buf[1..], base, false),
|
||||||
|
Some(b'-') => parse_int_with_sign(&buf[1..], base, true),
|
||||||
|
Some(_) => parse_int_with_sign(buf, base, false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `std.fmt.parseUnsigned(T, buf, base)` (no sign accepted).
|
||||||
|
pub fn parse_unsigned<T: ZigInt>(buf: &[u8], base: u8) -> Result<T, ParseIntError> {
|
||||||
|
parse_int_with_sign(buf, base, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_int_with_sign<T: ZigInt>(
|
||||||
|
buf: &[u8],
|
||||||
|
base: u8,
|
||||||
|
negative: bool,
|
||||||
|
) -> Result<T, ParseIntError> {
|
||||||
|
if buf.is_empty() {
|
||||||
|
return Err(ParseIntError::InvalidCharacter);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut buf_base = base;
|
||||||
|
let mut buf_start = buf;
|
||||||
|
if base == 0 {
|
||||||
|
// Treat is as a decimal number by default.
|
||||||
|
buf_base = 10;
|
||||||
|
// Detect the base by looking at buf prefix.
|
||||||
|
if buf.len() > 2 && buf[0] == b'0' {
|
||||||
|
match buf[1].to_ascii_lowercase() {
|
||||||
|
b'b' => {
|
||||||
|
buf_base = 2;
|
||||||
|
buf_start = &buf[2..];
|
||||||
|
}
|
||||||
|
b'o' => {
|
||||||
|
buf_base = 8;
|
||||||
|
buf_start = &buf[2..];
|
||||||
|
}
|
||||||
|
b'x' => {
|
||||||
|
buf_base = 16;
|
||||||
|
buf_start = &buf[2..];
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if buf_start[0] == b'_' || buf_start[buf_start.len() - 1] == b'_' {
|
||||||
|
return Err(ParseIntError::InvalidCharacter);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut acc: i128 = 0;
|
||||||
|
for &c in buf_start {
|
||||||
|
if c == b'_' {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let digit = i128::from(char_to_digit(c, buf_base)?);
|
||||||
|
acc = acc
|
||||||
|
.checked_mul(i128::from(buf_base))
|
||||||
|
.ok_or(ParseIntError::Overflow)?;
|
||||||
|
acc = if negative {
|
||||||
|
acc.checked_sub(digit)
|
||||||
|
} else {
|
||||||
|
acc.checked_add(digit)
|
||||||
|
}
|
||||||
|
.ok_or(ParseIntError::Overflow)?;
|
||||||
|
if acc < T::MIN_I128 || acc > T::MAX_I128 {
|
||||||
|
return Err(ParseIntError::Overflow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(T::from_i128(acc))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// Floats
|
||||||
|
|
||||||
|
/// Float types that can be targeted by [`parse_float`].
|
||||||
|
pub trait ZigFloat: Sized + Copy + std::str::FromStr + std::fmt::Display {
|
||||||
|
fn is_nan_value(self) -> bool;
|
||||||
|
const MANTISSA_BITS: u32;
|
||||||
|
const FRACTIONAL_BITS: u32;
|
||||||
|
const EXP_BITS: u32;
|
||||||
|
const EXP_MAX: i64;
|
||||||
|
const EXP_MIN: i64;
|
||||||
|
const INF: Self;
|
||||||
|
const NAN: Self;
|
||||||
|
fn neg(self) -> Self;
|
||||||
|
fn zero(negative: bool) -> Self;
|
||||||
|
fn from_bits64(bits: u64) -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! zig_float {
|
||||||
|
($t:ty, $bits:ty, $m:expr, $e:expr, $max:expr, $min:expr) => {
|
||||||
|
impl ZigFloat for $t {
|
||||||
|
const MANTISSA_BITS: u32 = $m;
|
||||||
|
const FRACTIONAL_BITS: u32 = $m;
|
||||||
|
const EXP_BITS: u32 = $e;
|
||||||
|
const EXP_MAX: i64 = $max;
|
||||||
|
const EXP_MIN: i64 = $min;
|
||||||
|
const INF: Self = <$t>::INFINITY;
|
||||||
|
const NAN: Self = <$t>::NAN;
|
||||||
|
fn is_nan_value(self) -> bool {
|
||||||
|
self.is_nan()
|
||||||
|
}
|
||||||
|
fn neg(self) -> Self {
|
||||||
|
-self
|
||||||
|
}
|
||||||
|
fn zero(negative: bool) -> Self {
|
||||||
|
if negative { -0.0 } else { 0.0 }
|
||||||
|
}
|
||||||
|
fn from_bits64(bits: u64) -> Self {
|
||||||
|
<$t>::from_bits(bits as $bits)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
zig_float!(f32, u32, 23, 8, 127, -126);
|
||||||
|
zig_float!(f64, u64, 52, 11, 1023, -1022);
|
||||||
|
|
||||||
|
struct Number {
|
||||||
|
exponent: i64,
|
||||||
|
mantissa: u64,
|
||||||
|
many_digits: bool,
|
||||||
|
hex: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `FloatStream.zig`.
|
||||||
|
struct Stream<'a> {
|
||||||
|
slice: &'a [u8],
|
||||||
|
offset: usize,
|
||||||
|
underscore_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Stream<'a> {
|
||||||
|
fn new(slice: &'a [u8]) -> Self {
|
||||||
|
Stream {
|
||||||
|
slice,
|
||||||
|
offset: 0,
|
||||||
|
underscore_count: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn offset_true(&self) -> usize {
|
||||||
|
self.offset - self.underscore_count
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.offset = 0;
|
||||||
|
self.underscore_count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn first(&self) -> Option<u8> {
|
||||||
|
self.slice.get(self.offset).copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn first_is(&self, cs: &[u8]) -> bool {
|
||||||
|
self.first().is_some_and(|c| cs.contains(&c))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn first_is_lower(&self, c: u8) -> bool {
|
||||||
|
self.first().is_some_and(|ok| ok | 0x20 == c)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn first_is_digit(&self, base: u8) -> bool {
|
||||||
|
self.first().is_some_and(|c| is_digit(c, base))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn advance(&mut self, n: usize) {
|
||||||
|
self.offset += n;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scan_digit(&mut self, base: u8) -> Option<u8> {
|
||||||
|
loop {
|
||||||
|
let ok = self.first()?;
|
||||||
|
if ok.is_ascii_digit() {
|
||||||
|
self.advance(1);
|
||||||
|
return Some(ok - b'0');
|
||||||
|
} else if base == 16 && (b'a'..=b'f').contains(&ok) {
|
||||||
|
self.advance(1);
|
||||||
|
return Some(ok - b'a' + 10);
|
||||||
|
} else if base == 16 && (b'A'..=b'F').contains(&ok) {
|
||||||
|
self.advance(1);
|
||||||
|
return Some(ok - b'A' + 10);
|
||||||
|
} else if ok == b'_' {
|
||||||
|
self.advance(1);
|
||||||
|
self.underscore_count += 1;
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_digit(c: u8, base: u8) -> bool {
|
||||||
|
if base == 10 {
|
||||||
|
c.is_ascii_digit()
|
||||||
|
} else {
|
||||||
|
c.is_ascii_hexdigit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_parse_digits(stream: &mut Stream<'_>, x: &mut u64, base: u8) {
|
||||||
|
while let Some(digit) = stream.scan_digit(base) {
|
||||||
|
*x = x
|
||||||
|
.wrapping_mul(u64::from(base))
|
||||||
|
.wrapping_add(u64::from(digit));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn min_n_digit_int(digit_count: usize) -> u64 {
|
||||||
|
let mut n: u64 = 1;
|
||||||
|
for _ in 1..digit_count {
|
||||||
|
n = n.wrapping_mul(10);
|
||||||
|
}
|
||||||
|
n
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_parse_n_digits(stream: &mut Stream<'_>, x: &mut u64, base: u8, n: usize) {
|
||||||
|
while *x < min_n_digit_int(n) {
|
||||||
|
if let Some(digit) = stream.scan_digit(base) {
|
||||||
|
*x = x
|
||||||
|
.wrapping_mul(u64::from(base))
|
||||||
|
.wrapping_add(u64::from(digit));
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_scientific(stream: &mut Stream<'_>) -> Option<i64> {
|
||||||
|
let mut exponent: i64 = 0;
|
||||||
|
let mut negative = false;
|
||||||
|
|
||||||
|
if let Some(c) = stream.first() {
|
||||||
|
negative = c == b'-';
|
||||||
|
if c == b'-' || c == b'+' {
|
||||||
|
stream.advance(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if stream.first_is_digit(10) {
|
||||||
|
while let Some(digit) = stream.scan_digit(10) {
|
||||||
|
// no overflows here, saturate well before overflow
|
||||||
|
if exponent < 0x1000_0000 {
|
||||||
|
exponent = 10 * exponent + i64::from(digit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Some(if negative { -exponent } else { exponent });
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn valid_underscores(s: &[u8], base: u8) -> bool {
|
||||||
|
let mut i = 0;
|
||||||
|
while i < s.len() {
|
||||||
|
if s[i] == b'_' {
|
||||||
|
// underscore at start of end
|
||||||
|
if i == 0 || i + 1 == s.len() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// consecutive underscores
|
||||||
|
if !is_digit(s[i - 1], base) || !is_digit(s[i + 1], base) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// next is guaranteed a digit, skip an extra
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_partial_number_base(
|
||||||
|
stream: &mut Stream<'_>,
|
||||||
|
n: &mut usize,
|
||||||
|
base: u8,
|
||||||
|
max_mantissa_digits: usize,
|
||||||
|
exp_char_lower: u8,
|
||||||
|
) -> Option<Number> {
|
||||||
|
// parse initial digits before dot
|
||||||
|
let mut mantissa: u64 = 0;
|
||||||
|
try_parse_digits(stream, &mut mantissa, base);
|
||||||
|
let int_end = stream.offset_true();
|
||||||
|
let mut n_digits = stream.offset_true() as isize;
|
||||||
|
|
||||||
|
// handle dot with the following digits
|
||||||
|
let mut exponent: i64 = 0;
|
||||||
|
if stream.first_is(b".") {
|
||||||
|
stream.advance(1);
|
||||||
|
let marker = stream.offset_true();
|
||||||
|
try_parse_digits(stream, &mut mantissa, base);
|
||||||
|
let n_after_dot = stream.offset_true() - marker;
|
||||||
|
exponent = -(n_after_dot as i64);
|
||||||
|
n_digits += n_after_dot as isize;
|
||||||
|
}
|
||||||
|
|
||||||
|
// adjust required shift to offset mantissa for base-16 (2^4)
|
||||||
|
if base == 16 {
|
||||||
|
exponent *= 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
if n_digits == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle scientific format
|
||||||
|
let mut exp_number: i64 = 0;
|
||||||
|
if stream.first_is_lower(exp_char_lower) {
|
||||||
|
stream.advance(1);
|
||||||
|
exp_number = parse_scientific(stream)?;
|
||||||
|
exponent += exp_number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// length must be complete parsed length
|
||||||
|
*n += stream.offset;
|
||||||
|
|
||||||
|
if stream.underscore_count > 0 && !valid_underscores(stream.slice, base) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// common case with not many digits
|
||||||
|
if n_digits <= max_mantissa_digits as isize {
|
||||||
|
return Some(Number {
|
||||||
|
exponent,
|
||||||
|
mantissa,
|
||||||
|
many_digits: false,
|
||||||
|
hex: base == 16,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
n_digits -= max_mantissa_digits as isize;
|
||||||
|
let mut many_digits = false;
|
||||||
|
stream.reset(); // re-parse from beginning
|
||||||
|
while stream.first_is(b"0._") {
|
||||||
|
let next = stream.first().unwrap_or(0);
|
||||||
|
if next == b'_' {
|
||||||
|
stream.underscore_count += 1;
|
||||||
|
} else {
|
||||||
|
n_digits -= isize::from(next.saturating_sub(b'0' - 1));
|
||||||
|
}
|
||||||
|
stream.advance(1);
|
||||||
|
}
|
||||||
|
if n_digits > 0 {
|
||||||
|
// at this point we have more than max_mantissa_digits significant digits
|
||||||
|
many_digits = true;
|
||||||
|
mantissa = 0;
|
||||||
|
stream.reset();
|
||||||
|
try_parse_n_digits(stream, &mut mantissa, base, max_mantissa_digits);
|
||||||
|
|
||||||
|
exponent = if mantissa >= min_n_digit_int(max_mantissa_digits) {
|
||||||
|
// big int
|
||||||
|
int_end as i64 - stream.offset_true() as i64
|
||||||
|
} else {
|
||||||
|
// the next byte must be present and be '.'
|
||||||
|
stream.advance(1);
|
||||||
|
let marker = stream.offset_true();
|
||||||
|
try_parse_n_digits(stream, &mut mantissa, base, max_mantissa_digits);
|
||||||
|
marker as i64 - stream.offset_true() as i64
|
||||||
|
};
|
||||||
|
if base == 16 {
|
||||||
|
exponent *= 4;
|
||||||
|
}
|
||||||
|
// add back the explicit part
|
||||||
|
exponent += exp_number;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(Number {
|
||||||
|
exponent,
|
||||||
|
mantissa,
|
||||||
|
many_digits,
|
||||||
|
hex: base == 16,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the parsed hex-float `Number`, or a validated decimal slice.
|
||||||
|
enum Parsed {
|
||||||
|
Hex(Number),
|
||||||
|
Decimal,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_number(s: &[u8]) -> Option<Parsed> {
|
||||||
|
let mut consumed: usize = 0;
|
||||||
|
let parsed = if s.len() >= 2 && s[0] == b'0' && s[1].eq_ignore_ascii_case(&b'x') {
|
||||||
|
let mut stream = Stream::new(&s[2..]);
|
||||||
|
consumed += 2;
|
||||||
|
parse_partial_number_base(&mut stream, &mut consumed, 16, 16, b'p').map(Parsed::Hex)
|
||||||
|
} else {
|
||||||
|
let mut stream = Stream::new(s);
|
||||||
|
parse_partial_number_base(&mut stream, &mut consumed, 10, 19, b'e').map(|_| Parsed::Decimal)
|
||||||
|
};
|
||||||
|
// must consume entire float (no trailing data)
|
||||||
|
if s.len() == consumed { parsed } else { None }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_inf_or_nan<T: ZigFloat>(s: &[u8], negative: bool) -> Option<T> {
|
||||||
|
let starts = |s: &[u8], p: &[u8]| s.len() >= p.len() && s[..p.len()].eq_ignore_ascii_case(p);
|
||||||
|
if starts(s, b"inf") {
|
||||||
|
let consumed = if starts(&s[3..], b"inity") { 8 } else { 3 };
|
||||||
|
if s.len() != consumed {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
return Some(if negative { T::INF.neg() } else { T::INF });
|
||||||
|
}
|
||||||
|
if starts(s, b"nan") {
|
||||||
|
return (s.len() == 3).then_some(T::NAN);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `convert_hex.zig`.
|
||||||
|
fn convert_hex<T: ZigFloat>(n_: &Number, negative: bool) -> T {
|
||||||
|
let mut mantissa = n_.mantissa;
|
||||||
|
let mut exponent = n_.exponent;
|
||||||
|
|
||||||
|
if mantissa == 0 {
|
||||||
|
return T::zero(negative);
|
||||||
|
}
|
||||||
|
|
||||||
|
let max_exp = T::EXP_MAX;
|
||||||
|
let min_exp = T::EXP_MIN;
|
||||||
|
let mantissa_bits = T::MANTISSA_BITS;
|
||||||
|
let fractional_bits = T::FRACTIONAL_BITS;
|
||||||
|
let exp_bits = T::EXP_BITS;
|
||||||
|
let exp_bias = min_exp - 1;
|
||||||
|
|
||||||
|
// mantissa now implicitly divided by 2^fractional_bits
|
||||||
|
exponent += i64::from(fractional_bits);
|
||||||
|
|
||||||
|
while mantissa != 0 && mantissa >> (mantissa_bits + 2) == 0 {
|
||||||
|
mantissa <<= 1;
|
||||||
|
exponent -= 1;
|
||||||
|
}
|
||||||
|
if n_.many_digits {
|
||||||
|
mantissa |= 1;
|
||||||
|
}
|
||||||
|
while mantissa >> (1 + fractional_bits + 2) != 0 {
|
||||||
|
mantissa = (mantissa >> 1) | (mantissa & 1);
|
||||||
|
exponent += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If exponent is too negative, denormalize in hopes of making it
|
||||||
|
// representable. (The -2 is for the rounding bits.)
|
||||||
|
while mantissa > 1 && exponent < min_exp - 2 {
|
||||||
|
mantissa = (mantissa >> 1) | (mantissa & 1);
|
||||||
|
exponent += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Round using two bottom bits.
|
||||||
|
let mut round = mantissa & 3;
|
||||||
|
mantissa >>= 2;
|
||||||
|
round |= mantissa & 1; // round to even (round up if mantissa is odd)
|
||||||
|
exponent += 2;
|
||||||
|
if round == 3 {
|
||||||
|
mantissa += 1;
|
||||||
|
if mantissa == 1 << (1 + fractional_bits) {
|
||||||
|
mantissa >>= 1;
|
||||||
|
exponent += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Denormal or zero
|
||||||
|
if mantissa >> fractional_bits == 0 {
|
||||||
|
exponent = exp_bias;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Infinity and range error
|
||||||
|
if exponent > max_exp {
|
||||||
|
return if negative { T::INF.neg() } else { T::INF };
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut bits = mantissa & ((1u64 << mantissa_bits) - 1);
|
||||||
|
bits |= (((exponent - exp_bias) as u64) & ((1u64 << exp_bits) - 1)) << mantissa_bits;
|
||||||
|
if negative {
|
||||||
|
bits |= 1u64 << (mantissa_bits + exp_bits);
|
||||||
|
}
|
||||||
|
T::from_bits64(bits)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Zig's `{d}` for floats: like Rust's `Display` except NaN is `nan`
|
||||||
|
/// (infinities are `inf` / `-inf` in both).
|
||||||
|
#[must_use]
|
||||||
|
pub fn format_float_d<T: ZigFloat>(v: T) -> String {
|
||||||
|
if v.is_nan_value() {
|
||||||
|
"nan".to_string()
|
||||||
|
} else {
|
||||||
|
v.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `std.fmt.parseFloat(T, s)`.
|
||||||
|
pub fn parse_float<T: ZigFloat>(s: &[u8]) -> Result<T, ParseFloatError> {
|
||||||
|
if s.is_empty() {
|
||||||
|
return Err(ParseFloatError);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut i = 0;
|
||||||
|
let negative = s[i] == b'-';
|
||||||
|
if s[i] == b'-' || s[i] == b'+' {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
if s.len() == i {
|
||||||
|
return Err(ParseFloatError);
|
||||||
|
}
|
||||||
|
let s = &s[i..];
|
||||||
|
|
||||||
|
match parse_number(s) {
|
||||||
|
None => parse_inf_or_nan::<T>(s, negative).ok_or(ParseFloatError),
|
||||||
|
Some(Parsed::Hex(n)) => {
|
||||||
|
debug_assert!(n.hex);
|
||||||
|
Ok(convert_hex::<T>(&n, negative))
|
||||||
|
}
|
||||||
|
Some(Parsed::Decimal) => {
|
||||||
|
// The grammar is validated; underscores are ignorable now.
|
||||||
|
let cleaned: String = s
|
||||||
|
.iter()
|
||||||
|
.filter(|&&c| c != b'_')
|
||||||
|
.map(|&c| char::from(c))
|
||||||
|
.collect();
|
||||||
|
let v: T = cleaned.parse().map_err(|_| ParseFloatError)?;
|
||||||
|
Ok(if negative { v.neg() } else { v })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[allow(clippy::approx_constant)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn pi<T: ZigInt>(s: &str, base: u8) -> Result<T, ParseIntError> {
|
||||||
|
parse_int::<T>(s.as_bytes(), base)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_int_zig_std_test() {
|
||||||
|
assert_eq!(pi::<i32>("-10", 10), Ok(-10));
|
||||||
|
assert_eq!(pi::<i32>("+10", 10), Ok(10));
|
||||||
|
assert_eq!(pi::<u32>("+10", 10), Ok(10));
|
||||||
|
assert_eq!(pi::<u32>("-10", 10), Err(ParseIntError::Overflow));
|
||||||
|
for bad in [" 10", "10 ", "_10_", "0x_10_", "0x10_", "0x_10"] {
|
||||||
|
assert_eq!(pi::<u32>(bad, 10), Err(ParseIntError::InvalidCharacter));
|
||||||
|
}
|
||||||
|
assert_eq!(pi::<u8>("255", 10), Ok(255));
|
||||||
|
assert_eq!(pi::<u8>("256", 10), Err(ParseIntError::Overflow));
|
||||||
|
|
||||||
|
// +0 and -0 should work for unsigned
|
||||||
|
assert_eq!(pi::<u8>("-0", 10), Ok(0));
|
||||||
|
assert_eq!(pi::<u8>("+0", 10), Ok(0));
|
||||||
|
|
||||||
|
assert_eq!(pi::<i8>("-128", 10), Ok(-128));
|
||||||
|
assert_eq!(pi::<i64>("-4398046511104", 10), Ok(-4_398_046_511_104));
|
||||||
|
|
||||||
|
// empty string or bare +- is invalid
|
||||||
|
for bad in ["", "+", "-"] {
|
||||||
|
assert_eq!(pi::<u32>(bad, 10), Err(ParseIntError::InvalidCharacter));
|
||||||
|
assert_eq!(pi::<i32>(bad, 10), Err(ParseIntError::InvalidCharacter));
|
||||||
|
}
|
||||||
|
|
||||||
|
// autodectect the base
|
||||||
|
assert_eq!(pi::<i32>("111", 0), Ok(111));
|
||||||
|
assert_eq!(pi::<i32>("1_1_1", 0), Ok(111));
|
||||||
|
assert_eq!(pi::<i32>("+0b111", 0), Ok(7));
|
||||||
|
assert_eq!(pi::<i32>("+0B111", 0), Ok(7));
|
||||||
|
assert_eq!(pi::<i32>("+0b1_11", 0), Ok(7));
|
||||||
|
assert_eq!(pi::<i32>("+0o111", 0), Ok(73));
|
||||||
|
assert_eq!(pi::<i32>("+0O111", 0), Ok(73));
|
||||||
|
assert_eq!(pi::<i32>("+0o11_1", 0), Ok(73));
|
||||||
|
assert_eq!(pi::<i32>("+0x111", 0), Ok(273));
|
||||||
|
assert_eq!(pi::<i32>("-0b111", 0), Ok(-7));
|
||||||
|
assert_eq!(pi::<i32>("-0b11_1", 0), Ok(-7));
|
||||||
|
assert_eq!(pi::<i32>("-0o111", 0), Ok(-73));
|
||||||
|
assert_eq!(pi::<i32>("-0x111", 0), Ok(-273));
|
||||||
|
assert_eq!(pi::<i32>("-0X111", 0), Ok(-273));
|
||||||
|
assert_eq!(pi::<i32>("-0x1_11", 0), Ok(-273));
|
||||||
|
|
||||||
|
// bare binary/octal/decimal prefix is invalid
|
||||||
|
for bad in ["0b", "0o", "0x"] {
|
||||||
|
assert_eq!(pi::<u32>(bad, 0), Err(ParseIntError::InvalidCharacter));
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(pi::<i8>("-10", 2), Ok(-2));
|
||||||
|
assert_eq!(pi::<i8>("-10", 8), Ok(-8));
|
||||||
|
assert_eq!(pi::<i8>("-10", 16), Ok(-16));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_unsigned_zig_std_test() {
|
||||||
|
let pu = |s: &str, b| parse_unsigned::<u64>(s.as_bytes(), b);
|
||||||
|
assert_eq!(parse_unsigned::<u16>(b"050124", 10), Ok(50124));
|
||||||
|
assert_eq!(parse_unsigned::<u16>(b"65535", 10), Ok(65535));
|
||||||
|
assert_eq!(parse_unsigned::<u16>(b"65_535", 10), Ok(65535));
|
||||||
|
assert_eq!(
|
||||||
|
parse_unsigned::<u16>(b"65536", 10),
|
||||||
|
Err(ParseIntError::Overflow)
|
||||||
|
);
|
||||||
|
assert_eq!(pu("0ffffffffffffffff", 16), Ok(u64::MAX));
|
||||||
|
assert_eq!(pu("0f_fff_fff_fff_fff_fff", 16), Ok(u64::MAX));
|
||||||
|
assert_eq!(pu("10000000000000000", 16), Err(ParseIntError::Overflow));
|
||||||
|
assert_eq!(parse_unsigned::<u32>(b"DeadBeef", 16), Ok(0xDEAD_BEEF));
|
||||||
|
assert_eq!(
|
||||||
|
parse_unsigned::<u32>(b"f", 10),
|
||||||
|
Err(ParseIntError::InvalidCharacter)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_unsigned::<u8>(b"109", 8),
|
||||||
|
Err(ParseIntError::InvalidCharacter)
|
||||||
|
);
|
||||||
|
assert_eq!(parse_unsigned::<u32>(b"NUMBER", 36), Ok(1_442_151_747));
|
||||||
|
// parseUnsigned does not expect a sign
|
||||||
|
assert_eq!(
|
||||||
|
parse_unsigned::<u8>(b"+0", 10),
|
||||||
|
Err(ParseIntError::InvalidCharacter)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_unsigned::<u8>(b"-0", 10),
|
||||||
|
Err(ParseIntError::InvalidCharacter)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_unsigned::<u8>(b"", 10),
|
||||||
|
Err(ParseIntError::InvalidCharacter)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_int_non_ascii_and_prefix_edge_cases() {
|
||||||
|
// `buf.len > 2` gates prefix detection: "0x" alone is decimal "0x".
|
||||||
|
assert_eq!(pi::<u32>("0x1", 0), Ok(1));
|
||||||
|
assert_eq!(pi::<u32>("0_1", 0), Ok(1));
|
||||||
|
assert_eq!(pi::<u32>("1__0", 0), Ok(10));
|
||||||
|
assert_eq!(pi::<u32>("0X10", 0), Ok(16));
|
||||||
|
assert_eq!(
|
||||||
|
parse_int::<u32>("é1".as_bytes(), 0),
|
||||||
|
Err(ParseIntError::InvalidCharacter)
|
||||||
|
);
|
||||||
|
// Prefixes are only honoured with base 0.
|
||||||
|
assert_eq!(pi::<u32>("0x10", 10), Err(ParseIntError::InvalidCharacter));
|
||||||
|
assert_eq!(pi::<u32>("0b11", 16), Ok(0xb11));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pf64(s: &str) -> Result<f64, ParseFloatError> {
|
||||||
|
parse_float::<f64>(s.as_bytes())
|
||||||
|
}
|
||||||
|
fn pf32(s: &str) -> Result<f32, ParseFloatError> {
|
||||||
|
parse_float::<f32>(s.as_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn approx(a: f64, b: f64) -> bool {
|
||||||
|
(a - b).abs() < 1e-7
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_float_zig_std_test() {
|
||||||
|
for bad in ["", " 1", "1abc", "+", "-"] {
|
||||||
|
assert!(pf64(bad).is_err(), "{bad:?}");
|
||||||
|
assert!(pf32(bad).is_err(), "{bad:?}");
|
||||||
|
}
|
||||||
|
assert_eq!(pf64("0"), Ok(0.0));
|
||||||
|
assert_eq!(pf64("+0"), Ok(0.0));
|
||||||
|
assert_eq!(pf64("-0"), Ok(0.0));
|
||||||
|
assert!(pf64("-0").unwrap().is_sign_negative());
|
||||||
|
assert_eq!(pf64("0e0"), Ok(0.0));
|
||||||
|
assert_eq!(pf64("2e3"), Ok(2000.0));
|
||||||
|
assert_eq!(pf64("1e0"), Ok(1.0));
|
||||||
|
assert_eq!(pf64("-2e3"), Ok(-2000.0));
|
||||||
|
assert_eq!(pf64("-1e0"), Ok(-1.0));
|
||||||
|
assert_eq!(pf64("1.234e3"), Ok(1234.0));
|
||||||
|
assert!(approx(pf64("3.141").unwrap(), 3.141));
|
||||||
|
assert!(approx(pf64("-3.141").unwrap(), -3.141));
|
||||||
|
assert_eq!(pf64("1e-5000"), Ok(0.0));
|
||||||
|
assert_eq!(pf64("1e+5000"), Ok(f64::INFINITY));
|
||||||
|
assert_eq!(
|
||||||
|
pf64("0.4e0066999999999999999999999999999999999999999999999999999"),
|
||||||
|
Ok(f64::INFINITY)
|
||||||
|
);
|
||||||
|
assert!(approx(
|
||||||
|
pf64("0_1_2_3_4_5_6.7_8_9_0_0_0e0_0_1_0").unwrap() / 1e10,
|
||||||
|
123_456.789
|
||||||
|
));
|
||||||
|
// underscore rule: only between two digits, never repeated.
|
||||||
|
for bad in [
|
||||||
|
"0123456.789000e_0010",
|
||||||
|
"_0123456.789000e0010",
|
||||||
|
"0__123456.789000e_0010",
|
||||||
|
"0123456_.789000e0010",
|
||||||
|
"0123456.789000e0010_",
|
||||||
|
] {
|
||||||
|
assert!(pf64(bad).is_err(), "{bad:?}");
|
||||||
|
}
|
||||||
|
assert!(approx(pf64("1e-2").unwrap(), 0.01));
|
||||||
|
assert!(approx(pf64("1234e-2").unwrap(), 12.34));
|
||||||
|
assert!(approx(pf64("1.").unwrap(), 1.0));
|
||||||
|
assert!(approx(pf64("0.").unwrap(), 0.0));
|
||||||
|
assert!(approx(pf64(".1").unwrap(), 0.1));
|
||||||
|
assert!(approx(pf64(".0").unwrap(), 0.0));
|
||||||
|
assert!(approx(pf64(".1e-1").unwrap(), 0.01));
|
||||||
|
for bad in [".", ".e1", "0.e"] {
|
||||||
|
assert!(pf64(bad).is_err(), "{bad:?}");
|
||||||
|
}
|
||||||
|
assert!(approx(pf64("123142.1").unwrap(), 123_142.1));
|
||||||
|
assert!(approx(pf64("-123142.1124").unwrap(), -123_142.112_4));
|
||||||
|
assert!(approx(
|
||||||
|
pf64("2.71828182845904523536").unwrap(),
|
||||||
|
2.718_281_828_459_045
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_float_nan_and_inf() {
|
||||||
|
assert!(pf64("nAn").unwrap().is_nan());
|
||||||
|
assert_eq!(pf64("inF"), Ok(f64::INFINITY));
|
||||||
|
assert_eq!(pf64("-INF"), Ok(f64::NEG_INFINITY));
|
||||||
|
assert_eq!(pf64("+Inf"), Ok(f64::INFINITY));
|
||||||
|
assert_eq!(pf32("-iNf"), Ok(f32::NEG_INFINITY));
|
||||||
|
assert_eq!(pf64("infinity"), Ok(f64::INFINITY));
|
||||||
|
assert_eq!(pf64("-InFiNiTy"), Ok(f64::NEG_INFINITY));
|
||||||
|
assert!(pf64("-nan").unwrap().is_nan());
|
||||||
|
for bad in ["infx", "infinit", "nanx", "in", "na", "infinityy", "i_nf"] {
|
||||||
|
assert!(pf64(bad).is_err(), "{bad:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn format_float_d_matches_zig() {
|
||||||
|
assert_eq!(format_float_d(f64::NAN), "nan");
|
||||||
|
assert_eq!(format_float_d(-f32::NAN), "nan");
|
||||||
|
assert_eq!(format_float_d(f64::INFINITY), "inf");
|
||||||
|
assert_eq!(format_float_d(f64::NEG_INFINITY), "-inf");
|
||||||
|
assert_eq!(format_float_d(13.5f32), "13.5");
|
||||||
|
assert_eq!(format_float_d(-0.0f64), "-0");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_float_largest_normals() {
|
||||||
|
assert_eq!(pf32("3.4028234664E38").unwrap().to_bits(), 0x7f7f_ffff);
|
||||||
|
assert_eq!(
|
||||||
|
pf64("1.7976931348623157E308").unwrap().to_bits(),
|
||||||
|
0x7fef_ffff_ffff_ffff
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_float_hex_special_and_zero() {
|
||||||
|
assert_eq!(pf32("0x9999p9999"), Ok(f32::INFINITY));
|
||||||
|
assert_eq!(pf32("-0x9999p9999"), Ok(f32::NEG_INFINITY));
|
||||||
|
assert_eq!(pf32("0x0"), Ok(0.0));
|
||||||
|
assert_eq!(pf32("-0x0"), Ok(0.0));
|
||||||
|
assert_eq!(pf32("0x0p42"), Ok(0.0));
|
||||||
|
assert_eq!(pf32("-0x0.00000p42"), Ok(0.0));
|
||||||
|
assert_eq!(pf32("0x0.00000p666"), Ok(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_float_hex_f32() {
|
||||||
|
assert!(pf32("0x").is_err());
|
||||||
|
assert_eq!(pf32("0x1p0"), Ok(1.0));
|
||||||
|
assert_eq!(pf32("-0x1p-1"), Ok(-0.5));
|
||||||
|
assert_eq!(pf32("0x10p+10"), Ok(16384.0));
|
||||||
|
assert_eq!(pf32("0x10p-10"), Ok(0.015_625));
|
||||||
|
assert_eq!(pf32("0x0.ffffffp128"), Ok(hexf(0xff_ffff, 128 - 24)));
|
||||||
|
assert_eq!(pf32("0x1.fffffeP+127"), Ok(f32::MAX));
|
||||||
|
assert_eq!(pf32("-0x1.fffffeP+127"), Ok(-f32::MAX));
|
||||||
|
assert_eq!(pf32("0x1p-126"), Ok(f32::MIN_POSITIVE));
|
||||||
|
assert_eq!(pf32("-0x1p-126"), Ok(-f32::MIN_POSITIVE));
|
||||||
|
assert_eq!(pf32("0x1P-149"), Ok(f32::from_bits(1)));
|
||||||
|
assert_eq!(pf32("-0x1P-149"), Ok(-f32::from_bits(1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hexf(m: u32, e: i32) -> f32 {
|
||||||
|
m as f32 * 2f32.powi(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_float_hex_f64() {
|
||||||
|
assert_eq!(pf64("0x1p0"), Ok(1.0));
|
||||||
|
assert_eq!(pf64("-0x1p-1"), Ok(-0.5));
|
||||||
|
assert_eq!(pf64("0x10p+10"), Ok(16384.0));
|
||||||
|
assert_eq!(pf64("0x10p-10"), Ok(0.015_625));
|
||||||
|
assert_eq!(pf64("0x1.fffffffffffffp+1023"), Ok(f64::MAX));
|
||||||
|
assert_eq!(pf64("-0x1.fffffffffffffp1023"), Ok(-f64::MAX));
|
||||||
|
assert_eq!(pf64("0x1p-1022"), Ok(f64::MIN_POSITIVE));
|
||||||
|
assert_eq!(pf64("-0x1p-1022"), Ok(-f64::MIN_POSITIVE));
|
||||||
|
assert_eq!(pf64("0x1p-1074"), Ok(f64::from_bits(1)));
|
||||||
|
assert_eq!(pf64("-0x1p-1074"), Ok(-f64::from_bits(1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_float_hex_with_underscores_and_many_digits() {
|
||||||
|
assert_eq!(pf64("0x1.cp+3"), Ok(14.0));
|
||||||
|
assert_eq!(pf32("0x1.cp+3"), Ok(14.0));
|
||||||
|
assert_eq!(pf64("0X1_0"), Ok(16.0));
|
||||||
|
assert_eq!(pf64("0x1_0p1_0"), Ok(16.0 * 1024.0));
|
||||||
|
assert!(pf64("0x_1").is_err());
|
||||||
|
assert!(pf64("0x1_").is_err());
|
||||||
|
assert!(pf64("0x1p_1").is_err());
|
||||||
|
assert!(pf64("0x1__0").is_err());
|
||||||
|
// many_digits: result must match between f32 and a direct f32 round.
|
||||||
|
let a = pf32("0xffffffffffffffff.0p0").unwrap();
|
||||||
|
assert_eq!(a, 1.844_674_4e19_f32);
|
||||||
|
// Decimal numbers with underscores.
|
||||||
|
assert_eq!(pf32("1_4"), Ok(14.0));
|
||||||
|
assert!(pf32("1__4").is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -244,7 +244,7 @@ impl DerivedConfig {
|
|||||||
.value
|
.value
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|p| {
|
.filter_map(|p| {
|
||||||
let path = match p.path().to_path() {
|
let path = match p.path().to_syscall_path() {
|
||||||
Ok(path) => path,
|
Ok(path) => path,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
log::warn!("invalid shader path: {error}");
|
log::warn!("invalid shader path: {error}");
|
||||||
@@ -869,7 +869,7 @@ impl<G: GraphicsApi> DrawState<G> {
|
|||||||
if let Some(p) = &self.config.bg_image
|
if let Some(p) = &self.config.bg_image
|
||||||
&& let Some(image) = p
|
&& let Some(image) = p
|
||||||
.path()
|
.path()
|
||||||
.to_path()
|
.to_syscall_path()
|
||||||
.map_err(|error| log::warn!("invalid background image path: {error}"))
|
.map_err(|error| log::warn!("invalid background image path: {error}"))
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|path| load_background_image(&path))
|
.and_then(|path| load_background_image(&path))
|
||||||
|
|||||||
@@ -532,6 +532,17 @@ mod tests {
|
|||||||
assert_eq!(detect_shell(&sh("")), None);
|
assert_eq!(detect_shell(&sh("")), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn detect_shell_splits_on_all_zig_separators() {
|
||||||
|
for sep in ["\n", "\r", "\t", " "] {
|
||||||
|
let v: Vec<_> = sh(&format!("bash{sep}-i")).arg_iterator().collect();
|
||||||
|
assert_eq!(v, ["bash", "-i"]);
|
||||||
|
assert_eq!(detect_shell(&sh(&format!("zsh{sep}-l"))), Some(Shell::Zsh));
|
||||||
|
}
|
||||||
|
// The first argument must end at the LF, not include `-i`.
|
||||||
|
assert_eq!(detect_shell(&sh("/bin/fish\n-l")), Some(Shell::Fish));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn arg_iterator_shell_and_direct() {
|
fn arg_iterator_shell_and_direct() {
|
||||||
let v: Vec<_> = sh("echo hello world").arg_iterator().collect();
|
let v: Vec<_> = sh("echo hello world").arg_iterator().collect();
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# Z11: RV9 gap groups (Ghostty 1.3.1 parity)
|
||||||
|
|
||||||
|
Fixes for the six gap groups in `review-rv9-codex.md`. No version change, no commit.
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
1. **Interior NUL in font names.** `fontconfig::Pattern::add_string_bytes`
|
||||||
|
ends the value at the first NUL (the C-string handoff Zig does) instead of
|
||||||
|
failing, so `font-family`, `font-style` and codepoint-map families no longer
|
||||||
|
hit the `assert!` in `discovery::to_fc_pattern`.
|
||||||
|
Test: `discovery::tests::interior_nul_in_font_names_ends_the_c_string`.
|
||||||
|
2. **Numeric grammars.** New `ghostty_os::zig_fmt`: exact ports of
|
||||||
|
`std.fmt.parseInt`/`parseUnsigned` (underscores, `0b/0o/0x` in both cases,
|
||||||
|
sign rules, `len > 2` prefix gate, Overflow vs InvalidCharacter) and
|
||||||
|
`std.fmt.parseFloat` (grammar of `parse.zig` incl. `validUnderscores`, hex
|
||||||
|
floats via a port of `convertHex` incl. the many-digits path, `inf`/
|
||||||
|
`infinity`/`nan`). Decimal values are rounded by Rust's correctly rounded
|
||||||
|
`FromStr` once the Zig grammar accepted the text. Ported all std tests.
|
||||||
|
Used by: primitive int/float fields (base 0), palette key (u8, base 0, Zig
|
||||||
|
error names), variation values, mouse-scroll bare float, quick-terminal
|
||||||
|
`px`/`%`, window-padding (u32, base 10), duration (`parseUnsigned` u64),
|
||||||
|
background-blur radius, codepoint/clipboard map hex (base 16), metric
|
||||||
|
modifiers, keybind numeric parameters (base 10 / float), ssh-cache entry
|
||||||
|
timestamp, `GIO_LAUNCHED_DESKTOP_FILE_PID`.
|
||||||
|
Also found by differential runs: Zig `{d}` prints `nan` (Rust `NaN`);
|
||||||
|
finalize clamps use Zig `@min/@max/clamp` NaN and +-0 semantics
|
||||||
|
(`cli_args::zig_min/zig_max/zig_clamp`); a failed `background-blur` radius
|
||||||
|
parse leaves `radius` (0 or the previous radius) like Zig's in-place union
|
||||||
|
initialisation.
|
||||||
|
3. **C strings.** `config_capi` getters, C action titles/strings
|
||||||
|
(`action_c::Keep`) and the embedded clipboard write callback now end the C
|
||||||
|
string at the first NUL instead of deleting NUL bytes.
|
||||||
|
4. **GTK new-window args.** `--working-directory=`/`--title=` trim only
|
||||||
|
`std.ascii.whitespace` (space, \t, \n, \r, VT, FF); U+00A0 etc. survive.
|
||||||
|
5. **Shell tokenizer.** New `ghostty_os::arg_iterator::ArgIteratorGeneral`
|
||||||
|
(port of `std.process.ArgIteratorGeneral`, including `comments`/
|
||||||
|
`single_quotes` options, `skip`, NUL terminates). `Command::arg_iterator`
|
||||||
|
(and so shell-integration detection) use it: space, tab, CR and LF split.
|
||||||
|
Ported the std "response file arg parsing" test plus separator/quote/NUL
|
||||||
|
tests; termio test `detect_shell_splits_on_all_zig_separators`.
|
||||||
|
6. **Parser diagnostics.** UTF-8 validation moved to where Zig would notice it:
|
||||||
|
metric modifiers (`InvalidFormat`), quick-terminal size (`MissingUnit`/
|
||||||
|
`InvalidValue`), `link` (`NotImplemented`), colors/palette
|
||||||
|
(`InvalidCharacter`), background-blur, keybind trigger (parsed after the
|
||||||
|
action, like `Parser.init`) and action parameters (known action with bad
|
||||||
|
bytes is `InvalidFormat`, unknown name `InvalidAction`).
|
||||||
|
Other differences found: config-file open errors are printed as
|
||||||
|
`error.FileNotFound` etc.; paths with interior NUL resolve/open as the C
|
||||||
|
string prefix (`EncodedOs::to_syscall_path`) for relative path expansion,
|
||||||
|
`config-file`, custom shaders, background image, gtk css.
|
||||||
|
|
||||||
|
## Differential harness
|
||||||
|
|
||||||
|
Script (not committed): for each of the 202 `Config.zig` fields x ~220 hostile
|
||||||
|
and valid values, run `/usr/bin/ghostty +validate-config --config-file=F` and
|
||||||
|
`+show-config` (value in `$XDG_CONFIG_HOME/ghostty/config`) against the Rust
|
||||||
|
binary and compare exit code, stdout and stderr. Final state: zero mismatches
|
||||||
|
except `theme` (resources path differs by install location),
|
||||||
|
`auto-update-channel` default (`tip` vs `stable`, build channel) and
|
||||||
|
`gtk-opengl-debug` (debug build).
|
||||||
|
|
||||||
|
Regression tests: `crates/ghostty-config/tests/zig_parity.rs`,
|
||||||
|
`ghostty_os::zig_fmt::tests`, `ghostty_os::arg_iterator::tests`,
|
||||||
|
`ghostty-config` command tests, lib/gtk tests listed above.
|
||||||
|
|
||||||
|
## Not covered / notes
|
||||||
|
|
||||||
|
- `ghostty-terminal` OSC/tmux/kitty parsers also call `parseInt`
|
||||||
|
(`terminal/osc/parsers/*.zig`, `tmux/*.zig`, `kitty/*.zig`); they are outside
|
||||||
|
the config/input scope of this task and still use Rust `str::parse`/
|
||||||
|
`from_str_radix`. `zig_fmt` is available for a follow-up.
|
||||||
|
- Windows/macOS behaviour is covered by cross-checks only.
|
||||||
Reference in new issue
Block a user