fix: preserve non-UTF-8 bytes in OS, config and CLI layers

Zig treats paths, arguments and environment values as byte slices; the
port converted them lossily in many places. Now (final-Z1.md):
- argv, env (incl. present non-UTF-8 values), HOME, PATH lookup,
  passwd, resources dir, TMPDIR, cgroup, config path canonicalization,
  +validate-config, +list-themes and Zig-string escapes (\xFF) keep raw
  bytes. String-based config values carry bytes via ghostty_os::osbytes,
  a bijective escape (noncharacter U+FDD0 + byte) that leaves all valid
  UTF-8 - including Nerd Font private-use icons - unchanged.
- os/file rlimit fallbacks exist on all targets.
- i18n locale canonicalization is an exact port of libintl's tables
  (e.g. pa-Guru -> pa_IN).
- New ghostty-build-info crate is the single source of version, channel,
  commit and build mode for all consumers.

Test Plan:
- cargo test -p ghostty-os -p ghostty-config -p ghostty-cli
  (incl. randomized osbytes round-trip property tests)
This commit is contained in:
ddidderr committed 2026-10-12 08:14:02 +02:00
1 parent 3f3f6680a5
commit 8519f1ba1b
32 files changed
+1464 -333

No files matched your search

+18 -14
View File
@@ -3,7 +3,14 @@
//! Zig reads the resources dir from global state; here it is passed
//! explicitly (`resources_dir`, the value of `ResourcesDir.app()`).
use std::{fs::File, path::Path};
use std::fs::File;
use ghostty_os::osbytes::{
config_str_to_os,
config_str_to_path,
os_to_config_string,
path_to_config_string,
};
use crate::diagnostics::{Diagnostic, DiagnosticList};
@@ -35,12 +42,9 @@ impl Location {
.ok(),
Location::Resources => {
let base = resources_dir?;
Some(
Path::new(base)
.join("themes")
.to_string_lossy()
.into_owned(),
)
Some(path_to_config_string(
&config_str_to_path(base).join("themes"),
))
}
}
}
@@ -161,7 +165,7 @@ pub fn open(
diags: &mut DiagnosticList,
) -> Option<OpenedTheme> {
// Absolute themes are loaded a different path.
if Path::new(theme).is_absolute() {
if config_str_to_path(theme).is_absolute() {
let file = open_absolute(theme, diags)?;
if !check_regular(&file, theme, diags) {
return None;
@@ -172,9 +176,9 @@ pub fn open(
});
}
let basename = Path::new(theme)
let basename = config_str_to_path(theme)
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.map(os_to_config_string)
.unwrap_or_default();
if theme != basename {
diag(
@@ -190,8 +194,8 @@ pub fn open(
// one that exists.
let mut it = LocationIterator::new(resources_dir);
while let Some((_, dir)) = it.next_location() {
let path = Path::new(&dir).join(theme).to_string_lossy().into_owned();
match File::open(&path) {
let path = path_to_config_string(&config_str_to_path(&dir).join(config_str_to_os(theme)));
match File::open(config_str_to_path(&path)) {
Ok(file) => {
if !check_regular(&file, theme, diags) {
return None;
@@ -215,7 +219,7 @@ pub fn open(
// our iterator, reiterate over in order to build a better error message.
it.reset();
while let Some((_, dir)) = it.next_location() {
let path = Path::new(&dir).join(theme).to_string_lossy().into_owned();
let path = path_to_config_string(&config_str_to_path(&dir).join(config_str_to_os(theme)));
diag(
diags,
format!("theme \"{theme}\" not found, tried path \"{path}\""),
@@ -229,7 +233,7 @@ pub fn open(
/// then messages will be appended to the given error list and None is
/// returned.
pub fn open_absolute(theme: &str, diags: &mut DiagnosticList) -> Option<File> {
match File::open(theme) {
match File::open(config_str_to_path(theme)) {
Ok(f) => Some(f),
Err(err) => {
if err.kind() == std::io::ErrorKind::NotFound {