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
@@ -9,6 +9,17 @@ pub fn iterator() -> std::env::ArgsOs {
|
||||
std::env::args_os()
|
||||
}
|
||||
|
||||
/// Collect the process arguments as `String`s without ever panicking on
|
||||
/// non-UTF-8 bytes (invalid sequences become U+FFFD). The CLI/config layers
|
||||
/// are string based; `+action` dispatch only depends on the ASCII prefix.
|
||||
pub fn collect_lossy() -> Vec<String> {
|
||||
lossy(iterator())
|
||||
}
|
||||
|
||||
fn lossy(it: impl Iterator<Item = std::ffi::OsString>) -> Vec<String> {
|
||||
it.map(|a| a.to_string_lossy().into_owned()).collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -18,4 +29,15 @@ mod tests {
|
||||
let mut iter = iterator();
|
||||
assert!(!iter.next().unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lossy_does_not_panic_on_non_utf8() {
|
||||
use std::os::unix::ffi::OsStringExt;
|
||||
let args = vec![
|
||||
std::ffi::OsString::from("ghostty"),
|
||||
std::ffi::OsString::from_vec(vec![b'+', b'x', 0xff]),
|
||||
];
|
||||
let out = lossy(args.into_iter());
|
||||
assert_eq!(out, vec!["ghostty".to_owned(), "+x\u{fffd}".to_owned()]);
|
||||
}
|
||||
}
|
||||
+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");
|
||||
|
||||
@@ -84,7 +84,7 @@ pub struct FlatpakHostCommand {
|
||||
|
||||
/// Environment variables for the child process. If this is None, this
|
||||
/// does not send any environment variables.
|
||||
pub env: Option<HashMap<String, String>>,
|
||||
pub env: Option<crate::env::EnvMap>,
|
||||
|
||||
/// File descriptors to send to the child process. It is up to the
|
||||
/// caller to create the file descriptors and set them up. They are
|
||||
@@ -113,14 +113,6 @@ impl FlatpakHostCommand {
|
||||
}
|
||||
}
|
||||
|
||||
fn state(&self) -> State {
|
||||
*self
|
||||
.shared
|
||||
.state
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
/// Spawn the command. This will start the host command. On return,
|
||||
/// the pid will be available. This must only be called with the
|
||||
/// state in "init".
|
||||
@@ -128,7 +120,19 @@ impl FlatpakHostCommand {
|
||||
let request = Request {
|
||||
argv: self.argv.clone(),
|
||||
cwd: self.cwd.clone(),
|
||||
env: self.env.clone().unwrap_or_default(),
|
||||
// The Development.HostCommand DBus method takes `a{ss}`, which
|
||||
// can only carry UTF-8, so non-UTF-8 bytes are replaced here.
|
||||
env: self
|
||||
.env
|
||||
.iter()
|
||||
.flat_map(crate::env::EnvMap::iter)
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
k.to_string_lossy().into_owned(),
|
||||
v.to_string_lossy().into_owned(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
fds: [
|
||||
self.stdin
|
||||
.try_clone()
|
||||
@@ -213,6 +217,38 @@ impl FlatpakHostCommand {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a signal to the started command. This does nothing if the
|
||||
/// command is not in the started state.
|
||||
pub fn signal(&self, sig: u8, pg: bool) -> Result<(), Error> {
|
||||
self.signal_handle().signal(sig, pg)
|
||||
}
|
||||
|
||||
/// A cloneable handle that can signal this command even after the
|
||||
/// command itself has been moved into an exit watcher
|
||||
/// (`wait_callback`). Zig's `FlatpakHostCommand` is a pointer shared
|
||||
/// between the exit watcher and `Exec.stop`; this is the equivalent.
|
||||
pub fn signal_handle(&self) -> FlatpakSignalHandle {
|
||||
FlatpakSignalHandle {
|
||||
shared: Arc::clone(&self.shared),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle used to signal a host command independent of its owner.
|
||||
#[derive(Clone)]
|
||||
pub struct FlatpakSignalHandle {
|
||||
shared: Arc<Shared>,
|
||||
}
|
||||
|
||||
impl FlatpakSignalHandle {
|
||||
fn state(&self) -> State {
|
||||
*self
|
||||
.shared
|
||||
.state
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
/// Send a signal to the started command. This does nothing if the
|
||||
/// command is not in the started state.
|
||||
pub fn signal(&self, sig: u8, pg: bool) -> Result<(), Error> {
|
||||
@@ -392,6 +428,23 @@ mod tests {
|
||||
assert_eq!(cmd.signal(15, false), Ok(()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signal_handle_survives_wait_callback() {
|
||||
let (r, w) = crate::pipe::pipe().unwrap();
|
||||
let w2 = w.try_clone().unwrap();
|
||||
let cmd = FlatpakHostCommand::new(vec!["true".into()], r, w, w2);
|
||||
let handle = cmd.signal_handle();
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
cmd.wait_callback(move |r| {
|
||||
let _ = tx.send(r);
|
||||
})
|
||||
.unwrap();
|
||||
// The watcher owns the command; the handle still works (no-op
|
||||
// since never started) and the watcher reports the state error.
|
||||
assert_eq!(handle.signal(1, true), Ok(()));
|
||||
assert_eq!(rx.recv().unwrap(), Err(Error::FlatpakMustBeStarted));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exit_status_extraction() {
|
||||
// Mirrors posix.W.EXITSTATUS on a raw wait status.
|
||||
|
||||
@@ -47,7 +47,10 @@ pub(crate) mod testutil;
|
||||
// Functions and types (mirrors the flat exports of `os/main.zig`).
|
||||
pub use dbus::launched_by_dbus_activation;
|
||||
pub use desktop::{DesktopEnvironment, desktop_environment, launched_from_desktop};
|
||||
pub use env::{append_env, append_env_always, get_env_map, getenv, getenv_not_empty, prepend_env};
|
||||
pub use env::{
|
||||
EnvMap, append_env, append_env_always, append_env_os, filter_snap_paths, get_env_map, getenv,
|
||||
getenv_not_empty, prepend_env, prepend_env_os,
|
||||
};
|
||||
pub use file::{Rlimit, alloc_tmp_dir, fix_max_files, restore_max_files};
|
||||
pub use flatpak::{FlatpakHostCommand, is_flatpak};
|
||||
pub use homedir::{expand_home, home};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
//! additional [`ParseOptions`] from `os/uri.zig`.
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::Write as _;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct ParseOptions {
|
||||
@@ -47,15 +48,26 @@ impl Component<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The raw (percent-decoded) value (`toRaw`). Invalid escapes are left
|
||||
/// as-is, matching `std.Uri.percentDecodeInPlace`.
|
||||
pub fn to_raw(&self) -> Cow<'_, str> {
|
||||
/// The raw (percent-decoded) value (`toRaw`) as bytes. Invalid escapes
|
||||
/// are left as-is, matching `std.Uri.percentDecodeInPlace`. The decoded
|
||||
/// bytes are NOT required to be UTF-8 (e.g. non-UTF-8 file names).
|
||||
pub fn to_raw_bytes(&self) -> Cow<'_, [u8]> {
|
||||
match self {
|
||||
Component::Raw(s) => Cow::Borrowed(s.as_ref()),
|
||||
Component::Raw(s) => Cow::Borrowed(s.as_bytes()),
|
||||
Component::PercentEncoded(s) => percent_decode(s),
|
||||
}
|
||||
}
|
||||
|
||||
/// Lossy (U+FFFD) UTF-8 view of [`Self::to_raw_bytes`], for values such
|
||||
/// as host names that are expected to be text. Use `to_raw_bytes` for
|
||||
/// paths.
|
||||
pub fn to_raw(&self) -> Cow<'_, str> {
|
||||
match self.to_raw_bytes() {
|
||||
Cow::Borrowed(b) => String::from_utf8_lossy(b),
|
||||
Cow::Owned(b) => Cow::Owned(String::from_utf8_lossy(&b).into_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
/// The percent-encoded value as it appears in the source, or the raw
|
||||
/// text for `Raw` components.
|
||||
pub fn as_str(&self) -> &str {
|
||||
@@ -68,9 +80,9 @@ impl Component<'_> {
|
||||
|
||||
/// Lenient percent decoding like `std.Uri.percentDecodeBackwards`/InPlace:
|
||||
/// valid `%XX` sequences are decoded, everything else passes through.
|
||||
fn percent_decode(s: &str) -> Cow<'_, str> {
|
||||
fn percent_decode(s: &str) -> Cow<'_, [u8]> {
|
||||
if !s.contains('%') {
|
||||
return Cow::Borrowed(s);
|
||||
return Cow::Borrowed(s.as_bytes());
|
||||
}
|
||||
let b = s.as_bytes();
|
||||
let mut out = Vec::with_capacity(b.len());
|
||||
@@ -87,7 +99,7 @@ fn percent_decode(s: &str) -> Cow<'_, str> {
|
||||
out.push(b[i]);
|
||||
i += 1;
|
||||
}
|
||||
Cow::Owned(String::from_utf8_lossy(&out).into_owned())
|
||||
Cow::Owned(out)
|
||||
}
|
||||
|
||||
fn hexval(c: u8) -> Option<u8> {
|
||||
@@ -318,6 +330,64 @@ pub fn parse(text: &str, options: ParseOptions) -> Result<Uri<'_>, ParseError> {
|
||||
Ok(uri)
|
||||
}
|
||||
|
||||
/// The pieces of a parsed URI that are needed by byte-oriented callers.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ParsedBytes {
|
||||
pub scheme: String,
|
||||
/// Percent-decoded host (lossy; host names are text).
|
||||
pub host: Option<String>,
|
||||
/// Percent-decoded path bytes (or the full raw tail with
|
||||
/// [`ParseOptions::raw_path`]). Not required to be UTF-8.
|
||||
pub path: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Parse a URI given as arbitrary bytes without losing any path bytes.
|
||||
///
|
||||
/// Bytes that are not valid UTF-8 are percent-escaped before parsing (so
|
||||
/// they decode back to exactly the same bytes), mirroring Zig's byte-string
|
||||
/// `std.Uri.parse`. With `raw_path` the original bytes are sliced directly.
|
||||
pub fn parse_bytes(text: &[u8], options: ParseOptions) -> Result<ParsedBytes, ParseError> {
|
||||
// Escape invalid bytes, remembering the offset mapping.
|
||||
let mut escaped = String::with_capacity(text.len());
|
||||
for chunk in text.utf8_chunks() {
|
||||
escaped.push_str(chunk.valid());
|
||||
for b in chunk.invalid() {
|
||||
let _ = write!(escaped, "%{b:02X}");
|
||||
}
|
||||
}
|
||||
|
||||
let uri = parse(&escaped, options)?;
|
||||
let path = if options.raw_path {
|
||||
let esc_start = offset_in(&escaped, uri.path.as_str());
|
||||
// Map the escaped offset back to the original byte offset. The path
|
||||
// never begins inside an escape (it starts at a delimiter).
|
||||
let mut orig_start = esc_start;
|
||||
let mut esc_pos = 0usize;
|
||||
let mut orig_pos = 0usize;
|
||||
for chunk in text.utf8_chunks() {
|
||||
let v = chunk.valid().len();
|
||||
if esc_start <= esc_pos + v {
|
||||
orig_start = orig_pos + (esc_start - esc_pos);
|
||||
break;
|
||||
}
|
||||
esc_pos += v;
|
||||
orig_pos += v;
|
||||
let inv = chunk.invalid().len();
|
||||
esc_pos += inv * 3;
|
||||
orig_pos += inv;
|
||||
orig_start = orig_pos;
|
||||
}
|
||||
text[orig_start.min(text.len())..].to_vec()
|
||||
} else {
|
||||
uri.path.to_raw_bytes().into_owned()
|
||||
};
|
||||
Ok(ParsedBytes {
|
||||
scheme: uri.scheme.to_owned(),
|
||||
host: uri.host.as_ref().map(|h| h.to_raw().into_owned()),
|
||||
path,
|
||||
})
|
||||
}
|
||||
|
||||
/// Checks if a string represents a valid MAC address, e.g. 12:34:56:ab:cd:ef.
|
||||
fn is_valid_mac_address(s: &str) -> bool {
|
||||
let b = s.as_bytes();
|
||||
@@ -407,6 +477,31 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn percent_decode_preserves_non_utf8_bytes() {
|
||||
let u = parse("file://localhost/a%FFb%e2%82", ParseOptions::default()).unwrap();
|
||||
assert_eq!(&*u.path.to_raw_bytes(), b"/a\xffb\xe2\x82");
|
||||
// The lossy text view still works for text values.
|
||||
assert_eq!(u.path.to_raw(), "/a\u{fffd}b\u{fffd}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_bytes_non_utf8() {
|
||||
let p = parse_bytes(b"file://localhost/a\xffb%FEc", ParseOptions::default()).unwrap();
|
||||
assert_eq!(p.scheme, "file");
|
||||
assert_eq!(p.host.as_deref(), Some("localhost"));
|
||||
assert_eq!(p.path, b"/a\xffb\xfec");
|
||||
|
||||
let raw = ParseOptions {
|
||||
raw_path: true,
|
||||
mac_address: true,
|
||||
};
|
||||
let p = parse_bytes(b"kitty-shell-cwd://localhost/a\xff%41?x", raw).unwrap();
|
||||
assert_eq!(p.path, b"/a\xff%41?x");
|
||||
let p = parse_bytes(b"kitty-shell-cwd://\xfflo/a", raw).ok();
|
||||
assert!(p.is_none() || p.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_raw_path() {
|
||||
let text = "file://localhost/path??#fragment";
|
||||
|
||||
Reference in new issue
Block a user