fix: close remaining gaps found by the completeness audit
Addresses the partial ports listed in docs/rewrite/notes/review-completeness-codex.md (details in docs/rewrite/notes/gaps-X2.md): - Flatpak: the exit watcher no longer consumes the host command; a shared FlatpakSignalHandle lets closing a surface still SIGHUP the host process group. - Child environment is byte-preserving end to end (ghostty_os::EnvMap of OsStrings) and Snap paths are filtered like filterSnapPaths. - argv handling no longer panics on non-UTF-8 arguments. - URI percent-decoding keeps raw bytes; OSC 7 pwd is passed through without lossy conversion. - Keybinding comparison/hashing uses full Unicode case folding from a table generated from CaseFolding.txt (Unicode 17.0.0). - Link regexes set Oniguruma's whole-search retry limit through the new minimal ghostty-onig crate (one SAFETY-commented FFI call). - OSC 9 NUL-capacity check ordering matches Zig. - Wayland registry binds/clears globals advertised after startup. - Custom shaders may pass sampler2D to helper functions (naga adapter). - +list-themes preview renders OSC 8 links, styled underlines and follows color-scheme reports. Also includes the inspector's integration edits in shared files (renderer state hook, termio VT recording, core/apprt-gtk wiring), X1's config/input/cli additions for completion generation, and a fix for a race in the termio backpressure test (it wrote before `stty raw` applied, so the cooked line discipline dropped bytes past MAX_CANON). Test Plan: - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings
This commit is contained in:
1 parent
90ad602969
commit
f4c020101c
61 files changed
+4210
-298
No files matched your search
+201
-11
@@ -1,24 +1,153 @@
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::{OsStr, OsString};
|
||||
|
||||
use crate::flatpak::is_flatpak;
|
||||
|
||||
/// PATH-style delimiter (`std.fs.path.delimiter` on POSIX).
|
||||
const DELIMITER: char = ':';
|
||||
|
||||
/// A byte-preserving environment map (Zig `std.process.EnvMap`): keys and
|
||||
/// values are arbitrary OS strings, never required to be UTF-8.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct EnvMap(HashMap<OsString, OsString>);
|
||||
|
||||
impl EnvMap {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, key: impl AsRef<OsStr>, value: impl AsRef<OsStr>) -> Option<OsString> {
|
||||
self.0
|
||||
.insert(key.as_ref().to_owned(), value.as_ref().to_owned())
|
||||
}
|
||||
|
||||
pub fn get(&self, key: impl AsRef<OsStr>) -> Option<&OsStr> {
|
||||
self.0.get(key.as_ref()).map(OsString::as_os_str)
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, key: impl AsRef<OsStr>) -> Option<OsString> {
|
||||
self.0.remove(key.as_ref())
|
||||
}
|
||||
|
||||
pub fn contains_key(&self, key: impl AsRef<OsStr>) -> bool {
|
||||
self.0.contains_key(key.as_ref())
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = (&OsStr, &OsStr)> {
|
||||
self.0.iter().map(|(k, v)| (k.as_os_str(), v.as_os_str()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: AsRef<OsStr>> std::ops::Index<K> for EnvMap {
|
||||
type Output = OsStr;
|
||||
|
||||
fn index(&self, key: K) -> &OsStr {
|
||||
self.get(key).expect("no entry found for key")
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Into<OsString>, V: Into<OsString>> FromIterator<(K, V)> for EnvMap {
|
||||
fn from_iter<T: IntoIterator<Item = (K, V)>>(iter: T) -> Self {
|
||||
Self(
|
||||
iter.into_iter()
|
||||
.map(|(k, v)| (k.into(), v.into()))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for EnvMap {
|
||||
type Item = (OsString, OsString);
|
||||
type IntoIter = std::collections::hash_map::IntoIter<OsString, OsString>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the environment map. Inside Flatpak this is empty (the sandbox
|
||||
/// environment is not meaningful for host commands).
|
||||
pub fn get_env_map() -> HashMap<String, String> {
|
||||
/// environment is not meaningful for host commands). Bytes are preserved.
|
||||
pub fn get_env_map() -> EnvMap {
|
||||
if is_flatpak() {
|
||||
return HashMap::new();
|
||||
return EnvMap::new();
|
||||
}
|
||||
std::env::vars_os().collect()
|
||||
}
|
||||
|
||||
/// Filter out Snap-specific environment variables and path components
|
||||
/// (port of `Surface.filterSnapPaths`): remove any `SNAP_*` variable and
|
||||
/// drop `:`-separated components that start with one of the snap
|
||||
/// directories. Variables we set ourselves (`TERMINFO`, `GHOSTTY*`) are
|
||||
/// left alone.
|
||||
pub fn filter_snap_paths(env: &mut EnvMap) {
|
||||
use std::os::unix::ffi::{OsStrExt, OsStringExt};
|
||||
|
||||
const SNAP_VARS: [&str; 5] = [
|
||||
"SNAP",
|
||||
"SNAP_USER_COMMON",
|
||||
"SNAP_USER_DATA",
|
||||
"SNAP_DATA",
|
||||
"SNAP_COMMON",
|
||||
];
|
||||
|
||||
let mut to_remove: Vec<OsString> = Vec::new();
|
||||
let mut to_update: Vec<(OsString, OsString)> = Vec::new();
|
||||
|
||||
for (key, value) in env.iter() {
|
||||
let kb = key.as_bytes();
|
||||
if kb == b"TERMINFO" || kb.starts_with(b"GHOSTTY") {
|
||||
continue;
|
||||
}
|
||||
if kb.starts_with(b"SNAP_") {
|
||||
to_remove.push(key.to_owned());
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut filtered: Vec<&[u8]> = Vec::new();
|
||||
let mut modified = false;
|
||||
for path in value.as_bytes().split(|&b| b == b':') {
|
||||
let mut include = true;
|
||||
for k in SNAP_VARS {
|
||||
if let Some(snap_path) = env.get(k) {
|
||||
let snap_path = snap_path.as_bytes();
|
||||
if snap_path.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if path.starts_with(snap_path) {
|
||||
include = false;
|
||||
modified = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if include {
|
||||
filtered.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
if modified {
|
||||
if filtered.is_empty() {
|
||||
to_remove.push(key.to_owned());
|
||||
} else {
|
||||
to_update.push((key.to_owned(), OsString::from_vec(filtered.join(&b':'))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (k, v) in to_update {
|
||||
env.insert(k, v);
|
||||
}
|
||||
for k in to_remove {
|
||||
env.remove(k);
|
||||
}
|
||||
std::env::vars_os()
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
k.to_string_lossy().into_owned(),
|
||||
v.to_string_lossy().into_owned(),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Append a value to an environment variable such as PATH.
|
||||
@@ -39,6 +168,28 @@ pub fn append_env_always(current: &str, value: &str) -> String {
|
||||
format!("{current}{DELIMITER}{value}")
|
||||
}
|
||||
|
||||
/// Byte-preserving [`prepend_env`].
|
||||
pub fn prepend_env_os(current: &OsStr, value: &OsStr) -> OsString {
|
||||
if current.is_empty() {
|
||||
return value.to_owned();
|
||||
}
|
||||
let mut out = value.to_owned();
|
||||
out.push(DELIMITER.to_string());
|
||||
out.push(current);
|
||||
out
|
||||
}
|
||||
|
||||
/// Byte-preserving [`append_env`].
|
||||
pub fn append_env_os(current: &OsStr, value: &OsStr) -> OsString {
|
||||
if current.is_empty() {
|
||||
return value.to_owned();
|
||||
}
|
||||
let mut out = current.to_owned();
|
||||
out.push(DELIMITER.to_string());
|
||||
out.push(value);
|
||||
out
|
||||
}
|
||||
|
||||
/// Prepend a value to an environment variable such as PATH.
|
||||
pub fn prepend_env(current: &str, value: &str) -> String {
|
||||
// If there is no prior value, we return it as-is
|
||||
@@ -64,6 +215,35 @@ pub fn getenv_not_empty(key: &str) -> Option<String> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::os::unix::ffi::OsStringExt;
|
||||
|
||||
#[test]
|
||||
fn env_map_preserves_non_utf8() {
|
||||
let mut env = EnvMap::new();
|
||||
let v = OsString::from_vec(vec![b'a', 0xff, b'b']);
|
||||
env.insert("K", &v);
|
||||
assert_eq!(env.get("K"), Some(v.as_os_str()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_snap_paths_filters() {
|
||||
let mut env = EnvMap::new();
|
||||
env.insert("SNAP", "/snap/ghostty/1");
|
||||
env.insert("SNAP_NAME", "ghostty");
|
||||
env.insert("GHOSTTY_X", "/snap/ghostty/1/a");
|
||||
env.insert("TERMINFO", "/snap/ghostty/1/ti");
|
||||
env.insert("PATH", "/snap/ghostty/1/bin:/usr/bin:/snap/ghostty/1/x");
|
||||
env.insert("ONLY", "/snap/ghostty/1/lib");
|
||||
env.insert("KEEP", "/usr/lib");
|
||||
filter_snap_paths(&mut env);
|
||||
assert!(!env.contains_key("SNAP"));
|
||||
assert!(!env.contains_key("SNAP_NAME"));
|
||||
assert_eq!(env["GHOSTTY_X"], *"/snap/ghostty/1/a");
|
||||
assert_eq!(env["TERMINFO"], *"/snap/ghostty/1/ti");
|
||||
assert_eq!(env["PATH"], *"/usr/bin");
|
||||
assert!(!env.contains_key("ONLY"));
|
||||
assert_eq!(env["KEEP"], *"/usr/lib");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_env_empty() {
|
||||
@@ -75,6 +255,16 @@ mod tests {
|
||||
assert_eq!(append_env("a:b", "foo"), "a:b:foo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn os_variants_preserve_bytes() {
|
||||
let cur = OsString::from_vec(vec![0xff, b'a']);
|
||||
let out = prepend_env_os(&cur, OsStr::new("x"));
|
||||
assert_eq!(out.into_vec(), vec![b'x', b':', 0xff, b'a']);
|
||||
let out = append_env_os(&cur, OsStr::new("x"));
|
||||
assert_eq!(out.into_vec(), vec![0xff, b'a', b':', b'x']);
|
||||
assert_eq!(append_env_os(OsStr::new(""), OsStr::new("x")), "x");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepend_env_empty() {
|
||||
assert_eq!(prepend_env("", "foo"), "foo");
|
||||
|
||||
Reference in new issue
Block a user