use std::io;
use std::ptr::null_mut;
use std::thread;
use std::time::Duration;
use a7n_core::clipboard::{FILES, IMAGE_PNG, TEXT_HTML, TEXT_PLAIN, limit_for, validate_png};
use a7n_proto::limits::CLIPBOARD_TEXT_MAX;
use windows_sys::Win32::Foundation::{GlobalFree, HWND};
use windows_sys::Win32::System::DataExchange::{
CloseClipboard, GetClipboardData, OpenClipboard, RegisterClipboardFormatW, SetClipboardData,
};
use windows_sys::Win32::System::Memory::{
GMEM_MOVEABLE, GlobalAlloc, GlobalLock, GlobalSize, GlobalUnlock,
};
use windows_sys::Win32::System::Ole::{CF_DIB, CF_HDROP, CF_UNICODETEXT};
use windows_sys::core::w;
use crate::clipboard::cf_html;
pub(super) const RETRY_INTERVAL: Duration = Duration::from_millis(10);
const OPEN_ATTEMPTS: usize = 5;
pub(super) const MAX_OS_BYTES: usize = (CLIPBOARD_TEXT_MAX + 1) * size_of::<u16>();
pub(super) struct Formats {
pub html: u32,
pub png: u32,
pub exclude: u32,
pub history: u32,
pub cloud: u32,
pub drop_effect: u32,
}
impl Formats {
pub(super) fn new() -> io::Result<Self> {
let formats = unsafe {
Self {
png: RegisterClipboardFormatW(w!("PNG")),
html: RegisterClipboardFormatW(w!("HTML Format")),
exclude: RegisterClipboardFormatW(w!(
"ExcludeClipboardContentFromMonitorProcessing"
)),
history: RegisterClipboardFormatW(w!("CanIncludeInClipboardHistory")),
cloud: RegisterClipboardFormatW(w!("CanUploadToCloudClipboard")),
drop_effect: RegisterClipboardFormatW(w!("Preferred DropEffect")),
}
};
if [
formats.html,
formats.png,
formats.exclude,
formats.history,
formats.cloud,
formats.drop_effect,
]
.contains(&0)
{
return Err(io::Error::last_os_error());
}
Ok(formats)
}
pub(super) fn format(&self, mime: &str) -> Option<u32> {
match mime {
IMAGE_PNG => Some(self.png),
TEXT_PLAIN => Some(u32::from(CF_UNICODETEXT)),
TEXT_HTML => Some(self.html),
FILES => Some(u32::from(CF_HDROP)),
_ => None,
}
}
pub(super) fn write(&self, format: u32, data: &[u8]) -> bool {
if format == self.png || format == u32::from(CF_DIB) {
if data.len() > limit_for(IMAGE_PNG) || validate_png(data).is_err() {
return false;
}
return if format == self.png {
write_global(format, data)
} else {
super::image::to_dib(data).is_some_and(|dib| write_global(format, &dib))
};
}
if format == u32::from(CF_HDROP) {
if data.len() > limit_for(FILES) {
return false;
}
return super::drop_files::build(&crate::clipboard::uri_list::decode(data))
.is_some_and(|drop| write_global(format, &drop));
}
if data.len() > limit_for(TEXT_PLAIN) {
return false;
}
let Ok(text) = std::str::from_utf8(data) else {
return false;
};
let bytes = if format == u32::from(CF_UNICODETEXT) {
text.encode_utf16()
.chain(Some(0))
.flat_map(u16::to_ne_bytes)
.collect()
} else if format == self.html {
cf_html::encode(text)
} else {
return false;
};
write_global(format, &bytes)
}
}
pub(super) struct Open;
impl Open {
pub(super) fn clipboard(hwnd: HWND) -> Option<Self> {
for attempt in 0..OPEN_ATTEMPTS {
if unsafe { OpenClipboard(hwnd) } != 0 {
return Some(Self);
}
if attempt + 1 < OPEN_ATTEMPTS {
thread::sleep(RETRY_INTERVAL);
}
}
None
}
}
impl Drop for Open {
fn drop(&mut self) {
unsafe { CloseClipboard() };
}
}
pub(super) fn read_global(format: u32, maximum: usize) -> Option<Vec<u8>> {
let handle = unsafe { GetClipboardData(format) };
if handle.is_null() {
return None;
}
let size = unsafe { GlobalSize(handle) };
if size == 0 || size > maximum {
return None;
}
let pointer = unsafe { GlobalLock(handle) };
if pointer.is_null() {
return None;
}
let bytes = unsafe { std::slice::from_raw_parts(pointer.cast::<u8>(), size) }.to_vec();
unsafe { GlobalUnlock(handle) };
Some(bytes)
}
pub(super) fn write_global(format: u32, bytes: &[u8]) -> bool {
let handle = unsafe { GlobalAlloc(GMEM_MOVEABLE, bytes.len()) };
if handle.is_null() {
return false;
}
let pointer = unsafe { GlobalLock(handle) };
if pointer.is_null() {
unsafe { GlobalFree(handle) };
return false;
}
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), pointer.cast::<u8>(), bytes.len());
GlobalUnlock(handle);
}
if unsafe { SetClipboardData(format, handle) }.is_null() {
unsafe { GlobalFree(handle) };
return false;
}
true
}
pub(super) fn offer(format: u32) {
unsafe { SetClipboardData(format, null_mut()) };
}