use std::borrow::Cow;
use crate::{
IdeviceError, ReadWrite, RemoteXpcClient, RsdService, obf, xpc,
xpc::{Dictionary, XPCObject},
};
use super::CoreDeviceError;
pub const GENERAL_PASTEBOARD: &str = "general";
const UTI_UTF8_PLAIN_TEXT: &str = "public.utf8-plain-text";
const UTI_PLAIN_TEXT: &str = "public.plain-text";
const UTI_TEXT: &str = "public.text";
const TEXT_UTIS: [&str; 3] = [UTI_UTF8_PLAIN_TEXT, UTI_PLAIN_TEXT, UTI_TEXT];
pub const UTI_PNG: &str = "public.png";
pub const UTI_JPEG: &str = "public.jpeg";
pub const UTI_TIFF: &str = "public.tiff";
const IMAGE_UTIS: [&str; 3] = [UTI_PNG, UTI_JPEG, UTI_TIFF];
#[derive(Debug, Clone, Copy)]
pub enum DataInclusionPolicy {
AllResolved,
AllPromised,
MatchSource,
PromiseSecondary,
Threshold(i64),
}
impl DataInclusionPolicy {
fn to_xpc(self) -> XPCObject {
let (key, value) = match self {
DataInclusionPolicy::AllResolved => ("allResolved", Dictionary::new()),
DataInclusionPolicy::AllPromised => ("allPromised", Dictionary::new()),
DataInclusionPolicy::MatchSource => ("matchSource", Dictionary::new()),
DataInclusionPolicy::PromiseSecondary => ("promiseSecondary", Dictionary::new()),
DataInclusionPolicy::Threshold(bytes) => {
let mut inner = Dictionary::new();
inner.insert("bytes".into(), XPCObject::Int64(bytes));
("thresholdData", inner)
}
};
let mut d = Dictionary::new();
d.insert(key.into(), XPCObject::Dictionary(value));
XPCObject::Dictionary(d)
}
}
#[derive(Debug, Clone)]
pub enum PasteboardPayload {
Inline(Vec<u8>),
Promised { size: Option<i64> },
Error(String),
}
#[derive(Debug, Clone)]
pub struct PasteboardEntry {
pub uti: String,
pub payload: PasteboardPayload,
}
#[derive(Debug, Clone)]
pub struct PasteboardItem {
pub index: usize,
pub types: Vec<String>,
pub data: Vec<PasteboardEntry>,
}
#[derive(Debug, Clone)]
pub struct PasteboardSnapshot {
pub pasteboard_name: Option<String>,
pub change_count: Option<i64>,
pub items: Vec<PasteboardItem>,
}
#[derive(Debug, Clone)]
pub struct PromisedItem {
pub item_index: i64,
pub uti: String,
pub size: Option<i64>,
}
impl PasteboardSnapshot {
fn from_message(msg: &plist::Value) -> Result<Self, CoreDeviceError> {
let dict = msg
.as_dictionary()
.ok_or(CoreDeviceError::MalformedField("(root)"))?;
let snapshot = dict
.get("pasteboard")
.and_then(|p| p.as_dictionary())
.unwrap_or(dict);
let metadata = snapshot.get("metadata").and_then(|m| m.as_dictionary());
let pasteboard_name = metadata
.and_then(|m| m.get("pasteboardName"))
.and_then(|n| n.as_string())
.map(str::to_string);
let change_count = metadata
.and_then(|m| m.get("changeCount"))
.and_then(|c| c.as_signed_integer());
let items = snapshot
.get("items")
.and_then(|i| i.as_array())
.map(|arr| {
arr.iter()
.enumerate()
.map(|(index, item)| parse_item(index, item))
.collect()
})
.unwrap_or_default();
Ok(PasteboardSnapshot {
pasteboard_name,
change_count,
items,
})
}
pub fn text(&self) -> Option<String> {
for item in &self.items {
for uti in TEXT_UTIS {
if let Some(entry) = item.data.iter().find(|e| e.uti == uti)
&& let PasteboardPayload::Inline(bytes) = &entry.payload
&& let Ok(text) = String::from_utf8(bytes.clone())
{
return Some(text);
}
}
}
None
}
pub fn image(&self) -> Option<(&'static str, Vec<u8>)> {
for item in &self.items {
for uti in IMAGE_UTIS {
if let Some(entry) = item.data.iter().find(|e| e.uti == uti)
&& let PasteboardPayload::Inline(bytes) = &entry.payload
{
return Some((uti, bytes.clone()));
}
}
}
None
}
pub fn promised_items(&self) -> Vec<PromisedItem> {
let mut out = Vec::new();
for item in &self.items {
for entry in &item.data {
if let PasteboardPayload::Promised { size } = entry.payload {
out.push(PromisedItem {
item_index: item.index as i64,
uti: entry.uti.clone(),
size,
});
}
}
}
out
}
}
fn parse_item(index: usize, item: &plist::Value) -> PasteboardItem {
let dict = item.as_dictionary();
let types = dict
.and_then(|d| d.get("types"))
.and_then(|t| t.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_string().map(str::to_string))
.collect()
})
.unwrap_or_default();
let data = dict
.and_then(|d| d.get("data"))
.and_then(|d| d.as_dictionary())
.map(|map| {
map.iter()
.map(|(uti, datum)| PasteboardEntry {
uti: uti.clone(),
payload: parse_payload(datum),
})
.collect()
})
.unwrap_or_default();
PasteboardItem { index, types, data }
}
fn parse_payload(datum: &plist::Value) -> PasteboardPayload {
let Some(d) = datum.as_dictionary() else {
return PasteboardPayload::Promised { size: None };
};
if let Some(bytes) = d.get("data").and_then(|v| v.as_data()) {
PasteboardPayload::Inline(bytes.to_vec())
} else if let Some(err) = d.get("error") {
PasteboardPayload::Error(format!("{err:?}"))
} else {
PasteboardPayload::Promised {
size: d.get("size").and_then(|s| s.as_signed_integer()),
}
}
}
#[derive(Debug)]
pub struct PasteboardServiceClient<R: ReadWrite> {
inner: RemoteXpcClient<R>,
}
impl RsdService for PasteboardServiceClient<Box<dyn ReadWrite>> {
fn rsd_service_name() -> Cow<'static, str> {
obf!("com.apple.coredevice.pasteboardservice")
}
async fn from_stream(stream: Box<dyn ReadWrite>) -> Result<Self, IdeviceError> {
let mut inner = RemoteXpcClient::new(stream).await?;
inner.do_handshake().await?;
Ok(Self { inner })
}
}
impl<R: ReadWrite> PasteboardServiceClient<R> {
pub fn new(inner: RemoteXpcClient<R>) -> Self {
Self { inner }
}
pub async fn get(&mut self, pasteboard_name: &str) -> Result<PasteboardSnapshot, IdeviceError> {
self.get_with_policy(pasteboard_name, DataInclusionPolicy::AllResolved)
.await
}
pub async fn get_with_policy(
&mut self,
pasteboard_name: &str,
policy: DataInclusionPolicy,
) -> Result<PasteboardSnapshot, IdeviceError> {
let msg = xpc!({
"command": "PULL",
"pasteboardName": pasteboard_name,
"dataPolicy": policy.to_xpc(),
});
self.inner.send_object(msg, true).await?;
let reply = self.inner.recv().await?;
Ok(PasteboardSnapshot::from_message(&reply)?)
}
pub async fn set_text(
&mut self,
text: &str,
pasteboard_name: &str,
) -> Result<(), IdeviceError> {
let mut data = Dictionary::new();
for uti in TEXT_UTIS {
data.insert(uti.into(), xpc!({ "data": text.as_bytes() }));
}
let msg = xpc!({
"command": "SET",
"pasteboardName": pasteboard_name,
"items": [{
"types": TEXT_UTIS.to_vec(),
"data": XPCObject::Dictionary(data),
}],
"sourceMetadata": XPCObject::Null,
});
self.inner.send_object(msg, true).await?;
self.inner.recv().await?;
Ok(())
}
pub async fn set_image(
&mut self,
bytes: &[u8],
uti: &str,
pasteboard_name: &str,
) -> Result<(), IdeviceError> {
let mut data = Dictionary::new();
data.insert(uti.into(), xpc!({ "data": bytes }));
let msg = xpc!({
"command": "SET",
"pasteboardName": pasteboard_name,
"items": [{
"types": vec![uti],
"data": XPCObject::Dictionary(data),
}],
"sourceMetadata": XPCObject::Null,
});
self.inner.send_object(msg, true).await?;
self.inner.recv().await?;
Ok(())
}
pub async fn resolve(
&mut self,
pasteboard_name: &str,
item_index: i64,
uti: &str,
) -> Result<Option<Vec<u8>>, IdeviceError> {
let reply = self.resolve_raw(pasteboard_name, item_index, uti).await?;
Ok(reply
.as_dictionary()
.and_then(|d| d.get("data"))
.and_then(|d| d.as_data())
.map(|d| d.to_vec()))
}
pub async fn resolve_raw(
&mut self,
pasteboard_name: &str,
item_index: i64,
uti: &str,
) -> Result<plist::Value, IdeviceError> {
let msg = xpc!({
"command": "RESOLVE",
"pasteboardName": pasteboard_name,
"itemIndex": item_index,
"type": uti,
});
self.inner.send_object(msg, true).await?;
self.inner.recv().await
}
pub async fn set_change_notifications(
&mut self,
enable: bool,
pasteboard_name: &str,
policy: Option<DataInclusionPolicy>,
) -> Result<(), IdeviceError> {
let data_policy = policy.map(|p| p.to_xpc()).unwrap_or(XPCObject::Null);
let msg = xpc!({
"command": "AUTONOTIFY",
"enable": enable,
"pasteboardName": pasteboard_name,
"dataPolicy": data_policy,
});
self.inner.send_object(msg, false).await
}
pub async fn recv_push(&mut self) -> Result<PasteboardSnapshot, IdeviceError> {
let push = self.inner.recv_root().await?;
Ok(PasteboardSnapshot::from_message(&push)?)
}
}