use std::process::Command;
#[cfg(any(target_os = "windows", test))]
fn windows_set_clipboard_command_args(_text: &str) -> [&'static str; 4] {
[
"-NoProfile",
"-NonInteractive",
"-Command",
"Set-Clipboard -Value ([Console]::In.ReadToEnd())",
]
}
#[derive(Clone, Copy)]
pub struct ClipboardHandle;
impl ClipboardHandle {
pub fn read(&self) -> Option<String> {
read_clipboard()
}
pub fn write(&self, text: &str) -> bool {
write_clipboard(text)
}
pub fn is_available(&self) -> bool {
is_clipboard_available()
}
pub fn clear(&self) -> bool {
write_clipboard("")
}
}
pub fn use_clipboard() -> ClipboardHandle {
ClipboardHandle
}
pub fn read_clipboard() -> Option<String> {
#[cfg(target_os = "macos")]
{
Command::new("pbpaste").output().ok().and_then(|output| {
if output.status.success() {
String::from_utf8(output.stdout).ok()
} else {
None
}
})
}
#[cfg(target_os = "linux")]
{
Command::new("xclip")
.args(["-selection", "clipboard", "-o"])
.output()
.ok()
.and_then(|output| {
if output.status.success() {
String::from_utf8(output.stdout).ok()
} else {
None
}
})
.or_else(|| {
Command::new("xsel")
.args(["--clipboard", "--output"])
.output()
.ok()
.and_then(|output| {
if output.status.success() {
String::from_utf8(output.stdout).ok()
} else {
None
}
})
})
}
#[cfg(target_os = "windows")]
{
Command::new("powershell")
.args(["-command", "Get-Clipboard"])
.output()
.ok()
.and_then(|output| {
if output.status.success() {
String::from_utf8(output.stdout)
.ok()
.map(|s| s.trim().to_string())
} else {
None
}
})
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
{
None
}
}
pub fn write_clipboard(text: &str) -> bool {
#[cfg(target_os = "macos")]
{
use std::io::Write;
Command::new("pbcopy")
.stdin(std::process::Stdio::piped())
.spawn()
.and_then(|mut child| {
if let Some(stdin) = child.stdin.as_mut() {
stdin.write_all(text.as_bytes())?;
}
child.wait()
})
.map(|status| status.success())
.unwrap_or(false)
}
#[cfg(target_os = "linux")]
{
use std::io::Write;
Command::new("xclip")
.args(["-selection", "clipboard"])
.stdin(std::process::Stdio::piped())
.spawn()
.and_then(|mut child| {
if let Some(stdin) = child.stdin.as_mut() {
stdin.write_all(text.as_bytes())?;
}
child.wait()
})
.map(|status| status.success())
.unwrap_or(false)
}
#[cfg(target_os = "windows")]
{
use std::io::Write;
Command::new("powershell")
.args(windows_set_clipboard_command_args(text))
.stdin(std::process::Stdio::piped())
.spawn()
.and_then(|mut child| {
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(text.as_bytes())?;
}
child.wait()
})
.map(|status| status.success())
.unwrap_or(false)
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
{
let _ = text;
false
}
}
pub fn is_clipboard_available() -> bool {
#[cfg(target_os = "macos")]
{
Command::new("which")
.arg("pbcopy")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
#[cfg(target_os = "linux")]
{
Command::new("which")
.arg("xclip")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
|| Command::new("which")
.arg("xsel")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
#[cfg(target_os = "windows")]
{
true }
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
{
false
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_clipboard_handle() {
let clipboard = use_clipboard();
let _ = clipboard.is_available();
}
#[test]
fn test_is_clipboard_available() {
let _ = is_clipboard_available();
}
#[test]
fn windows_clipboard_payload_is_not_embedded_in_powershell_source() {
let payloads = [
r#"$(Start-Process calc)"#,
"`n; Write-Host injected",
"first line\r\nsecond line; Remove-Item -Recurse C:\\tmp",
];
for payload in payloads {
let args = windows_set_clipboard_command_args(payload);
let command_source = args.join(" ");
assert_eq!(
args,
[
"-NoProfile",
"-NonInteractive",
"-Command",
"Set-Clipboard -Value ([Console]::In.ReadToEnd())",
]
);
assert!(!command_source.contains(payload));
assert!(!command_source.contains("Start-Process"));
assert!(!command_source.contains("Write-Host injected"));
assert!(!command_source.contains("Remove-Item"));
}
}
}