Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove leaks from macOS clipboard getters #106

Merged
merged 1 commit into from
Aug 28, 2023
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
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ edition = "2018"

[features]
default = ["image-data"]
image-data = ["core-graphics", "image", "winapi/minwindef", "winapi/wingdi", "winapi/winnt"]
image-data = ["core-graphics", "once_cell", "image", "winapi/minwindef", "winapi/wingdi", "winapi/winnt"]
wayland-data-control = ["wl-clipboard-rs"]

[dependencies]
Expand All @@ -34,7 +34,7 @@ log = "0.4"
objc = "0.2"
objc_id = "0.1"
objc-foundation = "0.1"
once_cell = "1"
once_cell = { version = "1", optional = true }
core-graphics = { version = "0.22", optional = true }
image = { version = "0.24", optional = true, default-features = false, features = ["tiff"] }

Expand Down
123 changes: 59 additions & 64 deletions src/platform/osx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,25 @@ use core_graphics::{
};
use objc::{
msg_send,
rc::autoreleasepool,
runtime::{Class, Object},
sel, sel_impl,
};
use objc_foundation::{INSArray, INSObject, INSString, NSArray, NSDictionary, NSObject, NSString};
use objc_foundation::{INSArray, INSFastEnumeration, INSString, NSArray, NSObject, NSString};
use objc_id::{Id, Owned};
#[cfg(feature = "image-data")]
use once_cell::sync::Lazy;
use std::borrow::Cow;
use std::{borrow::Cow, ptr::NonNull};

// Required to bring NSPasteboard into the path of the class-resolver
#[link(name = "AppKit", kind = "framework")]
extern "C" {
static NSPasteboardTypeHTML: *const Object;
static NSPasteboardTypeString: *const Object;
#[cfg(feature = "image-data")]
static NSPasteboardTypeTIFF: *const Object;
}

static NSSTRING_CLASS: Lazy<&Class> = Lazy::new(|| Class::get("NSString").unwrap());
#[cfg(feature = "image-data")]
static NSIMAGE_CLASS: Lazy<&Class> = Lazy::new(|| Class::get("NSImage").unwrap());

Expand Down Expand Up @@ -181,73 +184,72 @@ impl<'clipboard> Get<'clipboard> {
}

pub(crate) fn text(self) -> Result<String, Error> {
let string_class = object_class(&NSSTRING_CLASS);
let classes: Id<NSArray<NSObject, Owned>> = NSArray::from_vec(vec![string_class]);
let options: Id<NSDictionary<NSObject, NSObject>> = NSDictionary::new();

let string_array: Id<NSArray<NSString>> = unsafe {
let obj: *mut NSArray<NSString> =
msg_send![self.pasteboard, readObjectsForClasses:&*classes options:&*options];

if obj.is_null() {
return Err(Error::ContentNotAvailable);
} else {
Id::from_ptr(obj)
// XXX: There does not appear to be an alternative for obtaining text without the need for
// autorelease behavior.
autoreleasepool(|| {
// XXX: We explicitly use `pasteboardItems` and not `stringForType` since the latter will concat
// multiple strings, if present, into one and return it instead of reading just the first which is `arboard`'s
// historical behavior.
let contents: Option<NonNull<NSArray<NSObject>>> =
unsafe { msg_send![self.pasteboard, pasteboardItems] };

let contents = contents.map(|c| unsafe { c.as_ref() }).ok_or_else(|| {
Error::Unknown { description: String::from("NSPasteboard#pasteboardItems errored") }
})?;

for item in contents.enumerator() {
let maybe_str: Option<NonNull<NSString>> =
unsafe { msg_send![item, stringForType:NSPasteboardTypeString] };

match maybe_str {
Some(string) => {
let string: Id<NSString, Owned> = unsafe { Id::from_ptr(string.as_ptr()) };
return Ok(string.as_str().to_owned());
}
None => continue,
}
}
};

string_array
.first_object()
.map(|obj| obj.as_str().to_owned())
.ok_or(Error::ContentNotAvailable)
Err(Error::ContentNotAvailable)
})
}

#[cfg(feature = "image-data")]
pub(crate) fn image(self) -> Result<ImageData<'static>, Error> {
use objc_foundation::NSData;
use std::io::Cursor;

let image_class: Id<NSObject> = object_class(&NSIMAGE_CLASS);
let classes = vec![image_class];
let classes: Id<NSArray<NSObject, Owned>> = NSArray::from_vec(classes);
let options: Id<NSDictionary<NSObject, NSObject>> = NSDictionary::new();

let contents: Id<NSArray<NSObject>> = unsafe {
let obj: *mut NSArray<NSObject> =
msg_send![self.pasteboard, readObjectsForClasses:&*classes options:&*options];
// XXX: There does not appear to be an alternative for obtaining images without the need for
// autorelease behavior.
let image = autoreleasepool(|| {
let obj: Option<NonNull<NSData>> =
unsafe { msg_send![self.pasteboard, dataForType: NSPasteboardTypeTIFF] };

if obj.is_null() {
return Err(Error::ContentNotAvailable);
let image_data: Id<NSData> = if let Some(obj) = obj {
unsafe { Id::from_ptr(obj.as_ptr()) }
} else {
Id::from_ptr(obj)
}
};
return Err(Error::ContentNotAvailable);
};

let obj = match contents.first_object() {
Some(obj) if obj.is_kind_of(&NSIMAGE_CLASS) => obj,
Some(_) | None => return Err(Error::ContentNotAvailable),
};
let data = unsafe {
let len: usize = msg_send![&*image_data, length];
let bytes: *const u8 = msg_send![&*image_data, bytes];

let tiff: &NSArray<NSObject> = unsafe { msg_send![obj, TIFFRepresentation] };
let data = unsafe {
let len: usize = msg_send![tiff, length];
let bytes: *const u8 = msg_send![tiff, bytes];
Cursor::new(std::slice::from_raw_parts(bytes, len))
};

Cursor::new(std::slice::from_raw_parts(bytes, len))
};
let reader = image::io::Reader::with_format(data, image::ImageFormat::Tiff);
match reader.decode() {
Ok(img) => {
let rgba = img.into_rgba8();
let (width, height) = rgba.dimensions();

Ok(ImageData {
width: width as usize,
height: height as usize,
bytes: rgba.into_raw().into(),
})
}
Err(_) => Err(Error::ConversionFailure),
}
let reader = image::io::Reader::with_format(data, image::ImageFormat::Tiff);
reader.decode().map_err(|_| Error::ConversionFailure)
})?;

let rgba = image.into_rgba8();
let (width, height) = rgba.dimensions();

Ok(ImageData {
width: width as usize,
height: height as usize,
bytes: rgba.into_raw().into(),
})
}
}

Expand Down Expand Up @@ -345,10 +347,3 @@ impl<'clipboard> Clear<'clipboard> {
Ok(())
}
}

/// Convenience function to get an Objective-C object from a
/// specific class.
fn object_class(class: &'static Class) -> Id<NSObject> {
// SAFETY: `Class` is a valid object and `Id` will not mutate it
unsafe { Id::from_ptr(class as *const Class as *mut NSObject) }
}