use super::*; use std::fs; use std::os::unix::fs::PermissionsExt; use std::sync::atomic::{AtomicU64, Ordering}; static FIXTURE_ID: AtomicU64 = AtomicU64::new(0); struct Fixture(PathBuf); impl Fixture { fn new() -> Self { let path = std::env::temp_dir().join(format!( "vamp-clipboard-{}-{}", std::process::id(), FIXTURE_ID.fetch_add(1, Ordering::Relaxed) )); fs::create_dir(&path).unwrap(); Self(path) } fn command(&self, name: &str, body: &str) -> PathBuf { let path = self.0.join(name); fs::write(&path, format!("#!/bin/sh\n{body}\n")).unwrap(); fs::set_permissions(&path, fs::Permissions::from_mode(0o700)).unwrap(); path } fn clipboard(&self, text: &[u8]) -> SystemClipboard { let state = self.0.join("state"); fs::write(&state, text).unwrap(); let state = format!("'{}'", state.to_str().unwrap().replace('\'', "'\\''")); SystemClipboard::with_commands( self.command("read", &format!("exec /bin/cat {state}")), self.command("write", &format!("exec /bin/cat > {state}")), ) } } impl Drop for Fixture { fn drop(&mut self) { fs::remove_dir_all(&self.0).unwrap(); } } #[test] fn construction_does_not_probe_or_spawn() { assert!(SystemClipboard::new().commands.is_none()); } #[test] fn round_trips_unicode_whitespace_and_large_pipes_exactly() { let fixture = Fixture::new(); let mut clipboard = fixture.clipboard(b""); for text in ["", " héllo\t世界🙂\r\n\n", &"é\n".repeat(300_000)] { clipboard.write(text).unwrap(); assert_eq!(fs::read(fixture.0.join("state")).unwrap(), text.as_bytes()); assert_eq!(clipboard.read().unwrap(), text); } } #[test] fn rejects_oversize_and_invalid_utf8_without_leaking_contents() { let fixture = Fixture::new(); let mut clipboard = fixture.clipboard(&[0xff, b'\n']); assert_eq!( clipboard.read().unwrap_err(), "System clipboard is not valid UTF-8" ); let maximum = "é".repeat(MAX_CLIPBOARD_BYTES / 2); clipboard.write(&maximum).unwrap(); assert_eq!(clipboard.read().unwrap(), maximum); let too_large = format!("{maximum}x"); assert_eq!( clipboard.write(&too_large).unwrap_err(), "System clipboard text exceeds 4 MiB" ); assert_eq!( fs::read(fixture.0.join("state")).unwrap(), maximum.as_bytes() ); fs::write(fixture.0.join("state"), &too_large).unwrap(); assert_eq!( clipboard.read().unwrap_err(), "System clipboard text exceeds 4 MiB" ); let mut incomplete = vec![b'x'; MAX_CLIPBOARD_BYTES - 1]; incomplete.push(0xc3); fs::write(fixture.0.join("state"), incomplete).unwrap(); assert_eq!( clipboard.read().unwrap_err(), "System clipboard is not valid UTF-8" ); } #[test] fn command_failures_and_stderr_cannot_leak_clipboard_text() { let fixture = Fixture::new(); let failed = fixture.command( "failed", "printf 'private clipboard payload' >&2\nprintf 'private clipboard payload'\nexit 23", ); let mut clipboard = SystemClipboard::with_commands(failed.clone(), failed); let error = clipboard.read().unwrap_err(); assert!(error.contains("failed")); assert!(!error.contains("private clipboard payload")); let error = clipboard.write("private clipboard payload").unwrap_err(); assert!(!error.contains("private clipboard payload")); let missing = fixture.0.join("not-an-executable"); let mut clipboard = SystemClipboard::with_commands(missing.clone(), missing); assert!(clipboard.read().unwrap_err().contains("Cannot start")); assert!( clipboard .write("secret") .unwrap_err() .contains("Cannot start") ); } #[test] fn ignored_stderr_does_not_block_large_writes() { let fixture = Fixture::new(); let mut clipboard = fixture.clipboard(b""); let state = fixture.0.join("state"); let state = format!("'{}'", state.to_str().unwrap().replace('\'', "'\\''")); clipboard .commands .as_mut() .unwrap() .as_mut() .unwrap() .writer .as_mut() .unwrap() .program = fixture.command( "noisy", &format!("/bin/dd if=/dev/zero bs=65536 count=8 >&2\nexec /bin/cat > {state}"), ); let text = "λ\n".repeat(300_000); clipboard.write(&text).unwrap(); assert_eq!(clipboard.read().unwrap(), text); } #[test] fn failed_process_cleanup_kills_and_reaps_a_running_child() { use std::io::Read; use std::os::unix::process::CommandExt; use std::process::{Command, Stdio}; unsafe extern "C" { fn waitpid(pid: i32, status: *mut i32, options: i32) -> i32; } let fixture = Fixture::new(); let hung = fixture.command("running", "printf R\nwhile :; do :; done"); let mut command = Command::new(hung); command .process_group(0) .stdout(Stdio::piped()) .stderr(Stdio::null()); let mut child = command.spawn().unwrap(); let pid = child.id() as i32; let mut ready = [0]; child .stdout .as_mut() .unwrap() .read_exact(&mut ready) .unwrap(); assert_eq!(ready, [b'R']); drop(ClipboardProcess(Some(child))); let mut status = 0; // SAFETY: waitpid's output pointer is valid; WNOHANG cannot block this check. assert_eq!(unsafe { waitpid(pid, &mut status, 1) }, -1); assert_eq!(std::io::Error::last_os_error().raw_os_error(), Some(10)); } #[test] fn immediate_injected_timeout_terminates_nonterminating_commands() { let fixture = Fixture::new(); let hung = fixture.command("hung", "while :; do :; done"); let mut clipboard = SystemClipboard::with_commands(hung.clone(), hung); // No assertion depends on wall-clock speed or a deadline remaining in the future. clipboard.timeout = Duration::ZERO; assert_eq!( clipboard.read().unwrap_err(), "System clipboard command timed out" ); assert_eq!( clipboard.write("text").unwrap_err(), "System clipboard command timed out" ); }