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

+32 -8
View File
@@ -2,9 +2,11 @@
use std::{
fmt,
path::{Component, Path as StdPath, PathBuf},
path::{Component, PathBuf},
};
use ghostty_os::osbytes::{config_str_to_path, path_to_config_string};
use crate::{
c_value::CValue,
cli_args::{ArgError, ConfigField},
@@ -32,7 +34,7 @@ impl Default for Path {
/// Lexically resolve `base/path` like `std.fs.path.resolve`.
fn resolve_lexical(base: &str, path: &str) -> String {
let joined = PathBuf::from(base).join(path);
let joined = config_str_to_path(base).join(config_str_to_path(path));
let mut out = PathBuf::new();
for c in joined.components() {
match c {
@@ -43,7 +45,7 @@ fn resolve_lexical(base: &str, path: &str) -> String {
other => out.push(other.as_os_str()),
}
}
out.to_string_lossy().into_owned()
path_to_config_string(&out)
}
impl Path {
@@ -109,12 +111,15 @@ impl Path {
/// absolute path. Errors are added to `diags` if they occur; an `Err` is
/// only returned if the base directory cannot be opened.
pub fn expand(&mut self, base: &str, diags: &mut DiagnosticList) -> std::io::Result<()> {
assert!(StdPath::new(base).is_absolute());
// Config strings carry non-UTF-8 bytes via a reversible escape
// encoding; filesystem calls use the decoded OS paths.
let base_path = config_str_to_path(base);
assert!(base_path.is_absolute());
let path = self.path().to_string();
// If it is already absolute we can ignore it.
if path.is_empty() || StdPath::new(&path).is_absolute() {
if path.is_empty() || config_str_to_path(&path).is_absolute() {
return Ok(());
}
@@ -143,11 +148,11 @@ impl Path {
}
// Make sure the base directory can be opened.
std::fs::read_dir(base)?;
std::fs::read_dir(&base_path)?;
let full = StdPath::new(base).join(&path);
let full = base_path.join(config_str_to_path(&path));
let abs = match std::fs::canonicalize(&full) {
Ok(p) => p.to_string_lossy().into_owned(),
Ok(p) => path_to_config_string(&p),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
// The file doesn't exist. Try to resolve the relative path
// another way.
@@ -374,4 +379,23 @@ mod tests {
assert_eq!(p, Path::Optional("/abs/path".into()));
assert!(diags.is_empty());
}
#[cfg(unix)]
#[test]
fn expand_symlink_to_non_utf8_target_round_trips() {
use std::os::unix::ffi::OsStrExt;
let tmp = ghostty_os::TempDir::init().unwrap();
let base = std::fs::canonicalize(tmp.path()).unwrap();
let target = base.join(std::ffi::OsStr::from_bytes(b"t\xffarget"));
std::fs::write(&target, b"x").unwrap();
std::os::unix::fs::symlink(&target, base.join("link")).unwrap();
let mut p = Path::Required("link".into());
let mut diags = DiagnosticList::default();
p.expand(&path_to_config_string(&base), &mut diags).unwrap();
assert_eq!(diags.items().len(), 0);
// The canonical (non-UTF-8) target is preserved byte for byte.
assert_eq!(config_str_to_path(p.path()), target);
assert!(std::fs::metadata(config_str_to_path(p.path())).is_ok());
}
}