refactor(config): parse config values from raw bytes like Zig

Zig parses config values (cli/args.zig, Config.zig field parsers,
config/string.zig) from raw byte slices, and each field type decides
whether it requires UTF-8 (InvalidValue otherwise) or stores bytes. The
port escape-encoded raw bytes into Strings at the line reader and let
fields parse the escaped text, so fields could observe encoding
artifacts; successive audits kept finding such leaks (RV5-RV8).

Field parsers now receive the original bytes: text fields validate UTF-8
where Zig does, byte/path fields store bytes (EncodedOs/OsString). This
resolves all 9 RV8 findings (docs/rewrite/notes/review-rv8-codex.md),
each with a regression test: Windows path conversion fails with
BadPathName instead of substituting U+FFFD; Windows process launch uses
strict UTF-8 like Zig's Command; the WTF-8 decoder accepts paired-
surrogate encodings like Zig; literal U+FDD0 Unicode key triggers;
selection-word-chars and clipboard-codepoint-map reject invalid UTF-8;
font style / codepoint-map family names use original bytes; C title
actions forward bytes; D-Bus new-window/Flatpak conversions match Zig.
Design, sweep and validation: docs/rewrite/notes/final-bytes-refactor.md.

Also moves the bundled iTerm2-Color-Schemes license out of
resources/themes/ so +list-themes no longer lists it as a theme
(output is now byte-identical to Ghostty 1.3.1).

Done by Codex (gpt-6-astra), reviewed and verified by the orchestrator.

Test Plan:
- cargo test --workspace --no-fail-fast (3428 passed, exit 0)
- just check-macos (both), just check-windows, just check-wasm
- +show-config --default --docs, +list-keybinds, +list-actions --docs,
  +list-themes --plain compared with /usr/bin/ghostty
This commit is contained in:
ddidderr committed 2026-10-12 11:57:01 +02:00
1 parent 37a1646611
commit 672e8a4ae1
100 files changed
+2310 -1054

No files matched your search

