Files
ghostty-rs/crates/ghostty-config/src/cli_args.rs
T
ddidderr f76179f49a fix: +show-face u21/UTF-8 semantics, Zig output buffering, isAbsolute
Resolves RV14 (docs/rewrite/notes/review-rv14-codex.md, final-Z17.md):
- +show-face --cp parses as u21 (new U21 field type): out-of-range
  values give an invalid-value diagnostic and exit 1 instead of a panic;
  --string accepts raw bytes and fails with error.InvalidUtf8 after the
  codepoint lookup like Zig.
- CLI actions write through ZigWriter, a model of Zig 0.15's buffered
  File.writer (fixed buffer, drain on overflow, explicit flush), so
  stdout/stderr interleave in Zig's flush order (+show-face, help,
  version, validate-config, show-config, list-*, ssh-cache, crash-report,
  edit-config, new-window).
- Zig 0.15.2 std.fs.path.isAbsolute port (Windows: \theme and /theme are
  absolute) replaces Path::is_absolute at every Zig isAbsolute site.
- io_uring is available iff a 256-entry ring can be created, like
  libxev; the reactor falls back to IORING_OP_TIMEOUT without EXT_ARG.
- Windows basename (`λ:theme`) verified against 0.15.2 (test added).

Test Plan:
- cargo test --workspace --no-fail-fast
- diff +show-face outputs (pipe) against /usr/bin/ghostty
2026-10-12 15:15:18 +02:00

1770 lines
54 KiB
Rust

