Files
2026-10-16 18:53:02 +02:00

193 lines
6.1 KiB
Rust

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"
);
}