Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ windows = { version = "0.62", features = [
"Storage_Streams",
"Foundation_Collections",
"Win32_UI_Shell",
"Win32_UI_Shell_Common",
"Win32_UI_Shell_PropertiesSystem",
"Win32_System_Com",
"Win32_System_Com_StructuredStorage",
Expand Down
55 changes: 53 additions & 2 deletions crates/cp-core/src/dib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,10 +167,25 @@ pub fn to_png(dib: &[u8]) -> Option<Vec<u8>> {
}

pub fn from_png(png: &[u8]) -> Option<Vec<u8>> {
if too_large(png.len()) {
from_encoded(png, image::ImageFormat::Png)
}

pub fn from_jpeg(jpeg: &[u8]) -> Option<Vec<u8>> {
from_encoded(jpeg, image::ImageFormat::Jpeg)
}

fn from_encoded(encoded: &[u8], format: image::ImageFormat) -> Option<Vec<u8>> {
if too_large(encoded.len()) {
return None;
}
let decoded = image::load_from_memory_with_format(encoded, format).ok()?;
from_image(&decoded)
}

pub fn from_image(decoded: &image::DynamicImage) -> Option<Vec<u8>> {
if decoded.width() == 0 || decoded.height() == 0 {
return None;
}
let decoded = image::load_from_memory_with_format(png, image::ImageFormat::Png).ok()?;
let mut bmp = std::io::Cursor::new(Vec::new());
decoded.write_to(&mut bmp, image::ImageFormat::Bmp).ok()?;
let bmp = bmp.into_inner();
Expand Down Expand Up @@ -590,6 +605,42 @@ mod tests {
assert_eq!(from_png(&vec![0u8; LARGEST_BITMAP + 1]), None);
}

#[test]
fn a_jpeg_becomes_a_bitmap_too_and_a_png_is_not_a_jpeg() {
let dib = Dib::rgb32(4, 4).build();
let png = to_png(&dib).expect("png");
let mut jpeg = Vec::new();
image::load_from_memory(&png)
.expect("relee")
.to_rgb8()
.write_to(
&mut std::io::Cursor::new(&mut jpeg),
image::ImageFormat::Jpeg,
)
.expect("jpeg");
let back = from_jpeg(&jpeg).expect("dib");
let head = header(&back).expect("cabecera");
assert_eq!((head.width, head.height.abs()), (4, 4));
assert_eq!(head.bit_count, 24, "un JPEG no trae alfa y el DIB tampoco");
assert_eq!(
from_jpeg(&png),
None,
"un PNG no se cuela por la puerta del JPEG"
);
assert_eq!(from_jpeg(&[]), None);
assert_eq!(from_jpeg(&vec![0u8; LARGEST_BITMAP + 1]), None);
}

#[test]
fn a_decoded_image_with_no_pixels_is_no_bitmap() {
assert_eq!(from_image(&image::DynamicImage::new_rgb8(0, 0)), None);
assert_eq!(from_image(&image::DynamicImage::new_rgba8(3, 0)), None);
let opaque = from_image(&image::DynamicImage::new_rgb8(1, 1)).expect("dib");
assert_eq!(header(&opaque).expect("cabecera").bit_count, 24);
let translucent = from_image(&image::DynamicImage::new_rgba8(1, 1)).expect("dib");
assert_eq!(header(&translucent).expect("cabecera").bit_count, 32);
}

#[test]
fn a_png_from_a_dib_is_a_png() {
let dib = Dib::rgb32(4, 4).build();
Expand Down
34 changes: 18 additions & 16 deletions crates/cp-win-sys/src/clipboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,22 +45,7 @@ impl Clipboard {
if handle.is_invalid() {
return None;
}
let global = HGLOBAL(handle.0);

let size = unsafe { GlobalSize(global) };
if size == 0 {
return None;
}

let address = unsafe { GlobalLock(global) };
if address.is_null() {
return None;
}

let bytes = unsafe { std::slice::from_raw_parts(address.cast::<u8>(), size) }.to_vec();

let _ = unsafe { GlobalUnlock(global) };
Some(bytes)
global_bytes(HGLOBAL(handle.0), usize::MAX)
}

pub fn size_of(&self, id: u32) -> Option<usize> {
Expand All @@ -80,6 +65,23 @@ impl Drop for Clipboard {
}
}

pub(crate) fn global_bytes(global: HGLOBAL, up_to: usize) -> Option<Vec<u8>> {
let size = unsafe { GlobalSize(global) };
if size == 0 || size > up_to {
return None;
}

let address = unsafe { GlobalLock(global) };
if address.is_null() {
return None;
}

let bytes = unsafe { std::slice::from_raw_parts(address.cast::<u8>(), size) }.to_vec();

let _ = unsafe { GlobalUnlock(global) };
Some(bytes)
}

pub fn sequence() -> Option<i64> {
let raw = unsafe { GetClipboardSequenceNumber() };
(raw != 0).then_some(i64::from(raw))
Expand Down
119 changes: 119 additions & 0 deletions crates/cp-win-sys/src/files.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
use std::os::windows::ffi::OsStrExt;
use std::path::Path;
use windows::Win32::UI::Shell::{
ILCreateFromPathW, ILFree, SHOpenFolderAndSelectItems, ShellExecuteW,
};
use windows::Win32::UI::WindowsAndMessaging::SW_SHOWNORMAL;
use windows::core::{PCWSTR, w};

use crate::com::{Apartment, shell_path};

const RUNS_WHEN_OPENED: [&str; 8] = ["bat", "cmd", "com", "exe", "lnk", "msi", "ps1", "scr"];

const LAUNCHED: usize = 32;

pub fn runs_when_opened(path: &Path) -> bool {
path.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| RUNS_WHEN_OPENED.contains(&ext.to_ascii_lowercase().as_str()))
}

pub fn open(path: &Path) -> bool {
if runs_when_opened(path) {
return reveal(path);
}
let Some(wide) = wide_of(path) else {
return false;
};
let _apartment = Apartment::enter();

let instance = unsafe {
ShellExecuteW(
None,
w!("open"),
PCWSTR(wide.as_ptr()),
None,
None,
SW_SHOWNORMAL,
)
};
instance.0 as usize > LAUNCHED
}

pub fn reveal(path: &Path) -> bool {
let Some(wide) = wide_of(path) else {
return false;
};
let _apartment = Apartment::enter();

let list = unsafe { ILCreateFromPathW(PCWSTR(wide.as_ptr())) };
if list.is_null() {
return false;
}

let selected = unsafe { SHOpenFolderAndSelectItems(list, None, 0) };

unsafe { ILFree(Some(list)) };
selected.is_ok()
}

fn wide_of(path: &Path) -> Option<Vec<u16>> {
let absolute = shell_path(path)?;
Some(
absolute
.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect(),
)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn what_would_run_is_revealed_instead_of_opened() {
for name in [
"setup.exe",
"script.BAT",
"run.cmd",
"tool.ps1",
"install.msi",
"atajo.lnk",
"viejo.com",
"fondo.scr",
] {
assert!(runs_when_opened(Path::new(name)), "{name} se ejecuta");
}
for name in [
"informe.pdf",
"foto.png",
"nota.txt",
"sin-extension",
"carpeta.d",
] {
assert!(!runs_when_opened(Path::new(name)), "{name} se abre");
}
}

#[test]
fn a_path_that_is_not_there_is_neither_opened_nor_revealed() {
let missing = Path::new(r"C:\no-existe-nada-de-nada\ni-esto.txt");
assert!(!open(missing));
assert!(!reveal(missing));
assert_eq!(wide_of(missing), None);
}

#[test]
fn the_wide_path_is_absolute_and_ends_in_a_terminator() {
let wide = wide_of(Path::new(".")).expect("ruta");
assert_eq!(wide.last(), Some(&0));
let text = String::from_utf16_lossy(&wide[..wide.len() - 1]);
assert!(text.contains(':'), "{text} no es absoluta");
assert!(
!text.starts_with(r"\\?\"),
"{text} lleva el prefijo que el shell no entiende"
);
}
}
2 changes: 2 additions & 0 deletions crates/cp-win-sys/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

pub mod clipboard;
pub mod com;
pub mod files;
pub mod formats;
pub mod frontmost;
pub mod keystroke;
pub mod media;
pub mod ocr;
pub mod ole;
pub mod paths;
pub mod permissions;
pub mod reading;
Expand Down
125 changes: 125 additions & 0 deletions crates/cp-win-sys/src/ole.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
use windows::Win32::Foundation::HGLOBAL;
use windows::Win32::System::Com::{
DVASPECT_CONTENT, FORMATETC, IDataObject, IStream, STGMEDIUM, STREAM_SEEK_SET, TYMED,
TYMED_HGLOBAL, TYMED_ISTREAM,
};
use windows::Win32::System::Ole::{
OleGetClipboard, OleInitialize, OleUninitialize, ReleaseStgMedium,
};

use crate::clipboard::global_bytes;

const CHUNK: usize = 64 * 1024;

pub struct Ole {
ours: bool,
}

impl Ole {
pub fn enter() -> Self {
let entered = unsafe { OleInitialize(None) };
Self {
ours: entered.is_ok(),
}
}
}

impl Drop for Ole {
fn drop(&mut self) {
if self.ours {
unsafe { OleUninitialize() };
}
}
}

pub fn indexed_contents(format: u32, count: usize, up_to: usize) -> Vec<Option<Vec<u8>>> {
let _ole = Ole::enter();
let Ok(data) = (unsafe { OleGetClipboard() }) else {
return vec![None; count];
};
(0..count)
.map(|index| {
let indexed = i32::try_from(index).ok();
indexed
.and_then(|lindex| one_of(&data, format, lindex, up_to))
.or_else(|| {
(count == 1)
.then(|| one_of(&data, format, -1, up_to))
.flatten()
})
})
.collect()
}

fn one_of(data: &IDataObject, format: u32, lindex: i32, up_to: usize) -> Option<Vec<u8>> {
let asked = FORMATETC {
cfFormat: u16::try_from(format).ok()?,
ptd: std::ptr::null_mut(),
dwAspect: DVASPECT_CONTENT.0,
lindex,
tymed: (TYMED_HGLOBAL.0 | TYMED_ISTREAM.0) as u32,
};

let mut medium: STGMEDIUM = unsafe { data.GetData(&asked) }.ok()?;
let bytes = match TYMED(medium.tymed as i32) {
TYMED_HGLOBAL => {
let global: HGLOBAL = unsafe { medium.u.hGlobal };
global_bytes(global, up_to)
}
TYMED_ISTREAM => {
let stream: Option<IStream> = unsafe { (*medium.u.pstm).clone() };
stream.and_then(|stream| stream_bytes(&stream, up_to))
}
_ => None,
};

unsafe { ReleaseStgMedium(&mut medium) };
bytes
}

fn stream_bytes(stream: &IStream, up_to: usize) -> Option<Vec<u8>> {
let _ = unsafe { stream.Seek(0, STREAM_SEEK_SET, None) };
let mut out = Vec::new();
let mut chunk = vec![0u8; CHUNK];
loop {
let mut read = 0u32;

let status =
unsafe { stream.Read(chunk.as_mut_ptr().cast(), CHUNK as u32, Some(&mut read)) };
if status.is_err() && read == 0 {
return None;
}
if read == 0 {
break;
}
out.extend_from_slice(&chunk[..read as usize]);
if out.len() > up_to {
return None;
}
}
(!out.is_empty()).then_some(out)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn entering_twice_is_not_a_problem() {
let first = Ole::enter();
let second = Ole::enter();
drop(second);
drop(first);
}

#[test]
fn asking_for_nothing_gets_nothing_and_does_not_touch_the_clipboard() {
assert!(indexed_contents(0xC000, 0, CHUNK).is_empty());
}

#[test]
fn a_format_nobody_offers_comes_back_absent_for_every_index() {
let seen = indexed_contents(0xFFFE, 3, CHUNK);
assert_eq!(seen, vec![None, None, None]);
}
}
Loading
Loading