use std::io::Read;
use std::os::unix::fs::PermissionsExt;
use std::os::unix::process::CommandExt;
use std::path::Path;
use std::process::{Child, Command, ExitStatus, Stdio};
use std::sync::mpsc::{self, RecvTimeoutError};
use std::time::{Duration, Instant};
pub(crate) const CLIPBOARD_READ_TIMEOUT: Duration = Duration::from_secs(5);
pub(crate) const CLIPBOARD_MAX_BYTES: usize = 1024 * 1024;
#[cfg(target_os = "macos")]
pub(crate) const PASTE_DIRS: &[&str] = &["/usr/bin"];
#[cfg(not(target_os = "macos"))]
pub(crate) const PASTE_DIRS: &[&str] = &[
"/usr/bin",
"/usr/local/bin",
"/run/current-system/sw/bin",
"/nix/var/nix/profiles/default/bin",
"/snap/bin",
];
#[derive(Debug, thiserror::Error)]
pub(crate) enum ClipboardError {
#[error(
"tm secrets set: `{program}` did not answer within {limit:?}; it was stopped and nothing was stored"
)]
Timeout { program: String, limit: Duration },
#[error("tm secrets set: the clipboard holds more than {limit} bytes; nothing was stored")]
TooLarge { limit: usize },
#[error("tm secrets set: `{program}` could not read the clipboard ({status})")]
Failed {
program: String,
status: std::process::ExitStatus,
},
#[error("tm secrets set: cannot run `{program}`: {source}")]
Run {
program: String,
source: std::io::Error,
},
#[error("tm secrets set: the clipboard does not hold UTF-8 text")]
NotUtf8,
#[error(
"tm secrets set: no clipboard reader found in {searched}; use `--value -` to read stdin"
)]
NoReader { searched: String },
}
const POLL: Duration = Duration::from_millis(10);
pub(crate) fn is_executable_file(path: &Path) -> bool {
std::fs::metadata(path)
.is_ok_and(|meta| meta.is_file() && meta.permissions().mode() & 0o111 != 0)
}
pub(crate) fn run_bounded(
program: &Path,
args: &[String],
timeout: Duration,
) -> Result<Vec<u8>, ClipboardError> {
let name = program.display().to_string();
let run_error = |source| ClipboardError::Run {
program: name.clone(),
source,
};
let timed_out = || ClipboardError::Timeout {
program: name.clone(),
limit: timeout,
};
let child = Command::new(program)
.args(args)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.process_group(0)
.spawn()
.map_err(run_error)?;
let mut guard = GroupGuard {
child,
reaped: false,
};
let deadline = Instant::now() + timeout;
let (tx, rx) = mpsc::channel();
let mut output = None;
match guard.child.stdout.take() {
Some(pipe) => drain(pipe, tx).map_err(run_error)?,
None => output = Some(Vec::new()),
}
let bytes = match output {
Some(bytes) => bytes,
None => match rx.recv_timeout(deadline.saturating_duration_since(Instant::now())) {
Ok(read) => settle(read, &name)?,
Err(RecvTimeoutError::Timeout) => return Err(timed_out()),
Err(RecvTimeoutError::Disconnected) => return Err(run_error(reader_lost())),
},
};
let status = loop {
if let Some(status) = guard.try_wait().map_err(run_error)? {
break status;
}
let now = Instant::now();
if now >= deadline {
return Err(timed_out());
}
std::thread::sleep(POLL.min(deadline - now));
};
if !status.success() {
return Err(ClipboardError::Failed {
program: name,
status,
});
}
Ok(bytes)
}
fn settle(read: std::io::Result<Capped>, program: &str) -> Result<Vec<u8>, ClipboardError> {
let capped = read.map_err(|source| ClipboardError::Run {
program: program.to_string(),
source,
})?;
if capped.overflow {
return Err(ClipboardError::TooLarge {
limit: CLIPBOARD_MAX_BYTES,
});
}
Ok(capped.bytes)
}
fn reader_lost() -> std::io::Error {
std::io::Error::other("the output reader stopped without a result")
}
struct Capped {
bytes: Vec<u8>,
overflow: bool,
}
fn drain<R: Read + Send + 'static>(
pipe: R,
tx: mpsc::Sender<std::io::Result<Capped>>,
) -> std::io::Result<()> {
std::thread::Builder::new()
.name("tm-clipboard-read".to_string())
.spawn(move || {
let mut bytes = Vec::new();
let read = pipe
.take(CLIPBOARD_MAX_BYTES as u64 + 1)
.read_to_end(&mut bytes)
.map(|_| {
let overflow = bytes.len() > CLIPBOARD_MAX_BYTES;
bytes.truncate(CLIPBOARD_MAX_BYTES);
Capped { bytes, overflow }
});
let _ = tx.send(read);
})?;
Ok(())
}
struct GroupGuard {
child: Child,
reaped: bool,
}
impl GroupGuard {
fn try_wait(&mut self) -> std::io::Result<Option<ExitStatus>> {
let status = self.child.try_wait()?;
self.reaped = status.is_some();
Ok(status)
}
}
impl Drop for GroupGuard {
fn drop(&mut self) {
if !self.reaped {
kill_group(self.child.id());
let _ = self.child.kill();
let _ = self.child.wait();
}
}
}
fn kill_group(pid: u32) {
let Some(pgid) = libc::pid_t::try_from(pid).ok().filter(|p| *p > 1) else {
return;
};
unsafe {
libc::kill(-pgid, libc::SIGKILL);
}
}