use std::ffi::c_void;
use std::sync::OnceLock;
use crate::{ClipboardError, MimeType, Selection, Uri};
use super::Backend;
#[allow(clippy::upper_case_acronyms, non_camel_case_types, dead_code)]
mod win32_types {
use std::ffi::c_void;
pub(super) type BOOL = i32;
pub(super) type DWORD = u32;
pub(super) type UINT = u32;
pub(super) type SIZE_T = usize;
pub(super) type HWND = *mut c_void;
pub(super) type HANDLE = *mut c_void;
pub(super) type HGLOBAL = HANDLE;
}
use win32_types::{BOOL, DWORD, HANDLE, HGLOBAL, HWND, SIZE_T, UINT};
const CF_UNICODETEXT: UINT = 13;
const CF_HDROP: UINT = 15;
const CF_DIBV5: UINT = 17;
const GMEM_MOVEABLE: UINT = 0x0002;
#[link(name = "user32")]
unsafe extern "system" {
fn OpenClipboard(hwnd: HWND) -> BOOL;
fn CloseClipboard() -> BOOL;
fn EmptyClipboard() -> BOOL;
fn GetClipboardData(format: UINT) -> HANDLE;
fn SetClipboardData(format: UINT, hMem: HANDLE) -> HANDLE;
fn IsClipboardFormatAvailable(format: UINT) -> BOOL;
fn EnumClipboardFormats(format: UINT) -> UINT;
fn RegisterClipboardFormatW(lpszFormat: *const u16) -> UINT;
}
#[link(name = "kernel32")]
unsafe extern "system" {
fn GlobalAlloc(uFlags: UINT, dwBytes: SIZE_T) -> HGLOBAL;
fn GlobalLock(hMem: HGLOBAL) -> *mut c_void;
fn GlobalUnlock(hMem: HGLOBAL) -> BOOL;
fn GlobalFree(hMem: HGLOBAL) -> HGLOBAL;
fn GlobalSize(hMem: HGLOBAL) -> SIZE_T;
#[allow(dead_code)]
fn GetLastError() -> DWORD;
}
fn cf_html_format() -> UINT {
static ID: OnceLock<UINT> = OnceLock::new();
*ID.get_or_init(|| {
let name: Vec<u16> = "HTML Format\0".encode_utf16().collect();
unsafe { RegisterClipboardFormatW(name.as_ptr()) }
})
}
fn cf_png_format() -> UINT {
static ID: OnceLock<UINT> = OnceLock::new();
*ID.get_or_init(|| {
let name: Vec<u16> = "PNG\0".encode_utf16().collect();
unsafe { RegisterClipboardFormatW(name.as_ptr()) }
})
}
fn cf_rtf_format() -> UINT {
static ID: OnceLock<UINT> = OnceLock::new();
*ID.get_or_init(|| {
let name: Vec<u16> = "Rich Text Format\0".encode_utf16().collect();
unsafe { RegisterClipboardFormatW(name.as_ptr()) }
})
}
struct ClipboardOpen;
impl ClipboardOpen {
fn new() -> Result<Self, ClipboardError> {
let ok = unsafe { OpenClipboard(std::ptr::null_mut()) };
if ok == 0 {
return Err(ClipboardError::io_other("OpenClipboard failed"));
}
Ok(Self)
}
}
impl Drop for ClipboardOpen {
fn drop(&mut self) {
unsafe {
CloseClipboard();
}
}
}
struct LockedHandle {
handle: HGLOBAL,
ptr: *mut c_void,
}
impl LockedHandle {
fn new(handle: HGLOBAL) -> Result<Self, ClipboardError> {
let ptr = unsafe { GlobalLock(handle) };
if ptr.is_null() {
return Err(ClipboardError::io_other("GlobalLock failed"));
}
Ok(Self { handle, ptr })
}
fn ptr(&self) -> *mut c_void {
self.ptr
}
}
impl Drop for LockedHandle {
fn drop(&mut self) {
unsafe {
GlobalUnlock(self.handle);
}
}
}
fn set_text(bytes: &[u8]) -> Result<(), ClipboardError> {
let text = std::str::from_utf8(bytes)
.map_err(|_| ClipboardError::io_other("clipboard text is not valid UTF-8"))?;
let utf16: Vec<u16> = text.encode_utf16().chain(std::iter::once(0)).collect();
let byte_len = utf16.len() * 2;
let _guard = ClipboardOpen::new()?;
let ok = unsafe { EmptyClipboard() };
if ok == 0 {
return Err(ClipboardError::io_other("EmptyClipboard failed"));
}
let handle: HGLOBAL = unsafe { GlobalAlloc(GMEM_MOVEABLE, byte_len) };
if handle.is_null() {
return Err(ClipboardError::io_other("GlobalAlloc failed"));
}
let locked = match LockedHandle::new(handle) {
Ok(l) => l,
Err(e) => {
unsafe { GlobalFree(handle) };
return Err(e);
}
};
unsafe {
std::ptr::copy_nonoverlapping(utf16.as_ptr(), locked.ptr().cast::<u16>(), utf16.len());
}
drop(locked);
let result = unsafe { SetClipboardData(CF_UNICODETEXT, handle) };
if result.is_null() {
unsafe { GlobalFree(handle) };
return Err(ClipboardError::io_other("SetClipboardData failed"));
}
Ok(())
}
fn get_text() -> Result<Vec<u8>, ClipboardError> {
let _guard = ClipboardOpen::new()?;
let avail = unsafe { IsClipboardFormatAvailable(CF_UNICODETEXT) };
if avail == 0 {
return Err(ClipboardError::UnsupportedMime);
}
let handle = unsafe { GetClipboardData(CF_UNICODETEXT) };
if handle.is_null() {
return Err(ClipboardError::io_other("GetClipboardData failed"));
}
let locked = LockedHandle::new(handle)?;
let byte_size = unsafe { GlobalSize(handle) };
let max_units = byte_size / 2;
let slice = unsafe { std::slice::from_raw_parts(locked.ptr().cast::<u16>(), max_units) };
let len_without_nul = slice.iter().position(|&c| c == 0).unwrap_or(max_units);
let text_slice = &slice[..len_without_nul];
let text = String::from_utf16(text_slice)
.map_err(|_| ClipboardError::io_other("clipboard data is not valid UTF-16"))?;
Ok(text.into_bytes())
}
fn set_bytes(format: UINT, bytes: &[u8]) -> Result<(), ClipboardError> {
let _guard = ClipboardOpen::new()?;
let ok = unsafe { EmptyClipboard() };
if ok == 0 {
return Err(ClipboardError::io_other("EmptyClipboard failed"));
}
let len = bytes.len();
let handle: HGLOBAL = unsafe { GlobalAlloc(GMEM_MOVEABLE, len) };
if handle.is_null() {
return Err(ClipboardError::io_other("GlobalAlloc failed"));
}
let locked = match LockedHandle::new(handle) {
Ok(l) => l,
Err(e) => {
unsafe { GlobalFree(handle) };
return Err(e);
}
};
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), locked.ptr().cast::<u8>(), len);
}
drop(locked);
let result = unsafe { SetClipboardData(format, handle) };
if result.is_null() {
unsafe { GlobalFree(handle) };
return Err(ClipboardError::io_other("SetClipboardData failed"));
}
Ok(())
}
fn get_bytes(format: UINT) -> Result<Vec<u8>, ClipboardError> {
let _guard = ClipboardOpen::new()?;
let avail = unsafe { IsClipboardFormatAvailable(format) };
if avail == 0 {
return Err(ClipboardError::UnsupportedMime);
}
let handle = unsafe { GetClipboardData(format) };
if handle.is_null() {
return Err(ClipboardError::io_other("GetClipboardData failed"));
}
let locked = LockedHandle::new(handle)?;
let byte_size = unsafe { GlobalSize(handle) };
let slice = unsafe { std::slice::from_raw_parts(locked.ptr().cast::<u8>(), byte_size) };
let out = slice.to_vec();
Ok(out)
}
fn set_html(html: &str) -> Result<(), ClipboardError> {
let id = cf_html_format();
if id == 0 {
return Err(ClipboardError::io_other(
"RegisterClipboardFormatW failed for CF_HTML",
));
}
let envelope = crate::cf_html::wrap(html);
set_bytes(id, &envelope)
}
fn get_html() -> Result<Vec<u8>, ClipboardError> {
let id = cf_html_format();
if id == 0 {
return Err(ClipboardError::io_other(
"RegisterClipboardFormatW failed for CF_HTML",
));
}
let envelope = get_bytes(id)?;
let fragment = crate::cf_html::unwrap(&envelope)?;
Ok(fragment.into_bytes())
}
fn set_rtf(bytes: &[u8]) -> Result<(), ClipboardError> {
let id = cf_rtf_format();
if id == 0 {
return Err(ClipboardError::io_other(
"RegisterClipboardFormatW failed for CF_RTF",
));
}
set_bytes(id, bytes)
}
fn get_rtf() -> Result<Vec<u8>, ClipboardError> {
let id = cf_rtf_format();
if id == 0 {
return Err(ClipboardError::io_other(
"RegisterClipboardFormatW failed for CF_RTF",
));
}
get_bytes(id)
}
fn set_uri_list(bytes: &[u8]) -> Result<(), ClipboardError> {
let uris = crate::uri::decode_uri_list(bytes)?;
let mut paths: Vec<std::path::PathBuf> = Vec::with_capacity(uris.len());
for u in &uris {
match u {
Uri::File(p) => paths.push(p.clone()),
Uri::Other(_) => {
return Err(ClipboardError::InvalidUri);
}
}
}
let path_refs: Vec<&std::path::Path> = paths.iter().map(|p| p.as_path()).collect();
let hdrop_bytes = crate::cf_hdrop::build(&path_refs)?;
set_bytes(CF_HDROP, &hdrop_bytes)
}
fn get_uri_list() -> Result<Vec<u8>, ClipboardError> {
let hdrop_bytes = get_bytes(CF_HDROP)?;
let paths = crate::cf_hdrop::parse(&hdrop_bytes)?;
let uris: Vec<Uri> = paths.into_iter().map(Uri::File).collect();
crate::uri::encode_uri_list(&uris)
}
fn set_png(bytes: &[u8]) -> Result<(), ClipboardError> {
let png_id = cf_png_format();
if png_id == 0 {
return Err(ClipboardError::io_other(
"RegisterClipboardFormatW failed for PNG",
));
}
let dib = crate::dib_png::png_to_dib(bytes)?;
let _guard = ClipboardOpen::new()?;
let ok = unsafe { EmptyClipboard() };
if ok == 0 {
return Err(ClipboardError::io_other("EmptyClipboard failed"));
}
let set_raw = |format: UINT, data: &[u8]| -> Result<(), ClipboardError> {
let len = data.len();
let handle: HGLOBAL = unsafe { GlobalAlloc(GMEM_MOVEABLE, len) };
if handle.is_null() {
return Err(ClipboardError::io_other("GlobalAlloc failed"));
}
let locked = match LockedHandle::new(handle) {
Ok(l) => l,
Err(e) => {
unsafe { GlobalFree(handle) };
return Err(e);
}
};
unsafe {
std::ptr::copy_nonoverlapping(data.as_ptr(), locked.ptr().cast::<u8>(), len);
}
drop(locked);
let result = unsafe { SetClipboardData(format, handle) };
if result.is_null() {
unsafe { GlobalFree(handle) };
return Err(ClipboardError::io_other("SetClipboardData failed"));
}
Ok(())
};
set_raw(png_id, bytes)?;
set_raw(CF_DIBV5, &dib)?;
Ok(())
}
fn get_png() -> Result<Vec<u8>, ClipboardError> {
let png_id = cf_png_format();
let _guard = ClipboardOpen::new()?;
if png_id != 0 {
let avail = unsafe { IsClipboardFormatAvailable(png_id) };
if avail != 0 {
let handle = unsafe { GetClipboardData(png_id) };
if !handle.is_null() {
let locked = LockedHandle::new(handle)?;
let byte_size = unsafe { GlobalSize(handle) };
let slice =
unsafe { std::slice::from_raw_parts(locked.ptr().cast::<u8>(), byte_size) };
return Ok(slice.to_vec());
}
}
}
let avail = unsafe { IsClipboardFormatAvailable(CF_DIBV5) };
if avail == 0 {
return Err(ClipboardError::UnsupportedMime);
}
let handle = unsafe { GetClipboardData(CF_DIBV5) };
if handle.is_null() {
return Err(ClipboardError::io_other(
"GetClipboardData(CF_DIBV5) failed",
));
}
let locked = LockedHandle::new(handle)?;
let byte_size = unsafe { GlobalSize(handle) };
let slice = unsafe { std::slice::from_raw_parts(locked.ptr().cast::<u8>(), byte_size) };
let dib = slice.to_vec();
drop(locked);
crate::dib_png::dib_to_png(&dib)
}
pub struct WindowsBackend;
impl WindowsBackend {
#[allow(dead_code)]
pub(crate) fn new() -> Self {
Self
}
}
impl Backend for WindowsBackend {
fn kind(&self) -> crate::BackendKind {
crate::BackendKind::Windows
}
fn capabilities(&self) -> crate::Capabilities {
crate::Capabilities::WRITE
| crate::Capabilities::READ
| crate::Capabilities::CLEAR
| crate::Capabilities::AVAILABLE
| crate::Capabilities::IMAGE
| crate::Capabilities::RICH_TEXT
| crate::Capabilities::URI_LIST
}
fn set(&self, sel: Selection, mime: MimeType, bytes: &[u8]) -> Result<(), ClipboardError> {
if sel != Selection::Clipboard {
return Err(ClipboardError::UnsupportedMime);
}
match mime {
MimeType::Text => set_text(bytes),
MimeType::Html => {
let html = std::str::from_utf8(bytes)
.map_err(|_| ClipboardError::io_other("HTML payload is not valid UTF-8"))?;
set_html(html)
}
MimeType::Rtf => set_rtf(bytes),
MimeType::UriList => set_uri_list(bytes),
MimeType::Png => set_png(bytes),
MimeType::Custom(_) => Err(ClipboardError::UnsupportedMime),
}
}
fn get(&self, sel: Selection, mime: MimeType) -> Result<Vec<u8>, ClipboardError> {
if sel != Selection::Clipboard {
return Err(ClipboardError::UnsupportedMime);
}
match mime {
MimeType::Text => get_text(),
MimeType::Html => get_html(),
MimeType::Rtf => get_rtf(),
MimeType::UriList => get_uri_list(),
MimeType::Png => get_png(),
_ => Err(ClipboardError::UnsupportedMime),
}
}
fn clear(&self, sel: Selection) -> Result<(), ClipboardError> {
if sel != Selection::Clipboard {
return Err(ClipboardError::UnsupportedMime);
}
let _guard = ClipboardOpen::new()?;
let ok = unsafe { EmptyClipboard() };
if ok == 0 {
return Err(ClipboardError::io_other("EmptyClipboard failed"));
}
Ok(())
}
fn available(&self, sel: Selection) -> Result<Vec<MimeType>, ClipboardError> {
if sel != Selection::Clipboard {
return Err(ClipboardError::UnsupportedMime);
}
let _guard = ClipboardOpen::new()?;
let mut out = Vec::new();
let html_id = cf_html_format();
let rtf_id = cf_rtf_format();
let png_id = cf_png_format();
let mut png_seen = false;
let mut fmt: UINT = 0;
loop {
fmt = unsafe { EnumClipboardFormats(fmt) };
if fmt == 0 {
break;
}
if fmt == CF_UNICODETEXT {
out.push(MimeType::Text);
} else if fmt == CF_HDROP {
out.push(MimeType::UriList);
} else if html_id != 0 && fmt == html_id {
out.push(MimeType::Html);
} else if rtf_id != 0 && fmt == rtf_id {
out.push(MimeType::Rtf);
} else if (png_id != 0 && fmt == png_id) || fmt == CF_DIBV5 {
if !png_seen {
out.push(MimeType::Png);
png_seen = true;
}
}
}
Ok(out)
}
}