+234 -135
View File
@@ -30,6 +30,39 @@ 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)]
@@ -103,7 +136,12 @@ pub trait ConfigField: Clone + PartialEq + Default + fmt::Debug {
/// Parse `value` (the text after `=`, `None` if there was no `=`) into
/// `self`. Repeatable types accumulate; scalar types overwrite.
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError>;
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
@@ -143,7 +181,7 @@ 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<&str>) -> Result<(), ArgError>;
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]> {
@@ -159,7 +197,7 @@ pub trait CliTarget {
/// stop parsing.
fn parse_manually_hook(
&mut self,
_arg: &str,
_arg: &[u8],
_iter: &mut dyn ArgIter,
) -> Result<bool, ArgError> {
Ok(true)
@@ -177,14 +215,14 @@ pub trait CliTarget {
/// Compatibility handler (Zig: `compatibility` decl). Called when
/// parsing a field fails. Return true if handled.
fn compatibility(&mut self, _key: &str, _value: Option<&str>) -> bool {
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<String>;
fn next_arg(&mut self) -> Option<Vec<u8>>;
/// Location for diagnostics.
fn location(&self) -> Option<Location> {
@@ -206,7 +244,7 @@ pub fn parse<T: CliTarget + ?Sized>(dst: &mut T, iter: &mut dyn ArgIter) -> Resu
// If the destination supports help then we check for it, call
// the help function and return.
if dst.supports_help() && (arg == "--help" || arg == "-h") {
if dst.supports_help() && (arg == b"--help" || arg == b"-h") {
dst.help()?;
return Ok(());
}
@@ -214,24 +252,27 @@ pub fn parse<T: CliTarget + ?Sized>(dst: &mut T, iter: &mut dyn ArgIter) -> Resu
// 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("--") else {
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: arg.clone(),
key: ghostty_os::osbytes::bytes_to_config_string(&arg),
message: "invalid field".to_string(),
location: loc,
});
continue;
};
let (key, value): (&str, Option<&str>) = match rest.find('=') {
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) {
@@ -251,7 +292,10 @@ pub fn parse<T: CliTarget + ?Sized>(dst: &mut T, iter: &mut dyn ArgIter) -> Resu
}
ArgError::InvalidValue => {
let mut m = match value {
Some(v) => format!("invalid value \"{v}\""),
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);
@@ -286,7 +330,7 @@ fn format_values(values: Option<&[&str]>, out: &mut String) {
pub fn compatibility_renamed<T: CliTarget + ?Sized>(
dst: &mut T,
to: &str,
value: Option<&str>,
value: Option<&[u8]>,
) -> bool {
match dst.parse_into_field(to, value) {
Ok(()) => true,
@@ -307,6 +351,14 @@ pub fn split_tagged_union(v: &str) -> (&str, &str) {
}
}
/// 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
@@ -320,21 +372,39 @@ pub fn parse_auto_struct_with(
v: &str,
mut set: impl FnMut(&str, &str) -> Result<bool, ArgError>,
) -> Result<(), ArgError> {
let mut iter = CommaSplitter::new(v);
while let Some(entry) = iter.next_field()? {
let idx = entry.find(':').ok_or(ArgError::InvalidValue)?;
let key = trim_ws(&entry[..idx]);
let raw = trim_ws(&entry[idx + 1..]);
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('"') && raw.ends_with('"') {
decoded = parse_zig_string_literal(raw).ok_or(ArgError::InvalidValue)?;
decoded.as_str()
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(key, value)? {
if !set(
std::str::from_utf8(key).map_err(|_| ArgError::InvalidValue)?,
value,
)? {
return Err(ArgError::InvalidValue);
}
}
@@ -342,22 +412,18 @@ pub fn parse_auto_struct_with(
}
/// Decode a double-quoted Zig string literal (including the quotes).
/// Returns `None` on failure. Bytes produced by `\x` escapes that do not form
/// valid UTF-8 stay raw bytes: they are carried through the reversible
/// `ghostty_os::osbytes` escape encoding (`\xFF` -> `U+FDD0 U+00FF`, decoded back
/// to `0xFF` by `config_str_to_bytes`). See [`parse_zig_string_literal_bytes`]
/// for the raw bytes.
/// 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> {
parse_zig_string_literal_bytes(s).map(|b| ghostty_os::osbytes::bytes_to_config_string(&b))
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: &str) -> Option<Vec<u8>> {
let b = ghostty_os::osbytes::config_str_to_bytes(s);
let b = b.as_slice();
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;
}
@@ -494,7 +560,8 @@ impl ConfigField for bool {
CValue::Bool(*self)
}
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
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(())
}
@@ -512,7 +579,8 @@ macro_rules! impl_int_field {
f(*self)
}
fn parse_cli(&mut self, value: Option<&str>) -> 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 n = parse_int_base0(v).ok_or(ArgError::InvalidValue)?;
*self = <$t>::try_from(n).map_err(|_| ArgError::InvalidValue)?;
@@ -546,7 +614,8 @@ macro_rules! impl_float_field {
f(*self)
}
fn parse_cli(&mut self, value: Option<&str>) -> 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)?;
*self = v.parse::<$t>().map_err(|_| ArgError::InvalidValue)?;
Ok(())
@@ -562,20 +631,44 @@ impl_float_field!(f32 => CValue::F32, f64 => CValue::F64);
impl ConfigField for String {
fn c_value_optional(&self) -> CValue {
CValue::String(self.clone())
CValue::String(ghostty_os::osbytes::valid_str_to_config_string(self))
}
fn c_value_none() -> CValue {
CValue::NullString
}
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
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, self)
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())
}
}
@@ -593,13 +686,13 @@ impl<T: ConfigField> ConfigField for Option<T> {
}
}
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
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_cli(value)
v.parse_bytes(value)
} else {
let mut tmp = T::default();
tmp.parse_cli(value)?;
tmp.parse_bytes(value)?;
*self = Some(tmp);
Ok(())
}
@@ -622,7 +715,7 @@ impl<T: ConfigField> ConfigField for Option<T> {
///
/// 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<Item = String>> {
pub struct ArgsIterator<I: Iterator> {
/// The underlying args iterator.
pub iterator: I,
/// Our current index into the iterator. This is 1-indexed because
@@ -630,28 +723,34 @@ pub struct ArgsIterator<I: Iterator<Item = String>> {
pub index: usize,
}
impl<I: Iterator<Item = String>> ArgsIterator<I> {
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<String> {
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.starts_with('+') {
if value.as_ref().starts_with(b"+") {
continue;
}
return Some(value);
return Some(value.as_ref().to_vec());
}
}
}
impl<I: Iterator<Item = String>> ArgIter for ArgsIterator<I> {
fn next_arg(&mut self) -> Option<String> {
impl<I: Iterator> ArgIter for ArgsIterator<I>
where
I::Item: AsRef<[u8]>,
{
fn next_arg(&mut self) -> Option<Vec<u8>> {
self.next()
}
@@ -662,47 +761,46 @@ impl<I: Iterator<Item = String>> ArgIter for ArgsIterator<I> {
/// 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<String>>> {
// Byte-safe: non-UTF-8 arguments keep their bytes (reversible escape
// encoding, see `ghostty_os::osbytes`).
ArgsIterator::new(
ghostty_os::args::collect_config_strings()
.into_iter()
.skip(1),
)
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> {
pub slice: &'a [String],
pub struct SliceIterator<'a, T> {
pub slice: &'a [T],
pub idx: usize,
}
impl ArgIter for SliceIterator<'_> {
fn next_arg(&mut self) -> Option<String> {
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.clone())
Some(v.as_ref().to_vec())
}
}
/// Construct a [`SliceIterator`] from a slice.
#[must_use]
pub fn slice_iterator(slice: &[String]) -> SliceIterator<'_> {
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<String>,
items: std::vec::IntoIter<Vec<u8>>,
}
impl VecIterator {
#[must_use]
pub fn new(items: Vec<String>) -> Self {
pub fn new<T: AsRef<[u8]>>(items: Vec<T>) -> Self {
Self {
items: items.into_iter(),
items: items
.into_iter()
.map(|v| v.as_ref().to_vec())
.collect::<Vec<_>>()
.into_iter(),
}
}
@@ -714,7 +812,7 @@ impl VecIterator {
}
impl ArgIter for VecIterator {
fn next_arg(&mut self) -> Option<String> {
fn next_arg(&mut self) -> Option<Vec<u8>> {
self.items.next()
}
}
@@ -785,9 +883,9 @@ impl<R: BufRead> LineIterator<R> {
}
}
pub fn next(&mut self) -> Option<String> {
pub fn next(&mut self) -> Option<Vec<u8>> {
let mut buf = Vec::new();
let entry: String = loop {
let entry: Vec<u8> = loop {
buf.clear();
match self.r.read_until(b'\n', &mut buf) {
Ok(0) => return None,
@@ -807,42 +905,48 @@ impl<R: BufRead> LineIterator<R> {
self.line += 1;
// Raw bytes survive via the reversible escape encoding.
let line = ghostty_os::osbytes::bytes_to_config_string(&buf);
// Trim any whitespace (including CR) around it
let trim = line.trim_matches(|c| c == ' ' || c == '\t' || c == '\r');
// 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('#') {
if trim.is_empty() || trim.starts_with(b"#") {
continue;
}
break trim.to_string();
break trim.to_vec();
};
let mut result = String::from("--");
if let Some(idx) = entry.find('=') {
let key = trim_ws(&entry[..idx]);
let mut value = trim_ws(&entry[idx + 1..]);
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('"') && value.ends_with('"') {
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.push_str(key);
result.push('=');
result.push_str(value);
result.extend_from_slice(key);
result.push(b'=');
result.extend_from_slice(value);
} else {
result.push_str(&entry);
result.extend_from_slice(&entry);
}
Some(result)
}
}
impl<R: BufRead> ArgIter for LineIterator<R> {
fn next_arg(&mut self) -> Option<String> {
fn next_arg(&mut self) -> Option<Vec<u8>> {
self.next()
}
@@ -889,8 +993,8 @@ mod tests {
}
}
impl CliTarget for Simple {
fn parse_into_field(&mut self, key: &str, value: Option<&str>) -> Result<(), ArgError> {
self.parse_field(key, value)
fn parse_into_field(&mut self, key: &str, value: Option<&[u8]>) -> Result<(), ArgError> {
self.parse_field_bytes(key, value)
}
}
@@ -901,8 +1005,8 @@ mod tests {
}
}
impl CliTarget for Quoted {
fn parse_into_field(&mut self, key: &str, value: Option<&str>) -> Result<(), ArgError> {
self.parse_field(key, value)
fn parse_into_field(&mut self, key: &str, value: Option<&[u8]>) -> Result<(), ArgError> {
self.parse_field_bytes(key, value)
}
}
@@ -913,8 +1017,8 @@ mod tests {
}
}
impl CliTarget for Reset {
fn parse_into_field(&mut self, key: &str, value: Option<&str>) -> Result<(), ArgError> {
self.parse_field(key, value)
fn parse_into_field(&mut self, key: &str, value: Option<&[u8]>) -> Result<(), ArgError> {
self.parse_field_bytes(key, value)
}
}
@@ -932,8 +1036,8 @@ mod tests {
}
}
impl CliTarget for Diag {
fn parse_into_field(&mut self, key: &str, value: Option<&str>) -> Result<(), ArgError> {
self.parse_field(key, value)
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)
@@ -1029,15 +1133,15 @@ mod tests {
pub a: bool,
}
impl CliTarget for Compat {
fn parse_into_field(&mut self, key: &str, value: Option<&str>) -> Result<(), ArgError> {
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_cli(value);
return self.a.parse_bytes(value);
}
Err(ArgError::InvalidField)
}
fn compatibility(&mut self, key: &str, value: Option<&str>) -> bool {
if key == "a" && value == Some("yuh") {
fn compatibility(&mut self, key: &str, value: Option<&[u8]>) -> bool {
if key == "a" && value == Some(b"yuh") {
self.a = true;
return true;
}
@@ -1057,14 +1161,14 @@ mod tests {
pub b: bool,
}
impl CliTarget for Renamed {
fn parse_into_field(&mut self, key: &str, value: Option<&str>) -> Result<(), ArgError> {
fn parse_into_field(&mut self, key: &str, value: Option<&[u8]>) -> Result<(), ArgError> {
match key {
"a" => self.a.parse_cli(value),
"b" => self.b.parse_cli(value),
"a" => self.a.parse_bytes(value),
"b" => self.b.parse_bytes(value),
_ => Err(ArgError::InvalidField),
}
}
fn compatibility(&mut self, key: &str, value: Option<&str>) -> bool {
fn compatibility(&mut self, key: &str, value: Option<&[u8]>) -> bool {
if key == "old" {
return compatibility_renamed(self, "a", value);
}
@@ -1086,12 +1190,12 @@ mod tests {
}
}
impl CliTarget for Underscore {
fn parse_into_field(&mut self, key: &str, value: Option<&str>) -> Result<(), ArgError> {
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(key, value)
self.parse_field_bytes(key, value)
}
}
@@ -1099,7 +1203,7 @@ mod tests {
fn parse_into_field_ignore_underscore_prefixed_fields() {
let mut data = Underscore::raw_default();
assert_eq!(
data.parse_into_field("_a", Some("42")),
data.parse_into_field("_a", Some(b"42")),
Err(ArgError::InvalidField)
);
assert_eq!(data.a, "12");
@@ -1117,7 +1221,8 @@ mod tests {
}
}
impl ConfigField for WithInit {
fn parse_cli(&mut self, _value: Option<&str>) -> Result<(), ArgError> {
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) {
@@ -1272,7 +1377,8 @@ mod tests {
pub v: String,
}
impl ConfigField for WithParse {
fn parse_cli(&mut self, _value: Option<&str>) -> Result<(), ArgError> {
fn parse_bytes(&mut self, _value: Option<&[u8]>) -> Result<(), ArgError> {
let _value = crate::cli_args::text_value(_value)?;
self.v = "HELLO!".into();
Ok(())
}
@@ -1313,7 +1419,8 @@ mod tests {
}
}
impl ConfigField for Basic {
fn parse_cli(&mut self, value: Option<&str>) -> 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 mut r = Basic::default();
let mut set = [false; 3];
@@ -1381,7 +1488,8 @@ mod tests {
D(String),
}
impl ConfigField for Tu {
fn parse_cli(&mut self, value: Option<&str>) -> 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 (tag, rest) = split_tagged_union(v);
*self = match tag {
@@ -1454,8 +1562,8 @@ mod tests {
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(), "--what");
assert_eq!(iter.next().unwrap(), "--a=42");
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);
}
@@ -1469,12 +1577,12 @@ mod tests {
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(), "--A");
assert_eq!(iter.next().unwrap(), "--B=42");
assert_eq!(iter.next().unwrap(), "--C");
assert_eq!(iter.next().unwrap(), "--D");
assert_eq!(iter.next().unwrap(), "--E");
assert_eq!(iter.next().unwrap(), "--F=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);
}
@@ -1482,7 +1590,7 @@ mod tests {
#[test]
fn line_iterator_end_in_newline() {
let mut iter = line_iter("A\n\n");
assert_eq!(iter.next().unwrap(), "--A");
assert_eq!(iter.next().unwrap(), b"--A");
assert_eq!(iter.next(), None);
assert_eq!(iter.next(), None);
}
@@ -1490,7 +1598,7 @@ mod tests {
#[test]
fn line_iterator_spaces_around_equals() {
let mut iter = line_iter("A = B\n\n");
assert_eq!(iter.next().unwrap(), "--A=B");
assert_eq!(iter.next().unwrap(), b"--A=B");
assert_eq!(iter.next(), None);
assert_eq!(iter.next(), None);
}
@@ -1498,15 +1606,15 @@ mod tests {
#[test]
fn line_iterator_no_value() {
let mut iter = line_iter("A = \n\n");
assert_eq!(iter.next().unwrap(), "--A=");
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(), "--A");
assert_eq!(iter.next().unwrap(), "--B=C");
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);
}
@@ -1515,8 +1623,8 @@ mod tests {
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(), "--A");
assert_eq!(iter.next().unwrap(), "--B=C");
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);
}
@@ -1527,8 +1635,8 @@ mod tests {
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(), "--A");
assert_eq!(iter.next().unwrap(), "--B=C");
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);
}
@@ -1558,21 +1666,16 @@ mod tests {
parse_zig_string_literal_bytes("\"a\\xFFb\"").unwrap(),
vec![b'a', 0xff, b'b']
);
let s = parse_zig_string_literal("\"a\\xFFb\"").unwrap();
assert_eq!(
ghostty_os::osbytes::config_str_to_bytes(&s),
vec![b'a', 0xff, b'b']
);
assert!(!s.contains('\u{fffd}'));
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 the escape encoding.
let raw = ghostty_os::osbytes::bytes_to_config_string(b"\"x\xfey\"");
// Raw (unescaped) invalid input bytes survive literal parsing.
let raw = b"\"x\xfey\"";
assert_eq!(
parse_zig_string_literal_bytes(&raw).unwrap(),
parse_zig_string_literal_bytes(raw).unwrap(),
vec![b'x', 0xfe, b'y']
);
}
@@ -1582,10 +1685,6 @@ mod tests {
let data: &[u8] = b"title = a\xffb\n";
let mut it = LineIterator::new(data);
let line = it.next().unwrap();
assert_eq!(
ghostty_os::osbytes::config_str_to_bytes(&line),
b"--title=a\xffb".to_vec()
);
assert!(!line.contains('\u{fffd}'));
assert_eq!(line, b"--title=a\xffb".to_vec());
}
}