diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-20 02:51:59 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-20 02:51:59 +0300 |
| commit | ed478be9835e984c42bc1e30fe84b5fb01a8c5d4 (patch) | |
| tree | 353b6ea5c7368a9f3b477bc8f1bbcd248e377577 /src/server/clipboard_image.rs | |
| parent | 2b97799b6715fd6c355db9c225029765971915e6 (diff) | |
fix: bridge remote clipboard image paste
refs #205
Diffstat (limited to 'src/server/clipboard_image.rs')
| -rw-r--r-- | src/server/clipboard_image.rs | 128 |
1 files changed, 128 insertions, 0 deletions
diff --git a/src/server/clipboard_image.rs b/src/server/clipboard_image.rs new file mode 100644 index 00000000..299267d3 --- /dev/null +++ b/src/server/clipboard_image.rs @@ -0,0 +1,128 @@ +use std::fs; +use std::io::{self, Write as _}; +use std::path::{Path, PathBuf}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +const STAGED_CLIPBOARD_IMAGE_MAX_AGE: Duration = Duration::from_secs(24 * 60 * 60); + +pub(crate) struct StagedClipboardImage { + pub(crate) path: PathBuf, + pub(crate) paste_text: String, +} + +pub(crate) fn stage( + client_id: u64, + extension: &str, + data: &[u8], +) -> io::Result<StagedClipboardImage> { + use std::os::unix::fs::OpenOptionsExt; + + let extension = sanitize_extension(extension); + let dir = ensure_staging_dir()?; + cleanup_stale(&dir); + + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_nanos()) + .unwrap_or(0); + + for attempt in 0..100 { + let path = dir.join(format!( + "client-{client_id}-clipboard-{unique}-{attempt}.{extension}" + )); + let mut file = match fs::OpenOptions::new() + .write(true) + .create_new(true) + .mode(0o600) + .open(&path) + { + Ok(file) => file, + Err(err) if err.kind() == io::ErrorKind::AlreadyExists => continue, + Err(err) => return Err(err), + }; + file.write_all(data)?; + return Ok(StagedClipboardImage { + paste_text: path.to_string_lossy().into_owned(), + path, + }); + } + + Err(io::Error::new( + io::ErrorKind::AlreadyExists, + "failed to allocate unique clipboard image staging path", + )) +} + +pub(crate) fn remove_files(paths: Vec<PathBuf>) { + for path in paths { + let _ = fs::remove_file(path); + } +} + +fn sanitize_extension(extension: &str) -> &'static str { + if extension.eq_ignore_ascii_case("png") { + "png" + } else if extension.eq_ignore_ascii_case("jpg") || extension.eq_ignore_ascii_case("jpeg") { + "jpg" + } else if extension.eq_ignore_ascii_case("gif") { + "gif" + } else if extension.eq_ignore_ascii_case("webp") { + "webp" + } else if extension.eq_ignore_ascii_case("bmp") { + "bmp" + } else { + "png" + } +} + +fn staging_dir() -> PathBuf { + let user_id = unsafe { libc::geteuid() }; + std::env::temp_dir().join(format!("herdr-clipboard-images-{user_id}")) +} + +fn ensure_staging_dir() -> io::Result<PathBuf> { + use std::os::unix::fs::PermissionsExt; + + let dir = staging_dir(); + fs::create_dir_all(&dir)?; + let metadata = fs::metadata(&dir)?; + if !metadata.is_dir() { + return Err(io::Error::other(format!( + "clipboard image staging path is not a directory: {}", + dir.display() + ))); + } + fs::set_permissions(&dir, fs::Permissions::from_mode(0o700))?; + Ok(dir) +} + +fn cleanup_stale(dir: &Path) { + let Ok(entries) = fs::read_dir(dir) else { + return; + }; + for entry in entries.flatten() { + let path = entry.path(); + let Ok(metadata) = entry.metadata() else { + continue; + }; + let Ok(modified) = metadata.modified() else { + continue; + }; + if modified.elapsed().unwrap_or_default() > STAGED_CLIPBOARD_IMAGE_MAX_AGE { + let _ = fs::remove_file(path); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn sanitize_extension_accepts_known_image_extensions() { + assert_eq!(sanitize_extension("PNG"), "png"); + assert_eq!(sanitize_extension("jpeg"), "jpg"); + assert_eq!(sanitize_extension("webp"), "webp"); + assert_eq!(sanitize_extension("sh"), "png"); + } +} |
