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:
1 parent
e4cdd8e007
commit
395cff8981
32 files changed
+1464
-333
No files matched your search
@@ -6,6 +6,7 @@ license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
flate2 = "1.1.10"
|
||||
ghostty-build-info.workspace = true
|
||||
ghostty-apprt.workspace = true
|
||||
ghostty-config.workspace = true
|
||||
ghostty-crash.workspace = true
|
||||
|
||||
+2
-133
@@ -1,130 +1,15 @@
|
||||
//! Build metadata for `+version` and the generated docs.
|
||||
//!
|
||||
//! * the compiler version (Zig: `builtin.zig_version_string`)
|
||||
//! * the app version, release channel and commit (Zig: `build/Config.zig`
|
||||
//! version detection + `build/GitVersion.zig`)
|
||||
//! * the build mode (Zig: `builtin.mode`)
|
||||
//!
|
||||
//! The version can be overridden with `GHOSTTY_VERSION_STRING` (Zig:
|
||||
//! `-Dversion-string`), which must be a semantic version.
|
||||
//! The app version, channel, commit and build mode live in
|
||||
//! `ghostty-build-info` (single source of truth).
|
||||
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
fn run(cmd: &str, args: &[&str], dir: &Path) -> Option<String> {
|
||||
let out = Command::new(cmd)
|
||||
.args(args)
|
||||
.current_dir(dir)
|
||||
.output()
|
||||
.ok()?;
|
||||
out.status
|
||||
.success()
|
||||
.then(|| String::from_utf8_lossy(&out.stdout).trim_end().to_string())
|
||||
}
|
||||
|
||||
/// The `app-version` from `[workspace.metadata.ghostty]` of the workspace
|
||||
/// manifest (two levels above this crate).
|
||||
fn app_version(manifest_dir: &Path) -> (u64, u64, u64) {
|
||||
let root = manifest_dir.join("../../Cargo.toml");
|
||||
println!("cargo:rerun-if-changed={}", root.display());
|
||||
let text = std::fs::read_to_string(root).unwrap_or_default();
|
||||
let v = text
|
||||
.lines()
|
||||
.find_map(|l| l.trim().strip_prefix("app-version"))
|
||||
.and_then(|l| l.split('"').nth(1))
|
||||
.unwrap_or("1.3.1");
|
||||
let mut it = v.split('.').map(|p| p.parse::<u64>().unwrap_or(0));
|
||||
(
|
||||
it.next().unwrap_or(1),
|
||||
it.next().unwrap_or(3),
|
||||
it.next().unwrap_or(1),
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns `(version_string, commit)` mirroring `Config.zig`.
|
||||
fn detect_version(manifest_dir: &Path) -> (String, Option<String>) {
|
||||
println!("cargo:rerun-if-env-changed=GHOSTTY_VERSION_STRING");
|
||||
if let Ok(v) = std::env::var("GHOSTTY_VERSION_STRING") {
|
||||
let commit = v.split_once('+').map(|(_, b)| b.to_string());
|
||||
return (v, commit);
|
||||
}
|
||||
|
||||
let (maj, min, pat) = app_version(manifest_dir);
|
||||
let base = format!("{maj}.{min}.{pat}");
|
||||
|
||||
// Git unavailable or not a repository: unknown dev version.
|
||||
let Some(branch) = run("git", &["rev-parse", "--abbrev-ref", "HEAD"], manifest_dir) else {
|
||||
return (format!("{base}-dev+0000000"), Some("0000000".into()));
|
||||
};
|
||||
if let Some(git_dir) = run("git", &["rev-parse", "--absolute-git-dir"], manifest_dir) {
|
||||
let git_dir = PathBuf::from(git_dir);
|
||||
println!("cargo:rerun-if-changed={}", git_dir.join("HEAD").display());
|
||||
println!("cargo:rerun-if-changed={}", git_dir.join("refs").display());
|
||||
println!(
|
||||
"cargo:rerun-if-changed={}",
|
||||
git_dir.join("packed-refs").display()
|
||||
);
|
||||
}
|
||||
let hash = run(
|
||||
"git",
|
||||
&[
|
||||
"-c",
|
||||
"log.showSignature=false",
|
||||
"log",
|
||||
"--pretty=format:%h",
|
||||
"-n",
|
||||
"1",
|
||||
],
|
||||
manifest_dir,
|
||||
)
|
||||
.unwrap_or_default();
|
||||
let tag = run(
|
||||
"git",
|
||||
&["describe", "--exact-match", "--tags"],
|
||||
manifest_dir,
|
||||
)
|
||||
.filter(|t| !t.is_empty());
|
||||
|
||||
if let Some(tag) = tag {
|
||||
// Tip releases behave just like any other pre-release so we skip.
|
||||
if tag != "tip" {
|
||||
assert!(
|
||||
tag == format!("v{base}"),
|
||||
"tagged releases must be in vX.Y.Z format matching the workspace app-version"
|
||||
);
|
||||
return (base, None);
|
||||
}
|
||||
}
|
||||
|
||||
// Slashes in branch names are replaced like in `GitVersion.zig`.
|
||||
let branch = branch.replace('/', "-");
|
||||
(format!("{base}-{branch}+{hash}"), Some(hash))
|
||||
}
|
||||
|
||||
/// The Zig optimize-mode name for the Cargo profile being built.
|
||||
fn build_mode() -> &'static str {
|
||||
// The profile name is the directory below `target/` that holds OUT_DIR.
|
||||
let out = std::env::var("OUT_DIR").unwrap_or_default();
|
||||
let profile = Path::new(&out)
|
||||
.ancestors()
|
||||
.skip_while(|p| p.file_name().is_none_or(|n| n != "build"))
|
||||
.nth(1)
|
||||
.and_then(|p| p.file_name())
|
||||
.map(|n| n.to_string_lossy().into_owned())
|
||||
.unwrap_or_default();
|
||||
let debug_assertions = std::env::var_os("CARGO_CFG_DEBUG_ASSERTIONS").is_some();
|
||||
let opt = std::env::var("OPT_LEVEL").unwrap_or_default();
|
||||
if profile == "production" || !debug_assertions {
|
||||
".ReleaseFast"
|
||||
} else if opt == "0" {
|
||||
".Debug"
|
||||
} else {
|
||||
".ReleaseSafe"
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates the per-action help table (Zig: `helpgen.zig` `genActions`):
|
||||
/// the doc comment of each action's `run` function becomes
|
||||
/// `Action::help_text`.
|
||||
@@ -175,20 +60,4 @@ fn main() {
|
||||
.and_then(|s| s.split_whitespace().nth(1).map(str::to_owned))
|
||||
.unwrap_or_else(|| "unknown".into());
|
||||
println!("cargo:rustc-env=GHOSTTY_RUSTC_VERSION={version}");
|
||||
|
||||
let manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap_or_default());
|
||||
let (version_string, commit) = detect_version(&manifest_dir);
|
||||
// Release channel: stable unless the version has a pre-release part.
|
||||
let channel = if version_string.contains('-') {
|
||||
"tip"
|
||||
} else {
|
||||
"stable"
|
||||
};
|
||||
println!("cargo:rustc-env=GHOSTTY_VERSION_STRING={version_string}");
|
||||
println!(
|
||||
"cargo:rustc-env=GHOSTTY_COMMIT={}",
|
||||
commit.unwrap_or_default()
|
||||
);
|
||||
println!("cargo:rustc-env=GHOSTTY_RELEASE_CHANNEL={channel}");
|
||||
println!("cargo:rustc-env=GHOSTTY_BUILD_MODE={}", build_mode());
|
||||
}
|
||||
@@ -53,7 +53,7 @@ pub trait ActionEnum: Sized + Copy {
|
||||
|
||||
/// Detect the action from the process CLI args (Zig: `detectArgs`).
|
||||
pub fn detect_args<E: ActionEnum>() -> Result<Option<E>, DetectError> {
|
||||
detect_iter(ghostty_os::args::collect_lossy())
|
||||
detect_iter(ghostty_os::args::collect_config_strings())
|
||||
}
|
||||
|
||||
/// Detect the action from any iterator. Each iterator value should yield
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
/// Re-export of the derived version constants (`VERSION_STRING`, `COMMIT`,
|
||||
/// `RELEASE_CHANNEL`, `BUILD_MODE`) from `ghostty-build-info`.
|
||||
pub use ghostty_build_info as derived;
|
||||
|
||||
/// A `major.minor.micro` version (Zig: `gtk_version.zig` / `adw_version.zig`).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Version {
|
||||
@@ -49,11 +53,11 @@ pub struct BuildInfo {
|
||||
impl Default for BuildInfo {
|
||||
fn default() -> Self {
|
||||
BuildInfo {
|
||||
// Derived by `build.rs` (git describe / workspace app-version).
|
||||
version_string: env!("GHOSTTY_VERSION_STRING").to_string(),
|
||||
commit: Some(env!("GHOSTTY_COMMIT").to_string()).filter(|c| !c.is_empty()),
|
||||
release_channel: env!("GHOSTTY_RELEASE_CHANNEL").to_string(),
|
||||
build_mode: env!("GHOSTTY_BUILD_MODE").to_string(),
|
||||
// Derived by `ghostty-build-info` (single source of truth).
|
||||
version_string: ghostty_build_info::VERSION_STRING.to_string(),
|
||||
commit: ghostty_build_info::COMMIT.map(str::to_string),
|
||||
release_channel: ghostty_build_info::RELEASE_CHANNEL.to_string(),
|
||||
build_mode: ghostty_build_info::BUILD_MODE.to_string(),
|
||||
app_runtime: ".gtk".to_string(),
|
||||
font_backend: ".fontconfig_freetype".to_string(),
|
||||
renderer: "renderer.generic.Renderer(renderer.OpenGL)".to_string(),
|
||||
|
||||
@@ -93,8 +93,7 @@ fn exec_editor(stderr: &mut dyn Write, path: &str) -> Result<u8, Error> {
|
||||
let Some(editor) = find_editor() else {
|
||||
// If we don't have `$EDITOR` set then we can't do anything
|
||||
// but we can still print a helpful message.
|
||||
write!(
|
||||
stderr,
|
||||
let msg = String::from(
|
||||
"The $EDITOR or $VISUAL environment variable is not set or is empty.\n\
|
||||
This environment variable is required to edit the Ghostty configuration\n\
|
||||
via this CLI command.\n\
|
||||
@@ -104,17 +103,20 @@ fn exec_editor(stderr: &mut dyn Write, path: &str) -> Result<u8, Error> {
|
||||
\n\
|
||||
If you prefer to edit the configuration file another way,\n\
|
||||
you can find the configuration file at the following path:\n\
|
||||
\n"
|
||||
)?;
|
||||
|
||||
\n",
|
||||
);
|
||||
// Output the path using the OSC8 sequence so that it is linked.
|
||||
writeln!(stderr, "\x1b]8;;file://{path}\x1b\\{path}\x1b]8;;\x1b\\")?;
|
||||
// (Paths are raw bytes: decode the reversible escape when writing.)
|
||||
let msg = format!("{msg}\x1b]8;;file://{path}\x1b\\{path}\x1b]8;;\x1b\\\n");
|
||||
stderr.write_all(&ghostty_os::osbytes::config_str_to_bytes(&msg))?;
|
||||
return Ok(1);
|
||||
};
|
||||
|
||||
// Flush before exec so no output is lost.
|
||||
let _ = stderr.flush();
|
||||
let err = std::process::Command::new(&editor).arg(path).exec();
|
||||
let err = std::process::Command::new(ghostty_os::osbytes::config_str_to_os(&editor))
|
||||
.arg(ghostty_os::osbytes::config_str_to_os(path))
|
||||
.exec();
|
||||
|
||||
// If we reached this point then exec failed.
|
||||
let err = match err.kind() {
|
||||
@@ -122,8 +124,7 @@ fn exec_editor(stderr: &mut dyn Write, path: &str) -> Result<u8, Error> {
|
||||
std::io::ErrorKind::PermissionDenied => "error.AccessDenied".to_string(),
|
||||
_ => err.to_string(),
|
||||
};
|
||||
write!(
|
||||
stderr,
|
||||
let msg = format!(
|
||||
"Failed to execute the editor. Error code={err}.\n\
|
||||
\n\
|
||||
This is usually due to the executable path not existing, invalid\n\
|
||||
@@ -132,6 +133,7 @@ fn exec_editor(stderr: &mut dyn Write, path: &str) -> Result<u8, Error> {
|
||||
\n\
|
||||
Editor: {editor}\n\
|
||||
Path: {path}\n"
|
||||
)?;
|
||||
);
|
||||
stderr.write_all(&ghostty_os::osbytes::config_str_to_bytes(&msg))?;
|
||||
Ok(1)
|
||||
}
|
||||
@@ -243,6 +243,28 @@ pub fn detect(args: &[String]) -> Result<Option<Action>, DetectError> {
|
||||
detect_iter::<Action, _, _>(args.iter())
|
||||
}
|
||||
|
||||
/// Convert raw OS arguments (Zig `[]const u8` argv) into the argument strings
|
||||
/// used by the CLI layers. No bytes are lost: invalid UTF-8 bytes use the
|
||||
/// reversible escape encoding of `ghostty_os::osbytes`, which the config
|
||||
/// layer (`cli_args`) decodes back to the original bytes.
|
||||
#[must_use]
|
||||
pub fn os_args_to_strings(args: &[std::ffi::OsString]) -> Vec<String> {
|
||||
args.iter()
|
||||
.map(|a| ghostty_os::osbytes::os_to_config_string(a))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// [`detect`] over raw OS arguments.
|
||||
pub fn detect_os(args: &[std::ffi::OsString]) -> Result<Option<Action>, DetectError> {
|
||||
detect(&os_args_to_strings(args))
|
||||
}
|
||||
|
||||
/// [`run_from_args`] over raw OS arguments (byte-preserving).
|
||||
#[must_use]
|
||||
pub fn run_from_os_args(args: &[std::ffi::OsString]) -> Option<u8> {
|
||||
run_from_args(&os_args_to_strings(args))
|
||||
}
|
||||
|
||||
/// The CLI entrypoint logic of `main_ghostty.zig` for actions: detect the
|
||||
/// action from `args` (the full argv), print the errors Ghostty prints for
|
||||
/// invalid invocations and run the action.
|
||||
@@ -338,6 +360,23 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn os_args_keep_non_utf8_bytes_and_still_dispatch() {
|
||||
use std::os::unix::ffi::OsStringExt;
|
||||
let args = vec![
|
||||
std::ffi::OsString::from("ghostty"),
|
||||
std::ffi::OsString::from_vec(b"--title=\xff".to_vec()),
|
||||
std::ffi::OsString::from("+version"),
|
||||
];
|
||||
assert_eq!(Some(Action::Version), detect_os(&args).unwrap());
|
||||
let strings = os_args_to_strings(&args);
|
||||
assert_eq!(
|
||||
ghostty_os::osbytes::config_str_to_bytes(&strings[1]),
|
||||
b"--title=\xff"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn names_round_trip() {
|
||||
for a in Action::ALL {
|
||||
|
||||
@@ -15,6 +15,12 @@ use ghostty_config::{
|
||||
config_struct,
|
||||
theme::{Location, LocationIterator},
|
||||
};
|
||||
use ghostty_os::osbytes::{
|
||||
config_str_to_bytes,
|
||||
config_str_to_path,
|
||||
os_to_config_string,
|
||||
path_to_config_string,
|
||||
};
|
||||
|
||||
use crate::{error::Error, tui};
|
||||
|
||||
@@ -121,7 +127,7 @@ pub fn collect_themes(resources_dir: Option<&str>) -> Vec<ThemeListElement> {
|
||||
let mut themes = vec![];
|
||||
let mut it = LocationIterator::new(resources_dir);
|
||||
while let Some((location, dir)) = it.next_location() {
|
||||
let rd = match std::fs::read_dir(&dir) {
|
||||
let rd = match std::fs::read_dir(config_str_to_path(&dir)) {
|
||||
Ok(rd) => rd,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
|
||||
Err(e) => {
|
||||
@@ -137,15 +143,14 @@ pub fn collect_themes(resources_dir: Option<&str>) -> Vec<ThemeListElement> {
|
||||
if !(ft.is_file() || ft.is_symlink()) {
|
||||
continue;
|
||||
}
|
||||
let name = entry.file_name().to_string_lossy().into_owned();
|
||||
// Entry names are raw bytes: keep them via the reversible escape.
|
||||
let raw_name = entry.file_name();
|
||||
let name = os_to_config_string(&raw_name);
|
||||
if name == ".DS_Store" {
|
||||
continue;
|
||||
}
|
||||
themes.push(ThemeListElement {
|
||||
path: std::path::Path::new(&dir)
|
||||
.join(&name)
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
path: path_to_config_string(&config_str_to_path(&dir).join(&raw_name)),
|
||||
location,
|
||||
theme: name,
|
||||
rank: None,
|
||||
@@ -170,8 +175,8 @@ pub fn should_include_theme(theme_filter: ColorScheme, theme_config: &Config) ->
|
||||
|
||||
/// Make `path` absolute (`Config::load_file` requires absolute paths).
|
||||
pub(crate) fn absolute_path(path: &str) -> String {
|
||||
std::path::absolute(path)
|
||||
.map_or_else(|_| path.to_string(), |p| p.to_string_lossy().into_owned())
|
||||
std::path::absolute(config_str_to_path(path))
|
||||
.map_or_else(|_| path.to_string(), |p| path_to_config_string(&p))
|
||||
}
|
||||
|
||||
fn resources_dir() -> Option<String> {
|
||||
@@ -273,11 +278,14 @@ pub fn write_plain(
|
||||
continue;
|
||||
}
|
||||
let loc = location_name(theme.location);
|
||||
if opts.path {
|
||||
writeln!(out, "{} ({loc}) {}", theme.theme, theme.path)?;
|
||||
// Names/paths are raw bytes in Zig: decode the reversible escape so
|
||||
// non-UTF-8 bytes are written back unchanged.
|
||||
let line = if opts.path {
|
||||
format!("{} ({loc}) {}\n", theme.theme, theme.path)
|
||||
} else {
|
||||
writeln!(out, "{} ({loc})", theme.theme)?;
|
||||
}
|
||||
format!("{} ({loc})\n", theme.theme)
|
||||
};
|
||||
out.write_all(&config_str_to_bytes(&line))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -297,6 +305,29 @@ mod tests {
|
||||
tmp
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn collect_themes_keeps_non_utf8_names() {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dir = tmp.path().join("themes");
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let raw = std::ffi::OsStr::from_bytes(b"th\xffeme");
|
||||
std::fs::write(dir.join(raw), "background = #000000\n").unwrap();
|
||||
let res = tmp.path().to_string_lossy().into_owned();
|
||||
let themes: Vec<_> = collect_themes(Some(&res))
|
||||
.into_iter()
|
||||
.filter(|t| t.location == Location::Resources)
|
||||
.collect();
|
||||
assert_eq!(1, themes.len());
|
||||
assert_eq!(config_str_to_bytes(&themes[0].theme), b"th\xffeme".to_vec());
|
||||
// The path points at the real file and the listing loads it.
|
||||
assert_eq!(config_str_to_path(&themes[0].path), dir.join(raw));
|
||||
let mut out = Vec::new();
|
||||
write_plain(&mut out, &themes, &Options::default()).unwrap();
|
||||
assert_eq!(out, b"th\xffeme (resources)\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collect_sort_and_filter() {
|
||||
let tmp = theme_dir(&[
|
||||
|
||||
@@ -1408,14 +1408,13 @@ fn error_name(e: &ghostty_config::config::Error) -> String {
|
||||
pub fn resolve_auto_theme_path() -> Result<String, Error> {
|
||||
let main_cfg_path = ghostty_config::file_load::preferred_default_file_path()
|
||||
.map_err(|e| Error::Other(e.to_string()))?;
|
||||
let base_dir = std::path::Path::new(&main_cfg_path)
|
||||
let main_cfg_path = ghostty_os::osbytes::config_str_to_path(&main_cfg_path);
|
||||
let base_dir = main_cfg_path
|
||||
.parent()
|
||||
.ok_or_else(|| Error::Other("BadPathName".to_string()))?;
|
||||
Ok(base_dir
|
||||
.join("auto")
|
||||
.join("theme.ghostty")
|
||||
.to_string_lossy()
|
||||
.into_owned())
|
||||
Ok(ghostty_os::osbytes::path_to_config_string(
|
||||
&base_dir.join("auto").join("theme.ghostty"),
|
||||
))
|
||||
}
|
||||
|
||||
/// `writeAutoThemeFile`.
|
||||
@@ -1424,10 +1423,14 @@ pub fn write_auto_theme_file(path: Option<&str>, theme_name: &str) -> Result<(),
|
||||
Some(p) => p.to_string(),
|
||||
None => resolve_auto_theme_path()?,
|
||||
};
|
||||
if let Some(dir) = std::path::Path::new(&auto_path).parent() {
|
||||
let auto_path = ghostty_os::osbytes::config_str_to_path(&auto_path);
|
||||
if let Some(dir) = auto_path.parent() {
|
||||
std::fs::create_dir_all(dir)?;
|
||||
}
|
||||
std::fs::write(&auto_path, format!("theme = {theme_name}\n"))?;
|
||||
std::fs::write(
|
||||
&auto_path,
|
||||
ghostty_os::osbytes::config_str_to_bytes(&format!("theme = {theme_name}\n")),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1734,7 +1737,11 @@ fn run_loop(app: &mut Preview) -> Result<(), Error> {
|
||||
match effect {
|
||||
Effect::Copy(text) => {
|
||||
let out = terminal.backend_mut();
|
||||
write!(out, "\x1b]52;c;{}\x1b\\", base64(text.as_bytes()))?;
|
||||
write!(
|
||||
out,
|
||||
"\x1b]52;c;{}\x1b\\",
|
||||
base64(&ghostty_os::osbytes::config_str_to_bytes(&text))
|
||||
)?;
|
||||
out.flush()?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,11 +59,12 @@ impl Options {
|
||||
let expanded =
|
||||
ghostty_os::expand_home(stripped).map_err(|e| ArgError::Other(e.to_string()))?;
|
||||
let realpath =
|
||||
std::fs::canonicalize(&expanded).map_err(|e| ArgError::Other(e.to_string()))?;
|
||||
std::fs::canonicalize(ghostty_os::osbytes::config_str_to_path(&expanded))
|
||||
.map_err(|e| ArgError::Other(e.to_string()))?;
|
||||
self.working_directory_seen = true;
|
||||
return Ok(Some(format!(
|
||||
"--working-directory={}",
|
||||
realpath.to_string_lossy()
|
||||
ghostty_os::osbytes::path_to_config_string(&realpath)
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -192,8 +193,13 @@ pub fn run(args: &[String]) -> Result<u8, Error> {
|
||||
if !opts.working_directory_seen {
|
||||
// This should be inserted at the beginning of the list, just in case `-e` was used.
|
||||
let wd = std::fs::canonicalize(".")?;
|
||||
opts.arguments
|
||||
.insert(0, format!("--working-directory={}", wd.to_string_lossy()));
|
||||
opts.arguments.insert(
|
||||
0,
|
||||
format!(
|
||||
"--working-directory={}",
|
||||
ghostty_os::osbytes::path_to_config_string(&wd)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
let target = match opts.class {
|
||||
|
||||
@@ -39,8 +39,10 @@ fn run_inner(opts: &Options, stdout: &mut dyn Write) -> Result<u8, Error> {
|
||||
|
||||
// If a config path is passed, validate it, otherwise validate default configs
|
||||
if let Some(config_path) = &opts.config_file {
|
||||
let abs_path = std::fs::canonicalize(config_path)?;
|
||||
cfg.load_file(&abs_path.to_string_lossy())?;
|
||||
// Byte-preserving: the option string carries non-UTF-8 bytes via the
|
||||
// reversible `osbytes` escape and the canonical path may too.
|
||||
let abs_path = std::fs::canonicalize(ghostty_os::osbytes::config_str_to_path(config_path))?;
|
||||
cfg.load_file(&ghostty_os::osbytes::path_to_config_string(&abs_path))?;
|
||||
cfg.load_recursive_files()?;
|
||||
} else {
|
||||
cfg = Config::load()?;
|
||||
@@ -109,4 +111,21 @@ mod tests {
|
||||
};
|
||||
assert!(run_inner(&opts, &mut Vec::new()).is_err());
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn validate_symlink_to_non_utf8_target() {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let target = tmp.path().join(std::ffi::OsStr::from_bytes(b"cfg\xff"));
|
||||
std::fs::write(&target, "font-size = 14\n").unwrap();
|
||||
let link = tmp.path().join("link");
|
||||
std::os::unix::fs::symlink(&target, &link).unwrap();
|
||||
let opts = Options {
|
||||
config_file: Some(link.to_string_lossy().into_owned()),
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
assert_eq!(0, run_inner(&opts, &mut out).unwrap());
|
||||
assert_eq!(out.len(), 0);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user