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

+13 -6
View File
@@ -74,12 +74,17 @@ pub enum ShapeRunError {
/// Create a CoreFoundation Dictionary suitable for settings the font
/// features of a CoreText font (`makeFeaturesDict`).
fn make_features_dict(feats: &[Feature]) -> CfBox<CFDictionary> {
fn make_features_dict(feats: &[Feature]) -> Result<CfBox<CFDictionary>, ShapeError> {
let mut items: Vec<CFRetained<CFDictionary<CFString, CFType>>> = Vec::new();
for feat in feats {
// Keys can only be ASCII.
let key = CFString::from_str(&String::from_utf8_lossy(&feat.tag));
if !feat.tag.is_ascii() {
return Err(ShapeError::CoreTextFailed);
}
let key = CFString::from_str(
std::str::from_utf8(&feat.tag).map_err(|_| ShapeError::CoreTextFailed)?,
);
let value = CFNumber::new_i32(feat.value as i32);
// SAFETY: valid static keys.
@@ -98,7 +103,9 @@ fn make_features_dict(feats: &[Feature]) -> CfBox<CFDictionary> {
let k = unsafe { kCTFontFeatureSettingsAttribute };
let dict = CFDictionary::<CFString, CFType>::from_slices(&[k], &[list.as_ref()]);
// SAFETY: the attribute dictionary is used as an untyped CFDictionary.
CfBox::new(unsafe { CFRetained::cast_unchecked::<CFDictionary>(dict) })
Ok(CfBox::new(unsafe {
CFRetained::cast_unchecked::<CFDictionary>(dict)
}))
}
/// Shaper that uses CoreText.
@@ -146,7 +153,7 @@ impl Shaper {
pub fn new(opts: &shape::Options) -> Result<Shaper, ShapeError> {
let mut feature_list = FeatureList::default();
for feature_str in &opts.features {
feature_list.append_from_string(feature_str);
feature_list.append_from_bytes(&feature_str.to_bytes());
}
// We need to construct two attrs dictionaries for font features;
@@ -155,8 +162,8 @@ impl Shaper {
let mut feats_df: Vec<Feature> = DEFAULT_FEATURES.to_vec();
feats_df.extend_from_slice(feats);
let features = make_features_dict(&feats_df);
let features_no_default = make_features_dict(feats);
let features = make_features_dict(&feats_df)?;
let features_no_default = make_features_dict(feats)?;
// For now we only support LTR text. If we shape RTL text then
// rendering will be very wrong so we need to explicitly force
+6 -2
View File
@@ -344,7 +344,11 @@ impl FeatureList {
/// Append features to this list from a string with a comma separated list.
/// See `Feature::from_reader` for more docs.
pub fn append_from_string(&mut self, s: &str) {
let mut reader = Reader::new(s.as_bytes());
self.append_from_bytes(s.as_bytes());
}
pub fn append_from_bytes(&mut self, s: &[u8]) {
let mut reader = Reader::new(s);
while !reader.at_end() {
let i = reader.pos;
if let Some(feature) = Feature::from_reader(&mut reader) {
@@ -352,7 +356,7 @@ impl FeatureList {
} else {
log::warn!(
"failed to parse font feature setting: \"{}\"",
String::from_utf8_lossy(&s.as_bytes()[i..reader.pos])
String::from_utf8_lossy(&s[i..reader.pos])
);
}
}
+2 -2
View File
@@ -76,7 +76,7 @@ impl Shaper {
let mut feature_list = FeatureList::default();
feature_list.features.extend_from_slice(&DEFAULT_FEATURES);
for feature_str in &opts.features {
feature_list.append_from_string(feature_str);
feature_list.append_from_bytes(&feature_str.to_bytes());
}
let hb_feats = feature_list
@@ -351,7 +351,7 @@ mod tests {
fn shape_ligatures_disabled_by_feature() {
let f = face(embedded::INCONSOLATA);
let mut s = Shaper::new(&shape::Options {
features: vec!["-liga".to_string()],
features: vec!["-liga".into()],
})
.unwrap();
let cells = s.shape_text(Some(&f), &text("===")).unwrap();
+2 -2
View File
@@ -93,7 +93,7 @@ pub(crate) fn test_shaper_with_font(font_req: TestFont) -> TestShaper {
let grid = SharedGrid::new(CodepointResolver::new(c)).unwrap();
let shaper = Shaper::new(&shape::Options {
// Some of our tests rely on dlig being enabled by default
features: vec!["dlig".to_string()],
features: vec!["dlig".into()],
})
.unwrap();
@@ -112,7 +112,7 @@ pub(crate) fn test_shaper_with_discovered_font(font_req: &str) -> Option<TestSha
let disco = Fontconfig::init();
let mut it = disco
.discover(&Descriptor {
family: Some(font_req.to_string()),
family: Some(font_req.into()),
size: 12.0,
monospace: false,
..Descriptor::default()