From 2b5755aa8a3732694b30367eb0cbb8db2eff47d8 Mon Sep 17 00:00:00 2001 From: Jeremy Cantrell Date: Sun, 16 Oct 2022 04:33:08 -0500 Subject: [PATCH] An attempt at safer message passing. --- src/app.rs | 11 ++++ src/init.lua | 118 +++++++++++++++++++++------------------- src/lib.rs | 1 + src/msg/in_/external.rs | 3 +- src/newlines.rs | 79 +++++++++++++++++++++++++++ src/pipe.rs | 6 +- src/runner.rs | 9 +-- 7 files changed, 159 insertions(+), 68 deletions(-) create mode 100644 src/newlines.rs diff --git a/src/app.rs b/src/app.rs index a21d5af..57e2443 100644 --- a/src/app.rs +++ b/src/app.rs @@ -15,6 +15,7 @@ pub use crate::msg::in_::ExternalMsg; pub use crate::msg::in_::InternalMsg; pub use crate::msg::in_::MsgIn; pub use crate::msg::out::MsgOut; +use crate::newlines::unescape_string; pub use crate::node::Node; pub use crate::node::ResolvedNode; pub use crate::pipe::Pipe; @@ -174,6 +175,7 @@ pub struct App { pub mode: Mode, pub layout: Layout, pub input: InputBuffer, + pub input_unescaped: String, pub pid: u32, pub session_path: String, pub pipe: Pipe, @@ -308,6 +310,7 @@ impl App { mode, layout, input, + input_unescaped: Default::default(), pid, session_path: session_path.clone(), pipe: Pipe::from_session_path(&session_path)?, @@ -554,6 +557,14 @@ impl App { } }); + self.input_unescaped = unescape_string( + &self.input + .buffer + .as_ref() + .map(|i| i.value().to_string()) + .unwrap_or_default(), + )?; + for msg in msgs { self = self.enqueue(Task::new(MsgIn::External(msg), Some(key))); } diff --git a/src/init.lua b/src/init.lua index e778716..fc15020 100644 --- a/src/init.lua +++ b/src/init.lua @@ -1156,7 +1156,7 @@ xplr.config.modes.builtin.default = { { BashExecSilently = [===[ NAME=$(basename "${XPLR_FOCUS_PATH:?}") - echo SetInputBuffer: "'${NAME:?}'" >> "${XPLR_PIPE_MSG_IN:?}" + printf "SetInputBuffer: %s\0" "${NAME:?}" >> "${XPLR_PIPE_MSG_IN:?}" ]===], }, }, @@ -1169,7 +1169,7 @@ xplr.config.modes.builtin.default = { { BashExecSilently = [===[ NAME=$(basename "${XPLR_FOCUS_PATH:?}") - echo SetInputBuffer: "'${NAME:?}'" >> "${XPLR_PIPE_MSG_IN:?}" + printf "SetInputBuffer: %s\0" "${NAME:?}" >> "${XPLR_PIPE_MSG_IN:?}" ]===], }, }, @@ -1205,7 +1205,7 @@ xplr.config.modes.builtin.default = { messages = { { BashExecSilently = [===[ - echo ChangeDirectory: "'${HOME:?}'" >> "${XPLR_PIPE_MSG_IN:?}" + printf "ChangeDirectory: %s\0" "${HOME:?}" >> "${XPLR_PIPE_MSG_IN:?}" ]===], }, }, @@ -1364,9 +1364,9 @@ xplr.config.modes.builtin.go_to_path = { { BashExecSilently = [===[ if [ -d "$XPLR_INPUT_BUFFER" ]; then - echo ChangeDirectory: "'$XPLR_INPUT_BUFFER'" >> "${XPLR_PIPE_MSG_IN:?}" + printf "ChangeDirectory: %s\0" "$XPLR_INPUT_BUFFER" >> "${XPLR_PIPE_MSG_IN:?}" elif [ -e "$XPLR_INPUT_BUFFER" ]; then - echo FocusPath: "'$XPLR_INPUT_BUFFER'" >> "${XPLR_PIPE_MSG_IN:?}" + printf "FocusPath: %s\0" "$XPLR_INPUT_BUFFER" >> "${XPLR_PIPE_MSG_IN:?}" fi ]===], }, @@ -1400,15 +1400,15 @@ xplr.config.modes.builtin.selection_ops = { messages = { { BashExec = [===[ - (while IFS= read -r line; do - if cp -vr -- "${line:?}" ./; then - echo LogSuccess: "'$line copied to $PWD'" >> "${XPLR_PIPE_MSG_IN:?}" - else - echo LogError: "'Failed to copy $line to $PWD'" >> "${XPLR_PIPE_MSG_IN:?}" - fi + (while IFS= read -r -d '' line; do + if cp -vr -- "${line:?}" ./; then + printf "LogSuccess: %s\0" "$line copied to $PWD" >> "${XPLR_PIPE_MSG_IN:?}" + else + printf "LogError: %s\0" "Failed to copy $line to $PWD" >> "${XPLR_PIPE_MSG_IN:?}" + fi done < "${XPLR_PIPE_SELECTION_OUT:?}") - echo ExplorePwdAsync >> "${XPLR_PIPE_MSG_IN:?}" - echo ClearSelection >> "${XPLR_PIPE_MSG_IN:?}" + printf "ExplorePwdAsync\0" >> "${XPLR_PIPE_MSG_IN:?}" + printf "ClearSelection\0" >> "${XPLR_PIPE_MSG_IN:?}" read -p "[enter to continue]" ]===], }, @@ -1420,14 +1420,14 @@ xplr.config.modes.builtin.selection_ops = { messages = { { BashExec = [===[ - (while IFS= read -r line; do - if mv -v -- "${line:?}" ./; then - echo LogSuccess: "'$line moved to $PWD'" >> "${XPLR_PIPE_MSG_IN:?}" - else - echo LogError: "'Failed to move $line to $PWD'" >> "${XPLR_PIPE_MSG_IN:?}" - fi + (while IFS= read -r -d '' line; do + if mv -v -- "${line:?}" ./; then + printf "LogSuccess: %s\0" "$line moved to $PWD" >> "${XPLR_PIPE_MSG_IN:?}" + else + printf "LogError: %s\0" "Failed to move $line to $PWD" >> "${XPLR_PIPE_MSG_IN:?}" + fi done < "${XPLR_PIPE_SELECTION_OUT:?}") - echo ExplorePwdAsync >> "${XPLR_PIPE_MSG_IN:?}" + printf "ExplorePwdAsync\0" >> "${XPLR_PIPE_MSG_IN:?}" read -p "[enter to continue]" ]===], }, @@ -1452,12 +1452,12 @@ xplr.config.modes.builtin.selection_ops = { elif command -v open; then OPENER=open else - echo LogError: '$OPENER not found' >> "${XPLR_PIPE_MSG_IN:?}" + printf "LogError: %s\0" "$OPENER not found" >> "${XPLR_PIPE_MSG_IN:?}" exit 1 fi fi - (while IFS= read -r line; do - $OPENER "${line:?}" > /dev/null 2>&1 + (while IFS= read -r -d '' line; do + $OPENER "${line:?}" > /dev/null 2>&1 done < "${XPLR_PIPE_RESULT_OUT:?}") ]===], }, @@ -1518,12 +1518,12 @@ xplr.config.modes.builtin.create_directory = { PTH="$XPLR_INPUT_BUFFER" if [ "${PTH}" ]; then mkdir -p -- "${PTH:?}" \ - && echo SetInputBuffer: "''" >> "${XPLR_PIPE_MSG_IN:?}" \ - && echo ExplorePwd >> "${XPLR_PIPE_MSG_IN:?}" \ - && echo LogSuccess: "'$PTH created'" >> "${XPLR_PIPE_MSG_IN:?}" \ - && echo FocusPath: "'$PTH'" >> "${XPLR_PIPE_MSG_IN:?}" + && printf "SetInputBuffer: ''\0" >> "${XPLR_PIPE_MSG_IN:?}" \ + && printf "ExplorePwd\0" >> "${XPLR_PIPE_MSG_IN:?}" \ + && printf "LogSuccess: %s\0" "$PTH created" >> "${XPLR_PIPE_MSG_IN:?}" \ + && printf "FocusPath: %s\0" "$PTH" >> "${XPLR_PIPE_MSG_IN:?}" else - echo PopMode >> "${XPLR_PIPE_MSG_IN:?}" + printf "PopMode\0" >> "${XPLR_PIPE_MSG_IN:?}" fi ]===], }, @@ -1561,12 +1561,12 @@ xplr.config.modes.builtin.create_file = { if [ "$PTH" ]; then mkdir -p -- "$(dirname $PTH)" \ && touch -- "$PTH" \ - && echo SetInputBuffer: "''" >> "${XPLR_PIPE_MSG_IN:?}" \ - && echo LogSuccess: "'$PTH created'" >> "${XPLR_PIPE_MSG_IN:?}" \ - && echo ExplorePwd >> "${XPLR_PIPE_MSG_IN:?}" \ - && echo FocusPath: "'$PTH'" >> "${XPLR_PIPE_MSG_IN:?}" + && printf "SetInputBuffer: ''\0" >> "${XPLR_PIPE_MSG_IN:?}" \ + && printf "LogSuccess: %s\0" "$PTH created" >> "${XPLR_PIPE_MSG_IN:?}" \ + && printf "ExplorePwd\0" >> "${XPLR_PIPE_MSG_IN:?}" \ + && printf "FocusPath: %s\0" "$PTH" >> "${XPLR_PIPE_MSG_IN:?}" else - echo PopMode >> "${XPLR_PIPE_MSG_IN:?}" + printf "PopMode\0" >> "${XPLR_PIPE_MSG_IN:?}" fi ]===], }, @@ -1668,7 +1668,7 @@ xplr.config.modes.builtin.go_to = { elif command -v open; then OPENER=open else - echo LogError: '$OPENER not found' >> "${XPLR_PIPE_MSG_IN:?}" + printf "LogError: %s\0" "$OPENER not found" >> "${XPLR_PIPE_MSG_IN:?}" exit 1 fi fi @@ -1704,12 +1704,12 @@ xplr.config.modes.builtin.rename = { SRC="${XPLR_FOCUS_PATH:?}" TARGET="${XPLR_INPUT_BUFFER:?}" if [ -e "${TARGET:?}" ]; then - echo LogError: "'$TARGET already exists'" >> "${XPLR_PIPE_MSG_IN:?}" + printf "LogError: %s\0" "$TARGET already exists" >> "${XPLR_PIPE_MSG_IN:?}" else mv -- "${SRC:?}" "${TARGET:?}" \ - && echo ExplorePwd >> "${XPLR_PIPE_MSG_IN:?}" \ - && echo FocusPath: "'$TARGET'" >> "${XPLR_PIPE_MSG_IN:?}" \ - && echo LogSuccess: "'$SRC renamed to $TARGET'" >> "${XPLR_PIPE_MSG_IN:?}" + && printf "ExplorePwd\0" >> "${XPLR_PIPE_MSG_IN:?}" \ + && printf "FocusPath: %s\0" "$TARGET" >> "${XPLR_PIPE_MSG_IN:?}" \ + && printf "LogSuccess: %s\0" "$SRC renamed to $TARGET" >> "${XPLR_PIPE_MSG_IN:?}" fi ]===], }, @@ -1746,12 +1746,12 @@ xplr.config.modes.builtin.duplicate_as = { SRC="${XPLR_FOCUS_PATH:?}" TARGET="${XPLR_INPUT_BUFFER:?}" if [ -e "${TARGET:?}" ]; then - echo LogError: "'$TARGET already exists'" >> "${XPLR_PIPE_MSG_IN:?}" + printf "LogError: %s\0" "$TARGET already exists" >> "${XPLR_PIPE_MSG_IN:?}" else cp -r -- "${SRC:?}" "${TARGET:?}" \ - && echo ExplorePwd >> "${XPLR_PIPE_MSG_IN:?}" \ - && echo FocusPath: "'$TARGET'" >> "${XPLR_PIPE_MSG_IN:?}" \ - && echo LogSuccess: "'$SRC duplicated as $TARGET'" >> "${XPLR_PIPE_MSG_IN:?}" + && printf "ExplorePwd\0" >> "${XPLR_PIPE_MSG_IN:?}" \ + && printf "FocusPath: %s\0" "$TARGET" >> "${XPLR_PIPE_MSG_IN:?}" \ + && printf "LogSuccess: %s\0" "$SRC duplicated as $TARGET" >> "${XPLR_PIPE_MSG_IN:?}" fi ]===], }, @@ -1779,14 +1779,14 @@ xplr.config.modes.builtin.delete = { messages = { { BashExec = [===[ - (while IFS= read -r line; do - if rm -rfv -- "${line:?}"; then - echo LogSuccess: "'$line deleted'" >> "${XPLR_PIPE_MSG_IN:?}" - else - echo LogError: "'Failed to delete $line'" >> "${XPLR_PIPE_MSG_IN:?}" - fi + (while IFS= read -r -d '' line; do + if rm -rfv -- "${line:?}"; then + printf "LogSuccess: %s\0" "$line deleted" >> "${XPLR_PIPE_MSG_IN:?}" + else + printf "LogError: %s\0" "Failed to delete $line" >> "${XPLR_PIPE_MSG_IN:?}" + fi done < "${XPLR_PIPE_RESULT_OUT:?}") - echo ExplorePwdAsync >> "${XPLR_PIPE_MSG_IN:?}" + printf "ExplorePwdAsync\0" >> "${XPLR_PIPE_MSG_IN:?}" read -p "[enter to continue]" ]===], }, @@ -1798,22 +1798,22 @@ xplr.config.modes.builtin.delete = { messages = { { BashExec = [===[ - (while IFS= read -r line; do + (while IFS= read -r -d '' line; do if [ -d "$line" ] && [ ! -L "$line" ]; then if rmdir -v -- "${line:?}"; then - echo LogSuccess: "'$line deleted'" >> "${XPLR_PIPE_MSG_IN:?}" + printf "LogSuccess: %s\0" "$line deleted" >> "${XPLR_PIPE_MSG_IN:?}" else - echo LogError: "'Failed to delete $line'" >> "${XPLR_PIPE_MSG_IN:?}" + printf "LogError: %s\0" "Failed to delete $line" >> "${XPLR_PIPE_MSG_IN:?}" fi else if rm -v -- "${line:?}"; then - echo LogSuccess: "'$line deleted'" >> "${XPLR_PIPE_MSG_IN:?}" + printf "LogSuccess: %s\0" "$line deleted" >> "${XPLR_PIPE_MSG_IN:?}" else - echo LogError: "'Failed to delete $line'" >> "${XPLR_PIPE_MSG_IN:?}" + printf "LogError: %s\0" "Failed to delete $line" >> "${XPLR_PIPE_MSG_IN:?}" fi fi done < "${XPLR_PIPE_RESULT_OUT:?}") - echo ExplorePwdAsync >> "${XPLR_PIPE_MSG_IN:?}" + printf "ExplorePwdAsync\0" >> "${XPLR_PIPE_MSG_IN:?}" read -p "[enter to continue]" ]===], }, @@ -2431,13 +2431,17 @@ end xplr.fn.builtin.fmt_general_table_row_cols_1 = function(m) local r = m.tree .. m.prefix + local function path_escape(path) + return string.gsub(string.gsub(path, "\\", "\\\\"), "\n", "\\n") + end + if m.meta.icon == nil then r = r .. "" else r = r .. m.meta.icon .. " " end - r = r .. m.relative_path + r = r .. path_escape(m.relative_path) if m.is_dir then r = r .. "/" @@ -2451,7 +2455,7 @@ xplr.fn.builtin.fmt_general_table_row_cols_1 = function(m) if m.is_broken then r = r .. "×" else - r = r .. m.symlink.absolute_path + r = r .. path_escape(m.symlink.absolute_path) if m.symlink.is_dir then r = r .. "/" diff --git a/src/lib.rs b/src/lib.rs index 8314c80..c1f48f8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,6 +17,7 @@ pub mod pipe; pub mod pwd_watcher; pub mod runner; pub mod ui; +pub mod newlines; #[cfg(test)] mod tests { diff --git a/src/msg/in_/external.rs b/src/msg/in_/external.rs index 41ae01a..fe79969 100644 --- a/src/msg/in_/external.rs +++ b/src/msg/in_/external.rs @@ -3,6 +3,7 @@ use indexmap::IndexSet; use regex::Regex; use serde::{Deserialize, Serialize}; use std::cmp::Ordering; +use crate::newlines::escape_string; #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub enum ExternalMsg { @@ -1070,7 +1071,7 @@ impl TryFrom<&str> for ExternalMsg { if value.starts_with('!') { serde_yaml::from_str(value) } else if let Some((msg, args)) = value.split_once(' ') { - let msg = format!("!{} {}", msg.trim_end_matches(':'), args); + let msg = format!("!{} {}", msg.trim_end_matches(':'), escape_string(args)); serde_yaml::from_str(&msg) } else { serde_yaml::from_str(value) diff --git a/src/newlines.rs b/src/newlines.rs new file mode 100644 index 0000000..231ba28 --- /dev/null +++ b/src/newlines.rs @@ -0,0 +1,79 @@ +struct UnescapedString<'a> { + s: std::str::Chars<'a>, +} + +impl<'a> UnescapedString<'a> { + fn new(s: &'a str) -> Self { + Self { s: s.chars() } + } +} + +impl Iterator for UnescapedString<'_> { + type Item = Result; + + fn next(&mut self) -> Option { + self.s.next().map(|c| match c { + '\\' => match self.s.next() { + None => Err(Error::EscapeAtEndOfString), + Some('n') => Ok('\n'), + Some('\\') => Ok('\\'), + Some(c) => Err(Error::UnrecognizedEscapedChar(c)), + }, + c => Ok(c), + }) + } +} + +#[derive(Debug, PartialEq)] +pub enum Error { + EscapeAtEndOfString, + UnrecognizedEscapedChar(char), +} + +use std::fmt; + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::EscapeAtEndOfString => { + write!(f, "Escape character at the end of the string") + } + Error::UnrecognizedEscapedChar(c) => { + write!(f, "Unrecognized escaped char: '{}'", c) + } + } + } +} + +impl std::error::Error for Error {} + +struct EscapedString<'a> { + s: std::str::Chars<'a>, +} + +impl<'a> EscapedString<'a> { + fn new(s: &'a str) -> Self { + Self { s: s.chars() } + } +} + +impl Iterator for EscapedString<'_> { + type Item = String; + + fn next(&mut self) -> Option { + match self.s.next() { + None => None, + Some('\\') => Some(String::from("\\\\")), + Some('\n') => Some(String::from("\\n")), + Some(c) => Some(String::from(c)), + } + } +} + +pub fn escape_string(s: &str) -> String { + EscapedString::new(s).collect() +} + +pub fn unescape_string(s: &str) -> Result { + UnescapedString::new(s).collect() +} diff --git a/src/pipe.rs b/src/pipe.rs index 0724bde..f3993f6 100644 --- a/src/pipe.rs +++ b/src/pipe.rs @@ -65,10 +65,12 @@ pub fn read_all(pipe: &str) -> Result> { file.read_to_string(&mut in_str)?; file.set_len(0)?; + let delim = '\0'; + if !in_str.is_empty() { let mut msgs = vec![]; - for msg in in_str.lines() { - msgs.push(msg.trim().try_into()?); + for msg in in_str.trim_matches(delim).split(delim) { + msgs.push(msg.try_into()?); } Ok(msgs) } else { diff --git a/src/runner.rs b/src/runner.rs index d1ee247..2fc5481 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -77,14 +77,7 @@ fn call(app: &app::App, cmd: app::Command, silent: bool) -> Result { Command::new(cmd.command.clone()) .env("XPLR_APP_VERSION", app.version.clone()) .env("XPLR_PID", &app.pid.to_string()) - .env( - "XPLR_INPUT_BUFFER", - app.input - .buffer - .as_ref() - .map(|i| i.value().to_string()) - .unwrap_or_default(), - ) + .env("XPLR_INPUT_BUFFER", &app.input_unescaped) .env("XPLR_FOCUS_PATH", app.focused_node_str()) .env("XPLR_FOCUS_INDEX", focus_index) .env("XPLR_SESSION_PATH", &app.session_path)