//! Generic CLI / config-line argument parser (port of `cli/args.zig`).
//!
//! The Zig implementation uses comptime reflection over the destination
//! struct. In Rust this is done with two traits:
//!
//! * [`ConfigField`]: implemented by every type that can be a field value
//! (bool, ints, floats, strings, enums, repeatable lists, ...). It knows
//! how to parse itself from a `--key=value` value and how to format itself
//! in the `+show-config` format.
//! * [`CliTarget`]: implemented by the destination struct (usually generated
//! with the [`config_struct!`](crate::config_struct) macro). It maps a key
//! to a field and optionally provides diagnostics, compatibility handlers,
//! a manual-parse hook and a `--help` hook.
use std::{fmt, io::BufRead};
use crate::{
c_value::CValue,
comma_splitter::CommaSplitter,
diagnostics::{Diagnostic, DiagnosticList, Location},
formatter::EntryFormatter,
};
/// For trimming
pub const WHITESPACE: &str = " \t";
/// Trim [`WHITESPACE`] characters from both ends.
#[must_use]
pub fn trim_ws(s: &str) -> &str {
s.trim_matches(|c| c == ' ' || c == '\t')
}
/// Trim Zig's `std.ascii.whitespace`, including vertical tab.
pub fn trim_ascii_whitespace(value: &[u8]) -> &[u8] {
let is_whitespace = |b: &u8| matches!(b, b' ' | b'\t' | b'\n' | b'\r' | 0x0b | 0x0c);
let start = value
.iter()
.position(|b| !is_whitespace(b))
.unwrap_or(value.len());
let end = value
.iter()
.rposition(|b| !is_whitespace(b))
.map_or(start, |i| i + 1);
&value[start..end]
}
/// Validate at a text field's parsing boundary, never in the line/argument reader.
pub fn text_value(value: Option<&[u8]>) -> Result<Option<&str>, ArgError> {
value
.map(|v| std::str::from_utf8(v).map_err(|_| ArgError::InvalidValue))
.transpose()
}
pub fn trim_bytes(value: &[u8]) -> &[u8] {
let start = value
.iter()
.position(|b| !matches!(b, b' ' | b'\t'))
.unwrap_or(value.len());
let end = value
.iter()
.rposition(|b| !matches!(b, b' ' | b'\t'))
.map_or(start, |i| i + 1);
&value[start..end]
}
/// The base errors for arg parsing. Additional errors can be returned due
/// to type-specific parsing but these are always possible.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ArgError {
ValueRequired,
InvalidField,
InvalidValue,
/// A type-specific error. The string is the (Zig) error name and is
/// shown to the user as `unknown error <name>`.
Other(String),
}
impl ArgError {
#[must_use]
pub fn other(name: &str) -> Self {
ArgError::Other(name.to_string())
}
}
impl fmt::Display for ArgError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ArgError::ValueRequired => f.write_str("ValueRequired"),
ArgError::InvalidField => f.write_str("InvalidField"),
ArgError::InvalidValue => f.write_str("InvalidValue"),
ArgError::Other(s) => f.write_str(s),
}
}
}
impl std::error::Error for ArgError {}
impl From<crate::comma_splitter::Error> for ArgError {
fn from(e: crate::comma_splitter::Error) -> Self {
ArgError::Other(e.to_string())
}
}
/// Coarse classification of a field's Zig type, used by the build-data
/// generator (shell completions) which switches on `@typeInfo(field.type)`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FieldKind {
/// `bool`
Bool,
/// `?bool`
OptionalBool,
/// An enum with the given variant names.
Enum(&'static [&'static str]),
/// `?Enum`
OptionalEnum(&'static [&'static str]),
/// An optional of any other type (`?T`).
OptionalOther,
/// A packed struct of bools without `parseCLI`, with its field names.
Packed(&'static [&'static str]),
/// `RepeatablePath`
RepeatablePath,
/// Anything else.
Other,
}
/// A type that can be the value of a configuration / CLI field.
pub trait ConfigField: Clone + PartialEq + Default + fmt::Debug {
/// For enum types, the list of valid values (used in error messages).
const VALID_VALUES: Option<&'static [&'static str]> = None;
/// Classification of this type for completion generation.
const KIND: FieldKind = match Self::VALID_VALUES {
Some(v) => FieldKind::Enum(v),
None => FieldKind::Other,
};
/// Parse `value` (the text after `=`, `None` if there was no `=`) into
/// `self`. Repeatable types accumulate; scalar types overwrite.
fn parse_bytes(&mut self, value: Option<&[u8]>) -> Result<(), ArgError>;
/// Convenience for plain UTF-8 input. Never accepts the OS escape encoding.
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
self.parse_bytes(value.map(str::as_bytes))
}
/// Called when the value was set to the empty string: reset to the
/// field's default value (Zig: the `init` function or the declared
/// default).
fn reset(&mut self, default: Self) {
*self = default;
}
/// Write this value in the `+show-config` format.
fn format_entry(&self, f: &mut EntryFormatter<'_>) -> fmt::Result;
/// The value in the form the C API exposes (Zig: `c_get.getValue`).
/// Types the C API doesn't support yet return [`CValue::Unsupported`].
fn c_value(&self) -> CValue {
CValue::Unsupported
}
/// [`ConfigField::c_value`] when the value is wrapped in an `Option`
/// (only `String` differs: `?[:0]const u8` is supported, `[]const u8`
/// is not).
fn c_value_optional(&self) -> CValue {
self.c_value()
}
/// The C value of an `Option<Self>` that is `None`.
fn c_value_none() -> CValue {
if matches!(Self::default().c_value(), CValue::Unsupported) {
CValue::Unsupported
} else {
CValue::Null
}
}
}
/// The destination of CLI parsing.
pub trait CliTarget {
/// Parse a single key/value pair into the destination
/// (Zig: `parseIntoField`). Returns `ArgError::InvalidField` if the key
/// is unknown.
fn parse_into_field(&mut self, key: &str, value: Option<&[u8]>) -> Result<(), ArgError>;
/// For enum-typed fields, the valid values (used for diagnostics).
fn valid_values(&self, _key: &str) -> Option<&'static [&'static str]> {
None
}
/// The diagnostic list, if supported (Zig: `_diagnostics` field).
fn diagnostics_mut(&mut self) -> Option<&mut DiagnosticList> {
None
}
/// Manual parse hook (Zig: `parseManuallyHook`). Return `Ok(false)` to
/// stop parsing.
fn parse_manually_hook(
&mut self,
_arg: &[u8],
_iter: &mut dyn ArgIter,
) -> Result<bool, ArgError> {
Ok(true)
}
/// Whether `--help`/`-h` is supported (Zig: `help` decl). If so,
/// [`CliTarget::help`] is called when seen.
fn supports_help(&self) -> bool {
false
}
fn help(&mut self) -> Result<(), ArgError> {
Ok(())
}
/// Compatibility handler (Zig: `compatibility` decl). Called when
/// parsing a field fails. Return true if handled.
fn compatibility(&mut self, _key: &str, _value: Option<&[u8]>) -> bool {
false
}
}
/// An iterator over arguments (Zig: any iterator with `next`).
pub trait ArgIter {
fn next_arg(&mut self) -> Option<Vec<u8>>;
/// Location for diagnostics.
fn location(&self) -> Option<Location> {
None
}
}
/// Parse the command line arguments from iter into dst.
///
/// If the destination reports diagnostics (see
/// [`CliTarget::diagnostics_mut`]) then errors for individual fields are
/// added to the list rather than returned.
pub fn parse<T: CliTarget + ?Sized>(dst: &mut T, iter: &mut dyn ArgIter) -> Result<(), ArgError> {
while let Some(arg) = iter.next_arg() {
// Do manual parsing if we have a hook for it.
if !dst.parse_manually_hook(&arg, iter)? {
return Ok(());
}
// If the destination supports help then we check for it, call
// the help function and return.
if dst.supports_help() && (arg == b"--help" || arg == b"-h") {
dst.help()?;
return Ok(());
}
// If this doesn't start with "--" then it isn't a config
// flag. We don't support positional arguments or configuration
// values set with spaces so this is an error.
let Some(rest) = arg.strip_prefix(b"--") else {
let loc = iter.location().unwrap_or_default();
let Some(diags) = dst.diagnostics_mut() else {
return Err(ArgError::InvalidField);
};
diags.append(Diagnostic {
key: ghostty_os::osbytes::bytes_to_config_string(&arg),
message: "invalid field".to_string(),
location: loc,
});
continue;
};
let (key, value) = match rest.iter().position(|b| *b == b'=') {
Some(idx) => (&rest[..idx], Some(&rest[idx + 1..])),
None => (rest, None),
};
// Keys are ASCII names. Keep arbitrary unknown keys byte-faithful in diagnostics.
let key = ghostty_os::osbytes::bytes_to_config_string(key);
let key = key.as_str();
if let Err(err) = dst.parse_into_field(key, value) {
// Fall back to compatibility handlers if able.
if dst.compatibility(key, value) {
log::info!(
"compatibility handler for {key} handled error, you may be using a deprecated field: {err}"
);
continue;
}
let loc = iter.location().unwrap_or_default();
let message = match &err {
ArgError::InvalidField => "unknown field".to_string(),
ArgError::ValueRequired => {
let mut m = String::from("value required");
format_values(dst.valid_values(key), &mut m);
m
}
ArgError::InvalidValue => {
let mut m = match value {
Some(v) => format!(
"invalid value \"{}\"",
ghostty_os::osbytes::bytes_to_config_string(v)
),
None => "invalid value \"null\"".to_string(),
};
format_values(dst.valid_values(key), &mut m);
m
}
ArgError::Other(name) => format!("unknown error error.{name}"),
};
let Some(diags) = dst.diagnostics_mut() else {
return Err(err);
};
diags.append(Diagnostic {
key: key.to_string(),
message,
location: loc,
});
}
}
Ok(())
}
fn format_values(values: Option<&[&str]>, out: &mut String) {
if let Some(values) = values {
out.push_str(", valid values are: ");
out.push_str(&values.join(", "));
}
}
/// Convenience for a compatibility handler that renames a field: parses
/// `value` into the field `to` of `dst`. Returns true if it succeeded.
pub fn compatibility_renamed<T: CliTarget + ?Sized>(
dst: &mut T,
to: &str,
value: Option<&[u8]>,
) -> bool {
match dst.parse_into_field(to, value) {
Ok(()) => true,
Err(err) => {
log::warn!("error parsing renamed field {to}: {err}");
false
}
}
}
/// Split a tagged-union value `tag:value` (Zig: `parseTaggedUnion`). The tag
/// is trimmed of whitespace. If there is no colon the value is empty.
#[must_use]
pub fn split_tagged_union(v: &str) -> (&str, &str) {
match v.find(':') {
Some(idx) => (trim_ws(&v[..idx]), &v[idx + 1..]),
None => (trim_ws(v), ""),
}
}
/// Split an ASCII union tag while preserving arbitrary payload bytes.
pub fn split_tagged_union_bytes(v: &[u8]) -> (&[u8], &[u8]) {
match v.iter().position(|b| *b == b':') {
Some(i) => (trim_bytes(&v[..i]), &v[i + 1..]),
None => (trim_bytes(v), b""),
}
}
/// Parse an "auto struct" value like `a:hello,b:42` (Zig: `parseAutoStruct`).
///
/// Entries are split by commas (with quoting / escaping, see
/// [`CommaSplitter`]). Each entry must be `key:value`; the value is trimmed
/// and, if double-quoted, decoded as a Zig string literal. For every entry
/// `set(key, value)` is called; it must return `Ok(true)` if the key matched
/// a field, `Ok(false)` if the key is unknown (which yields `InvalidValue`).
/// The caller is responsible for tracking which fields were set and for
/// applying defaults / required-field checks afterwards.
pub fn parse_auto_struct_with(
v: &str,
mut set: impl FnMut(&str, &str) -> Result<bool, ArgError>,
) -> Result<(), ArgError> {
parse_auto_struct_bytes_with(v.as_bytes(), |k, v| {
set(
k,
std::str::from_utf8(v).map_err(|_| ArgError::InvalidValue)?,
)
})
}
pub fn parse_auto_struct_bytes_with(
v: &[u8],
mut set: impl FnMut(&str, &[u8]) -> Result<bool, ArgError>,
) -> Result<(), ArgError> {
let mut iter = CommaSplitter::from_bytes(v);
while let Some(entry) = iter.next_bytes()? {
let idx = entry
.iter()
.position(|b| *b == b':')
.ok_or(ArgError::InvalidValue)?;
let key = trim_bytes(&entry[..idx]);
let raw = trim_bytes(&entry[idx + 1..]);
let decoded;
let value = if raw.len() >= 2 && raw.starts_with(b"\"") && raw.ends_with(b"\"") {
decoded = parse_zig_string_literal_bytes(raw).ok_or(ArgError::InvalidValue)?;
decoded.as_slice()
} else {
raw
};
if !set(
std::str::from_utf8(key).map_err(|_| ArgError::InvalidValue)?,
value,
)? {
return Err(ArgError::InvalidValue);
}
}
Ok(())
}
/// Decode a double-quoted Zig string literal (including the quotes).
/// Returns `None` on failure or invalid UTF-8. Byte fields use
/// [`parse_zig_string_literal_bytes`] to retain arbitrary literal and escaped bytes.
#[must_use]
pub fn parse_zig_string_literal(s: &str) -> Option<String> {
String::from_utf8(parse_zig_string_literal_bytes(s)?).ok()
}
/// Byte-exact [`parse_zig_string_literal`] (Zig `std.zig.string_literal`
/// decoding yields a `[]u8`).
#[must_use]
pub fn parse_zig_string_literal_bytes(s: impl AsRef<[u8]>) -> Option<Vec<u8>> {
let b = s.as_ref();
if b.len() < 2 || b[0] != b'"' || b[b.len() - 1] != b'"' {
return None;
}
let inner = &b[1..b.len() - 1];
let mut out: Vec<u8> = Vec::with_capacity(inner.len());
let mut i = 0;
while i < inner.len() {
let c = inner[i];
match c {
b'\\' => {
i += 1;
let e = *inner.get(i)?;
match e {
b'n' => out.push(b'\n'),
b'r' => out.push(b'\r'),
b't' => out.push(b'\t'),
b'\\' => out.push(b'\\'),
b'\'' => out.push(b'\''),
b'"' => out.push(b'"'),
b'x' => {
let h = inner.get(i + 1..i + 3)?;
let h = std::str::from_utf8(h).ok()?;
out.push(u8::from_str_radix(h, 16).ok()?);
i += 2;
}
b'u' => {
if *inner.get(i + 1)? != b'{' {
return None;
}
let close = inner[i + 2..].iter().position(|&x| x == b'}')? + i + 2;
let h = std::str::from_utf8(&inner[i + 2..close]).ok()?;
if h.is_empty() {
return None;
}
let cp = u32::from_str_radix(h, 16).ok()?;
let ch = char::from_u32(cp)?;
let mut buf = [0u8; 4];
out.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
i = close;
}
_ => return None,
}
}
b'"' | b'\n' => return None,
_ => out.push(c),
}
i += 1;
}
Some(out)
}
/// Parse a "packed struct" value (comma separated names, optionally
/// prefixed with `no-`, or a standalone boolean). `names` are the field
/// names; `set(index, value)` is called for each setting.
pub fn parse_packed_struct_with(
v: &str,
names: &[&str],
mut set: impl FnMut(usize, bool),
) -> Result<(), ArgError> {
// Allow standalone boolean values like "true" and "false" to
// turn on or off all of the struct's fields.
if let Ok(b) = parse_bool(v) {
for i in 0..names.len() {
set(i, b);
}
return Ok(());
}
for part_raw in v.split(',') {
let trimmed = trim_ws(part_raw);
let (part, value) = match trimmed.strip_prefix("no-") {
Some(rest) => (rest, false),
None => (trimmed, true),
};
match names.iter().position(|n| *n == part) {
Some(i) => set(i, value),
None => return Err(ArgError::InvalidValue),
}
}
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> {
match v {
"1" | "t" | "T" | "true" => Ok(true),
"0" | "f" | "F" | "false" => Ok(false),
_ => Err(ArgError::InvalidValue),
}
}
// ---------------------------------------------------------------------
// ConfigField impls for primitives
impl ConfigField for bool {
const KIND: FieldKind = FieldKind::Bool;
fn c_value(&self) -> CValue {
CValue::Bool(*self)
}
fn parse_bytes(&mut self, value: Option<&[u8]>) -> Result<(), ArgError> {
let value = crate::cli_args::text_value(value)?;
*self = parse_bool(value.unwrap_or("t"))?;
Ok(())
}
fn format_entry(&self, f: &mut EntryFormatter<'_>) -> fmt::Result {
writeln!(f.writer, "{} = {}", f.name, self)
}
}
macro_rules! impl_int_field {
($($t:ty => $cv:expr),*) => {$(
impl ConfigField for $t {
fn c_value(&self) -> CValue {
let f: fn($t) -> CValue = $cv;
f(*self)
}
fn parse_bytes(&mut self, value: Option<&[u8]>) -> Result<(), ArgError> {
let v = value.ok_or(ArgError::ValueRequired)?;
*self = ghostty_os::zig_fmt::parse_int::<$t>(v, 0)
.map_err(|_| ArgError::InvalidValue)?;
Ok(())
}
fn format_entry(&self, f: &mut EntryFormatter<'_>) -> fmt::Result {
writeln!(f.writer, "{} = {}", f.name, self)
}
}
)*};
}
impl_int_field!(
u8 => |v| CValue::Uint(u32::from(v)),
u16 => |_| CValue::Unsupported,
u32 => CValue::Uint,
u64 => |_| CValue::Unsupported,
usize => |_| CValue::Unsupported,
i8 => |_| CValue::Unsupported,
i16 => CValue::Short,
i32 => |_| CValue::Unsupported,
i64 => |_| CValue::Unsupported,
isize => |_| CValue::Unsupported
);
/// Zig `u21` (a Unicode scalar range integer, `0..=0x1F_FFFF`).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct U21(pub u32);
impl U21 {
pub const MAX: u32 = 0x1f_ffff;
}
impl ConfigField for U21 {
fn parse_bytes(&mut self, value: Option<&[u8]>) -> Result<(), ArgError> {
let v = value.ok_or(ArgError::ValueRequired)?;
let n = ghostty_os::zig_fmt::parse_int::<u32>(v, 0).map_err(|_| ArgError::InvalidValue)?;
if n > Self::MAX {
return Err(ArgError::InvalidValue);
}
*self = U21(n);
Ok(())
}
fn format_entry(&self, f: &mut EntryFormatter<'_>) -> fmt::Result {
writeln!(f.writer, "{} = {}", f.name, self.0)
}
}
macro_rules! impl_float_field {
($($t:ty => $cv:expr),*) => {$(
impl ConfigField for $t {
fn c_value(&self) -> CValue {
let f: fn($t) -> CValue = $cv;
f(*self)
}
fn parse_bytes(&mut self, value: Option<&[u8]>) -> Result<(), ArgError> {
let v = value.ok_or(ArgError::ValueRequired)?;
*self = ghostty_os::zig_fmt::parse_float::<$t>(v)
.map_err(|_| ArgError::InvalidValue)?;
Ok(())
}
fn format_entry(&self, f: &mut EntryFormatter<'_>) -> fmt::Result {
writeln!(f.writer, "{} = {}", f.name, ghostty_os::zig_fmt::format_float_d(*self))
}
}
)*};
}
impl_float_field!(f32 => CValue::F32, f64 => CValue::F64);
impl ConfigField for String {
fn c_value_optional(&self) -> CValue {
CValue::String(ghostty_os::osbytes::valid_str_to_config_string(self))
}
fn c_value_none() -> CValue {
CValue::NullString
}
fn parse_bytes(&mut self, value: Option<&[u8]>) -> Result<(), ArgError> {
let value = crate::cli_args::text_value(value)?;
*self = value.ok_or(ArgError::ValueRequired)?.to_string();
Ok(())
}
fn format_entry(&self, f: &mut EntryFormatter<'_>) -> fmt::Result {
writeln!(
f.writer,
"{} = {}",
f.name,
ghostty_os::osbytes::valid_str_to_config_string(self)
)
}
}
impl ConfigField for ghostty_os::osbytes::EncodedOs {
fn c_value_optional(&self) -> CValue {
CValue::String(self.as_encoded().to_owned())
}
fn c_value_none() -> CValue {
CValue::NullString
}
fn parse_bytes(&mut self, value: Option<&[u8]>) -> Result<(), ArgError> {
*self = Self::from_bytes(value.ok_or(ArgError::ValueRequired)?);
Ok(())
}
fn format_entry(&self, f: &mut EntryFormatter<'_>) -> fmt::Result {
writeln!(f.writer, "{} = {}", f.name, self.as_encoded())
}
}
impl<T: ConfigField> ConfigField for Option<T> {
const KIND: FieldKind = match T::KIND {
FieldKind::Bool => FieldKind::OptionalBool,
FieldKind::Enum(v) => FieldKind::OptionalEnum(v),
_ => FieldKind::OptionalOther,
};
fn c_value(&self) -> CValue {
match self {
Some(v) => v.c_value_optional(),
None => T::c_value_none(),
}
}
fn parse_bytes(&mut self, value: Option<&[u8]>) -> Result<(), ArgError> {
// For optional fields, we just treat it as the child type.
if let Some(v) = self {
v.parse_bytes(value)
} else {
let mut tmp = T::default();
tmp.parse_bytes(value)?;
*self = Some(tmp);
Ok(())
}
}
fn format_entry(&self, f: &mut EntryFormatter<'_>) -> fmt::Result {
match self {
Some(v) => v.format_entry(f),
None => writeln!(f.writer, "{} = ", f.name),
}
}
}
// ---------------------------------------------------------------------
// Iterators
/// An iterator that considers its location to be CLI args. It
/// iterates through an underlying iterator and increments a counter
/// to track the current CLI arg index.
///
/// This also ignores any argument that starts with `+`. It assumes that
/// actions were parsed out before this iterator was created.
pub struct ArgsIterator<I: Iterator> {
/// The underlying args iterator.
pub iterator: I,
/// Our current index into the iterator. This is 1-indexed because
/// the 0 value is used to indicate that we haven't read any values yet.
pub index: usize,
}
impl<I: Iterator> ArgsIterator<I>
where
I::Item: AsRef<[u8]>,
{
pub fn new(iterator: I) -> Self {
Self { iterator, index: 0 }
}
pub fn next(&mut self) -> Option<Vec<u8>> {
loop {
let value = self.iterator.next()?;
self.index += 1;
// We ignore any argument that starts with "+". This is used
// to indicate actions and are expected to be parsed out before
// this iterator is created.
if value.as_ref().starts_with(b"+") {
continue;
}
return Some(value.as_ref().to_vec());
}
}
}
impl<I: Iterator> ArgIter for ArgsIterator<I>
where
I::Item: AsRef<[u8]>,
{
fn next_arg(&mut self) -> Option<Vec<u8>> {
self.next()
}
fn location(&self) -> Option<Location> {
Some(Location::Cli(self.index))
}
}
/// Create an args iterator for the process args. This will skip argv0.
#[must_use]
pub fn args_iterator() -> ArgsIterator<std::iter::Skip<std::vec::IntoIter<Vec<u8>>>> {
// Keep original bytes until each field chooses whether to validate UTF-8.
ArgsIterator::new(ghostty_os::args::collect_bytes().into_iter().skip(1))
}
/// An iterator valid for arg parsing from a slice.
pub struct SliceIterator<'a, T> {
pub slice: &'a [T],
pub idx: usize,
}
impl<T: AsRef<[u8]>> ArgIter for SliceIterator<'_, T> {
fn next_arg(&mut self) -> Option<Vec<u8>> {
let v = self.slice.get(self.idx)?;
self.idx += 1;
Some(v.as_ref().to_vec())
}
}
/// Construct a [`SliceIterator`] from a slice.
#[must_use]
pub fn slice_iterator<T: AsRef<[u8]>>(slice: &[T]) -> SliceIterator<'_, T> {
SliceIterator { slice, idx: 0 }
}
/// A simple whitespace-splitting iterator used in tests and for
/// string-based argument parsing.
pub struct VecIterator {
items: std::vec::IntoIter<Vec<u8>>,
}
impl VecIterator {
#[must_use]
pub fn new<T: AsRef<[u8]>>(items: Vec<T>) -> Self {
Self {
items: items
.into_iter()
.map(|v| v.as_ref().to_vec())
.collect::<Vec<_>>()
.into_iter(),
}
}
/// Split on spaces (no quoting except double quotes around a value).
#[must_use]
pub fn from_str_args(s: &str) -> Self {
Self::new(split_args(s))
}
}
impl ArgIter for VecIterator {
fn next_arg(&mut self) -> Option<Vec<u8>> {
self.items.next()
}
}
/// Split a string into args like `std.process.ArgIteratorGeneral`: whitespace
/// separated, double quotes group (and are removed).
#[must_use]
pub fn split_args(s: &str) -> Vec<String> {
let mut out = vec![];
let mut cur = String::new();
let mut in_arg = false;
let mut quoted = false;
for c in s.chars() {
match c {
'"' => {
quoted = !quoted;
in_arg = true;
}
c if c.is_whitespace() && !quoted => {
if in_arg {
out.push(std::mem::take(&mut cur));
in_arg = false;
}
}
c => {
cur.push(c);
in_arg = true;
}
}
}
if in_arg {
out.push(cur);
}
out
}
/// Reads CLI args by line. Each CLI arg is expected to be a single line.
/// This is used to implement configuration files.
pub struct LineIterator<R: BufRead> {
/// Our stateful reader.
pub r: R,
/// Filepath that is used for diagnostics. It is only used for
/// diagnostic messages so it can be formatted however you want.
pub filepath: String,
/// The current line that we're on. 1-indexed; zero if we haven't read
/// any lines yet.
pub line: usize,
}
impl<R: BufRead> LineIterator<R> {
/// The maximum size a single line can be. We don't expect any
/// CLI arg to exceed this size.
pub const MAX_LINE_SIZE: usize = 4096;
pub fn new(r: R) -> Self {
Self {
r,
filepath: String::new(),
line: 0,
}
}
pub fn with_path(r: R, filepath: impl Into<String>) -> Self {
Self {
r,
filepath: filepath.into(),
line: 0,
}
}
pub fn next(&mut self) -> Option<Vec<u8>> {
let mut buf = Vec::new();
let entry: Vec<u8> = loop {
buf.clear();
match self.r.read_until(b'\n', &mut buf) {
Ok(0) => return None,
Ok(_) => {}
Err(e) => {
log::warn!("cannot read from \"{}\": {e}", self.filepath);
return None;
}
}
if buf.last() == Some(&b'\n') {
buf.pop();
}
if buf.len() > Self::MAX_LINE_SIZE - 2 {
log::warn!("cannot read from \"{}\": StreamTooLong", self.filepath);
return None;
}
self.line += 1;
// Zig trims spaces, tabs and carriage returns, without decoding text.
let start = buf
.iter()
.position(|b| !matches!(b, b' ' | b'\t' | b'\r'))
.unwrap_or(buf.len());
let end = buf
.iter()
.rposition(|b| !matches!(b, b' ' | b'\t' | b'\r'))
.map_or(start, |i| i + 1);
let trim = &buf[start..end];
// Ignore blank lines and comments
if trim.is_empty() || trim.starts_with(b"#") {
continue;
}
break trim.to_vec();
};
let mut result = b"--".to_vec();
if let Some(idx) = entry.iter().position(|b| *b == b'=') {
let key = trim_bytes(&entry[..idx]);
let mut value = trim_bytes(&entry[idx + 1..]);
// Detect a quoted string.
if value.len() >= 2 && value.starts_with(b"\"") && value.ends_with(b"\"") {
// Trim quotes since our CLI args processor expects
// quotes to already be gone.
value = &value[1..value.len() - 1];
}
result.extend_from_slice(key);
result.push(b'=');
result.extend_from_slice(value);
} else {
result.extend_from_slice(&entry);
}
Some(result)
}
}
impl<R: BufRead> ArgIter for LineIterator<R> {
fn next_arg(&mut self) -> Option<Vec<u8>> {
self.next()
}
fn location(&self) -> Option<Location> {
// If we have no filepath then we have no location.
if self.filepath.is_empty() {
return None;
}
Some(Location::File {
path: self.filepath.clone(),
line: self.line,
})
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use std::io::Cursor;
use super::*;
use crate::{config_enum, config_packed, config_struct};
fn parse_str<T: CliTarget>(dst: &mut T, s: &str) -> Result<(), ArgError> {
let mut it = VecIterator::from_str_args(s);
parse(dst, &mut it)
}
// ---- test structs -------------------------------------------------
config_struct! {
pub struct Simple {
"a" => a: String = String::new(),
"b" => b: bool = false,
"b-f" => b_f: bool = true,
}
extra {
diags: DiagnosticList = DiagnosticList::default(),
}
}
impl Default for Simple {
fn default() -> Self {
Self::raw_default()
}
}
impl CliTarget for Simple {
fn parse_into_field(&mut self, key: &str, value: Option<&[u8]>) -> Result<(), ArgError> {
self.parse_field_bytes(key, value)
}
}
config_struct! {
pub struct Quoted {
"a" => a: u8 = 0,
"b" => b: String = String::new(),
}
}
impl CliTarget for Quoted {
fn parse_into_field(&mut self, key: &str, value: Option<&[u8]>) -> Result<(), ArgError> {
self.parse_field_bytes(key, value)
}
}
config_struct! {
pub struct Reset {
"a" => a: u8 = 42,
"b" => b: bool = false,
}
}
impl CliTarget for Reset {
fn parse_into_field(&mut self, key: &str, value: Option<&[u8]>) -> Result<(), ArgError> {
self.parse_field_bytes(key, value)
}
}
config_struct! {
pub struct Cp {
"cp" => cp: Option<U21> = None,
}
}
#[test]
fn u21_field_matches_zig_range() {
let mut c = Cp::raw_default();
c.parse_field("cp", Some("0x1FFFFF")).unwrap();
assert_eq!(c.cp, Some(U21(0x1f_ffff)));
c.parse_field("cp", Some("0b1000001")).unwrap();
assert_eq!(c.cp, Some(U21(65)));
for bad in ["0x200000", "4294967295", "99999999999999999999", "-1", "x"] {
assert_eq!(
c.parse_field("cp", Some(bad)),
Err(ArgError::InvalidValue),
"{bad}"
);
}
assert_eq!(c.parse_field("cp", None), Err(ArgError::ValueRequired));
}
config_enum! {
pub enum OneTwo { One => "one", Two => "two" }
}
config_struct! {
pub struct Diag {
"a" => a: String = String::new(),
"b" => b: OneTwo = OneTwo::One,
}
extra {
diags: DiagnosticList = DiagnosticList::default(),
}
}
impl CliTarget for Diag {
fn parse_into_field(&mut self, key: &str, value: Option<&[u8]>) -> Result<(), ArgError> {
self.parse_field_bytes(key, value)
}
fn valid_values(&self, key: &str) -> Option<&'static [&'static str]> {
Self::valid_values_for(key)
}
fn diagnostics_mut(&mut self) -> Option<&mut DiagnosticList> {
Some(&mut self.diags)
}
}
#[test]
fn parse_simple() {
let mut data = Simple::raw_default();
parse_str(&mut data, "--a=42 --b --b-f=false").unwrap();
assert_eq!(data.a, "42");
assert!(data.b);
assert!(!data.b_f);
// Reparsing works
parse_str(&mut data, "--a=84").unwrap();
assert_eq!(data.a, "84");
assert!(data.b);
assert!(!data.b_f);
}
#[test]
fn parse_quoted_value() {
let mut data = Quoted::raw_default();
parse_str(&mut data, "--a=\"42\" --b=\"hello!\"").unwrap();
assert_eq!(data.a, 42);
assert_eq!(data.b, "hello!");
}
#[test]
fn parse_empty_value_resets_to_default() {
let mut data = Reset::raw_default();
parse_str(&mut data, "--a= --b=").unwrap();
assert_eq!(data.a, 42);
assert!(!data.b);
}
#[test]
fn parse_positional_arguments_are_invalid() {
let mut data = Reset::raw_default();
data.a = 0;
let r = parse_str(&mut data, "--a=84 what");
assert_eq!(r, Err(ArgError::InvalidField));
assert_eq!(data.a, 84);
}
#[test]
fn parse_diagnostic_tracking() {
let mut data = Diag::raw_default();
parse_str(&mut data, "--what --a=42").unwrap();
assert_eq!(data.a, "42");
assert_eq!(data.diags.items().len(), 1);
let diag = &data.diags.items()[0];
assert_eq!(diag.location, Location::None);
assert_eq!(diag.key, "what");
assert_eq!(diag.message, "unknown field");
}
#[test]
fn parse_diagnostic_location() {
let mut data = Diag::raw_default();
let mut iter = LineIterator::with_path(Cursor::new("a=42\nwhat\nb=two"), "test");
parse(&mut data, &mut iter).unwrap();
assert_eq!(data.a, "42");
assert_eq!(data.b, OneTwo::Two);
assert_eq!(data.diags.items().len(), 1);
let diag = &data.diags.items()[0];
assert_eq!(diag.key, "what");
assert_eq!(diag.message, "unknown field");
assert_eq!(
diag.location,
Location::File {
path: "test".into(),
line: 2
}
);
}
#[test]
fn parse_diagnostic_invalid_enum_value_lists_valid_values() {
let mut data = Diag::raw_default();
parse_str(&mut data, "--b=three").unwrap();
assert_eq!(
data.diags.items()[0].message,
"invalid value \"three\", valid values are: one, two"
);
}
pub struct Compat {
pub a: bool,
}
impl CliTarget for Compat {
fn parse_into_field(&mut self, key: &str, value: Option<&[u8]>) -> Result<(), ArgError> {
if key == "a" {
// Fails for anything but a valid bool.
return self.a.parse_bytes(value);
}
Err(ArgError::InvalidField)
}
fn compatibility(&mut self, key: &str, value: Option<&[u8]>) -> bool {
if key == "a" && value == Some(b"yuh") {
self.a = true;
return true;
}
false
}
}
#[test]
fn parse_compatibility_handler() {
let mut data = Compat { a: false };
parse_str(&mut data, "--a=yuh").unwrap();
assert!(data.a);
}
pub struct Renamed {
pub a: bool,
pub b: bool,
}
impl CliTarget for Renamed {
fn parse_into_field(&mut self, key: &str, value: Option<&[u8]>) -> Result<(), ArgError> {
match key {
"a" => self.a.parse_bytes(value),
"b" => self.b.parse_bytes(value),
_ => Err(ArgError::InvalidField),
}
}
fn compatibility(&mut self, key: &str, value: Option<&[u8]>) -> bool {
if key == "old" {
return compatibility_renamed(self, "a", value);
}
false
}
}
#[test]
fn parse_compatibility_renamed() {
let mut data = Renamed { a: false, b: false };
parse_str(&mut data, "--old=true --b=true").unwrap();
assert!(data.a);
assert!(data.b);
}
config_struct! {
pub struct Underscore {
"a" => a: String = "12".to_string(),
}
}
impl CliTarget for Underscore {
fn parse_into_field(&mut self, key: &str, value: Option<&[u8]>) -> Result<(), ArgError> {
// Underscore-prefixed fields are never parseable.
if key.starts_with('_') {
return Err(ArgError::InvalidField);
}
self.parse_field_bytes(key, value)
}
}
#[test]
fn parse_into_field_ignore_underscore_prefixed_fields() {
let mut data = Underscore::raw_default();
assert_eq!(
data.parse_into_field("_a", Some(b"42")),
Err(ArgError::InvalidField)
);
assert_eq!(data.a, "12");
}
#[derive(Clone, PartialEq, Debug)]
pub struct WithInit {
pub v: String,
}
impl Default for WithInit {
fn default() -> Self {
Self {
v: "default".into(),
}
}
}
impl ConfigField for WithInit {
fn parse_bytes(&mut self, _value: Option<&[u8]>) -> Result<(), ArgError> {
let _value = crate::cli_args::text_value(_value)?;
Ok(())
}
fn reset(&mut self, _default: Self) {
// `init`
self.v = "HELLO!".into();
}
fn format_entry(&self, f: &mut EntryFormatter<'_>) -> fmt::Result {
writeln!(f.writer, "{} = {}", f.name, self.v)
}
}
config_struct! {
pub struct HasInit {
"a" => a: WithInit = WithInit::default(),
}
}
#[test]
fn parse_into_field_struct_with_init_func() {
let mut data = HasInit::raw_default();
data.parse_field("a", Some("")).unwrap();
assert_eq!(data.a.v, "HELLO!");
}
macro_rules! one_field {
($name:ident, $ty:ty, $def:expr) => {
config_struct! {
pub struct $name {
"v" => v: $ty = $def,
}
}
};
}
#[test]
fn parse_into_field_string() {
one_field!(S, String, String::new());
let mut d = S::raw_default();
d.parse_field("v", Some("42")).unwrap();
assert_eq!(d.v, "42");
}
#[test]
fn parse_into_field_bool() {
one_field!(S, bool, false);
let mut d = S::raw_default();
for t in ["1", "t", "T", "true"] {
d.parse_field("v", Some(t)).unwrap();
assert!(d.v);
d.v = false;
}
for f in ["0", "f", "F", "false"] {
d.v = true;
d.parse_field("v", Some(f)).unwrap();
assert!(!d.v);
}
}
#[test]
fn parse_into_field_unsigned_numbers() {
one_field!(S, u8, 0);
let mut d = S::raw_default();
d.parse_field("v", Some("1")).unwrap();
assert_eq!(d.v, 1);
d.parse_field("v", Some("0x10")).unwrap();
assert_eq!(d.v, 16);
assert_eq!(d.parse_field("v", Some("256")), Err(ArgError::InvalidValue));
assert_eq!(d.parse_field("v", Some("-1")), Err(ArgError::InvalidValue));
}
#[test]
fn parse_into_field_floats() {
one_field!(S, f64, 0.0);
let mut d = S::raw_default();
d.parse_field("v", Some("1")).unwrap();
assert!((d.v - 1.0).abs() < f64::EPSILON);
}
config_enum! {
pub enum Three { One => "one", Two => "two", Three => "three" }
}
#[test]
fn parse_into_field_enums() {
one_field!(S, Three, Three::One);
let mut d = S::raw_default();
d.parse_field("v", Some("two")).unwrap();
assert_eq!(d.v, Three::Two);
}
config_packed! {
pub struct Pk {
"a" => a: false,
"b" => b: true,
}
}
#[test]
fn parse_into_field_packed_struct() {
one_field!(S, Pk, Pk::default());
let mut d = S::raw_default();
d.parse_field("v", Some("b")).unwrap();
assert!(!d.v.a);
assert!(d.v.b);
}
#[test]
fn parse_into_field_packed_struct_negation() {
one_field!(S, Pk, Pk::default());
let mut d = S::raw_default();
d.parse_field("v", Some("a,no-b")).unwrap();
assert!(d.v.a);
assert!(!d.v.b);
}
#[test]
fn parse_into_field_packed_struct_true_false() {
one_field!(S, Pk, Pk::default());
let mut d = S::raw_default();
d.parse_field("v", Some("true")).unwrap();
assert!(d.v.a && d.v.b);
d.parse_field("v", Some("false")).unwrap();
assert!(!d.v.a && !d.v.b);
assert_eq!(
d.parse_field("v", Some("true,a")),
Err(ArgError::InvalidValue)
);
}
#[test]
fn parse_into_field_packed_struct_whitespace() {
one_field!(S, Pk, Pk::default());
let mut d = S::raw_default();
d.parse_field("v", Some(" a, no-b ")).unwrap();
assert!(d.v.a);
assert!(!d.v.b);
}
#[test]
fn parse_into_field_optional_field() {
one_field!(S, Option<bool>, None);
let mut d = S::raw_default();
d.parse_field("v", Some("1")).unwrap();
assert_eq!(d.v, Some(true));
// Unset
d.parse_field("v", Some("")).unwrap();
assert_eq!(d.v, None);
}
#[derive(Clone, PartialEq, Debug, Default)]
pub struct WithParse {
pub v: String,
}
impl ConfigField for WithParse {
fn parse_bytes(&mut self, _value: Option<&[u8]>) -> Result<(), ArgError> {
let _value = crate::cli_args::text_value(_value)?;
self.v = "HELLO!".into();
Ok(())
}
fn format_entry(&self, f: &mut EntryFormatter<'_>) -> fmt::Result {
writeln!(f.writer, "{} = {}", f.name, self.v)
}
}
#[test]
fn parse_into_field_struct_with_parse_func() {
one_field!(S, WithParse, WithParse::default());
let mut d = S::raw_default();
d.parse_field("v", Some("42")).unwrap();
assert_eq!(d.v.v, "HELLO!");
}
#[test]
fn parse_into_field_optional_struct_with_parse_func() {
one_field!(S, Option<WithParse>, None);
let mut d = S::raw_default();
d.parse_field("v", Some("42")).unwrap();
assert_eq!(d.v.unwrap().v, "HELLO!");
}
#[derive(Clone, PartialEq, Debug)]
pub struct Basic {
pub a: String,
pub b: u32,
pub c: u8,
}
impl Default for Basic {
fn default() -> Self {
Self {
a: String::new(),
b: 0,
c: 12,
}
}
}
impl ConfigField for Basic {
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 mut r = Basic::default();
let mut set = [false; 3];
parse_auto_struct_with(v, |k, val| {
match k {
"a" => {
r.a.parse_cli(Some(val))?;
set[0] = true;
}
"b" => {
r.b.parse_cli(Some(val))?;
set[1] = true;
}
"c" => {
r.c.parse_cli(Some(val))?;
set[2] = true;
}
_ => return Ok(false),
}
Ok(true)
})?;
// a and b are required
if !set[0] || !set[1] {
return Err(ArgError::InvalidValue);
}
*self = r;
Ok(())
}
fn format_entry(&self, f: &mut EntryFormatter<'_>) -> fmt::Result {
writeln!(
f.writer,
"{} = a:{},b:{},c:{}",
f.name, self.a, self.b, self.c
)
}
}
#[test]
fn parse_into_field_struct_with_basic_fields() {
one_field!(S, Basic, Basic::default());
let mut d = S::raw_default();
d.parse_field("v", Some("a:hello,b:42")).unwrap();
assert_eq!(d.v.a, "hello");
assert_eq!(d.v.b, 42);
assert_eq!(d.v.c, 12);
d.parse_field("v", Some("a:world,b:84,c:24")).unwrap();
assert_eq!(d.v.a, "world");
assert_eq!(d.v.b, 84);
assert_eq!(d.v.c, 24);
assert_eq!(
d.parse_field("v", Some("a:hello")),
Err(ArgError::InvalidValue)
);
}
#[derive(Clone, PartialEq, Debug, Default)]
pub enum Tu {
#[default]
None,
A(u8),
B(u8),
C,
D(String),
}
impl ConfigField for Tu {
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 (tag, rest) = split_tagged_union(v);
*self = match tag {
"a" => {
let mut x = 0u8;
x.parse_cli(Some(rest))?;
Tu::A(x)
}
"b" => {
let mut x = 0u8;
x.parse_cli(Some(rest))?;
Tu::B(x)
}
"c" => {
if !rest.is_empty() {
return Err(ArgError::InvalidValue);
}
Tu::C
}
"d" => Tu::D(rest.to_string()),
_ => return Err(ArgError::InvalidValue),
};
Ok(())
}
fn format_entry(&self, _f: &mut EntryFormatter<'_>) -> fmt::Result {
Ok(())
}
}
#[test]
fn parse_into_field_tagged_union() {
one_field!(S, Tu, Tu::None);
let mut d = S::raw_default();
d.parse_field("v", Some("a:1")).unwrap();
assert_eq!(d.v, Tu::A(1));
d.parse_field("v", Some("b:2")).unwrap();
assert_eq!(d.v, Tu::B(2));
d.parse_field("v", Some("c")).unwrap();
assert_eq!(d.v, Tu::C);
d.parse_field("v", Some("d:hello")).unwrap();
assert_eq!(d.v, Tu::D("hello".into()));
}
#[test]
fn parse_into_field_tagged_union_unknown_field() {
one_field!(S, Tu, Tu::None);
let mut d = S::raw_default();
assert_eq!(d.parse_field("v", Some("x:1")), Err(ArgError::InvalidValue));
}
#[test]
fn parse_into_field_tagged_union_invalid_field_value() {
one_field!(S, Tu, Tu::None);
let mut d = S::raw_default();
assert_eq!(
d.parse_field("v", Some("a:hello")),
Err(ArgError::InvalidValue)
);
}
#[test]
fn parse_into_field_tagged_union_missing_tag() {
one_field!(S, Tu, Tu::None);
let mut d = S::raw_default();
assert_eq!(d.parse_field("v", Some("a")), Err(ArgError::InvalidValue));
assert_eq!(d.parse_field("v", Some(":a")), Err(ArgError::InvalidValue));
}
#[test]
fn args_iterator() {
let child = split_args("--what +list-things --a=42").into_iter();
let mut iter = ArgsIterator::new(child);
assert_eq!(iter.next().unwrap(), b"--what");
assert_eq!(iter.next().unwrap(), b"--a=42");
assert_eq!(iter.next(), None);
assert_eq!(iter.next(), None);
}
fn line_iter(s: &str) -> LineIterator<Cursor<Vec<u8>>> {
LineIterator::new(Cursor::new(s.as_bytes().to_vec()))
}
#[test]
fn line_iterator() {
let mut iter = line_iter(
"A\nB=42\nC\n\n# A comment\nD\n\n # An indented comment\n E\n\n# A quoted string with whitespace\nF= \"value \"",
);
assert_eq!(iter.next().unwrap(), b"--A");
assert_eq!(iter.next().unwrap(), b"--B=42");
assert_eq!(iter.next().unwrap(), b"--C");
assert_eq!(iter.next().unwrap(), b"--D");
assert_eq!(iter.next().unwrap(), b"--E");
assert_eq!(iter.next().unwrap(), b"--F=value ");
assert_eq!(iter.next(), None);
assert_eq!(iter.next(), None);
}
#[test]
fn line_iterator_end_in_newline() {
let mut iter = line_iter("A\n\n");
assert_eq!(iter.next().unwrap(), b"--A");
assert_eq!(iter.next(), None);
assert_eq!(iter.next(), None);
}
#[test]
fn line_iterator_spaces_around_equals() {
let mut iter = line_iter("A = B\n\n");
assert_eq!(iter.next().unwrap(), b"--A=B");
assert_eq!(iter.next(), None);
assert_eq!(iter.next(), None);
}
#[test]
fn line_iterator_no_value() {
let mut iter = line_iter("A = \n\n");
assert_eq!(iter.next().unwrap(), b"--A=");
assert_eq!(iter.next(), None);
}
#[test]
fn line_iterator_with_crlf_line_endings() {
let mut iter = line_iter("A\r\nB = C\r\n");
assert_eq!(iter.next().unwrap(), b"--A");
assert_eq!(iter.next().unwrap(), b"--B=C");
assert_eq!(iter.next(), None);
assert_eq!(iter.next(), None);
}
#[test]
fn line_iterator_with_buffered_reader() {
let r = std::io::BufReader::with_capacity(2, Cursor::new(b"A\nB = C\n".to_vec()));
let mut iter = LineIterator::new(r);
assert_eq!(iter.next().unwrap(), b"--A");
assert_eq!(iter.next().unwrap(), b"--B=C");
assert_eq!(iter.next(), None);
assert_eq!(iter.next(), None);
}
#[test]
fn line_iterator_with_buffered_and_primed_reader() {
use std::io::BufRead;
let mut r = std::io::BufReader::with_capacity(2, Cursor::new(b"A\nB = C\n".to_vec()));
r.fill_buf().unwrap();
let mut iter = LineIterator::new(r);
assert_eq!(iter.next().unwrap(), b"--A");
assert_eq!(iter.next().unwrap(), b"--B=C");
assert_eq!(iter.next(), None);
assert_eq!(iter.next(), None);
}
#[test]
fn numeric_fields_use_zig_parse_int_and_parse_float_grammars() {
let mut n = 0u32;
n.parse_bytes(Some(b"0X10")).unwrap();
assert_eq!(n, 16);
n.parse_bytes(Some(b"1__0")).unwrap();
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]
fn zig_string_literal_decode() {
assert_eq!(
parse_zig_string_literal("\"a\\n\\x41\\u{1F47B}\"").unwrap(),
"a\nA\u{1F47B}"
);
assert_eq!(parse_zig_string_literal("\"\\q\""), None);
}
#[test]
fn zig_string_literal_keeps_raw_bytes() {
// `\xFF` is not valid UTF-8 and must stay the single byte 0xFF.
assert_eq!(
parse_zig_string_literal_bytes("\"a\\xFFb\"").unwrap(),
vec![b'a', 0xff, b'b']
);
assert!(parse_zig_string_literal("\"a\\xFFb\"").is_none());
// Valid multi-byte sequences spelled with escapes still decode.
assert_eq!(
parse_zig_string_literal("\"\\xC3\\xA9\"").unwrap(),
"\u{e9}"
);
// Raw (unescaped) invalid input bytes survive literal parsing.
let raw = b"\"x\xfey\"";
assert_eq!(
parse_zig_string_literal_bytes(raw).unwrap(),
vec![b'x', 0xfe, b'y']
);
}
#[test]
fn line_iterator_keeps_invalid_utf8_bytes() {
let data: &[u8] = b"title = a\xffb\n";
let mut it = LineIterator::new(data);
let line = it.next().unwrap();
assert_eq!(line, b"--title=a\xffb".to_vec());
}
}