use std::os::fd::OwnedFd;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use a7n_core::clipboard::{FILES, Item, LocalChange};
use a7n_proto::limits::CLIPBOARD_FETCH_TIMEOUT;
#[cfg(test)]
use a7n_proto::limits::CLIPBOARD_TEXT_MAX;
use futures_util::future::join_all;
use tokio::sync::mpsc::Sender;
use tokio::sync::{OwnedSemaphorePermit, Semaphore, oneshot};
use tokio::time::timeout;
use super::served::{Serve, Served};
use super::{MAX_TRANSFERS, READ_TIMEOUT, WRITE_TIMEOUT, pipe};
use crate::clipboard::formats::{item_mime_of, render_files};
use crate::clipboard::uri_list;
use crate::clipboard::{FILE_PASTE_TIMEOUT, PasteRequest};
#[derive(Debug, Clone)]
pub(super) struct Limiter(Arc<Semaphore>);
impl Limiter {
pub(super) fn new() -> Self {
Self(Arc::new(Semaphore::new(MAX_TRANSFERS)))
}
pub(super) fn acquire(&self) -> Option<OwnedSemaphorePermit> {
Arc::clone(&self.0).try_acquire_owned().ok()
}
}
async fn read_items(reads: Vec<(&'static str, OwnedFd)>) -> Vec<Item> {
join_all(reads.into_iter().map(|(mime, fd)| async move {
let read = timeout(
READ_TIMEOUT,
pipe::read_limited(fd, a7n_core::clipboard::limit_for(mime)),
)
.await;
match read {
Ok(Ok(Some(data))) if !data.is_empty() => Some(Item {
mime: mime.to_owned(),
data,
}),
_ => None,
}
}))
.await
.into_iter()
.flatten()
.collect()
}
pub(super) async fn publish(
reads: Vec<(&'static str, OwnedFd)>,
sensitive: bool,
epoch: &AtomicU64,
stamp: u64,
changes: &Sender<LocalChange>,
) {
let (listed, items): (Vec<_>, Vec<_>) = read_items(reads)
.await
.into_iter()
.partition(|item| item.mime == FILES);
if epoch.load(Ordering::SeqCst) != stamp {
return;
}
let change = LocalChange {
files: listed
.into_iter()
.flat_map(|item| uri_list::decode(&item.data))
.collect(),
items,
sensitive,
source_app: None,
};
let _ = changes.send(change).await;
}
pub(super) async fn answer(
served: &Served,
requested: &str,
pastes: &Sender<PasteRequest>,
fd: OwnedFd,
) -> bool {
let fetched;
let rendered;
let data = match served.decide(requested) {
Serve::Inline(data) => data,
Serve::Lazy {
generation,
item_index,
} => {
let (reply, receiver) = oneshot::channel();
let request = PasteRequest {
generation,
item_index,
reply,
};
if pastes.try_send(request).is_err() {
return false;
}
let item_mime = item_mime_of(requested).unwrap_or("");
let wait = if item_mime == FILES {
FILE_PASTE_TIMEOUT
} else {
CLIPBOARD_FETCH_TIMEOUT
};
fetched = timeout(wait, receiver).await;
match &fetched {
Ok(Ok(Some(data))) if data.len() <= a7n_core::clipboard::limit_for(item_mime) => {
rendered = render_files(requested, data);
&rendered
}
_ => return false,
}
}
Serve::Refuse => return false,
};
matches!(
timeout(WRITE_TIMEOUT, pipe::write_all(fd, data)).await,
Ok(Ok(()))
)
}
#[cfg(test)]
mod tests {
use rustix::pipe::{PipeFlags, pipe_with};
use super::*;
#[tokio::test]
async fn read_items_interdependent_writers_reads_concurrently_in_input_order() {
let (first, first_writer) = pipe_with(PipeFlags::CLOEXEC).unwrap();
let (second, second_writer) = pipe_with(PipeFlags::CLOEXEC).unwrap();
let data = vec![7; CLIPBOARD_TEXT_MAX];
let write = async {
pipe::write_all(second_writer, &data).await.unwrap();
pipe::write_all(first_writer, b"first").await.unwrap();
};
let (items, ()) = tokio::join!(
read_items(vec![("text/plain", first), ("text/html", second)]),
write,
);
assert_eq!(
items,
vec![
Item {
mime: "text/plain".to_owned(),
data: b"first".to_vec()
},
Item {
mime: "text/html".to_owned(),
data
},
]
);
}
#[tokio::test]
async fn publish_selection_changes_during_read_discards_stale_items() {
let (reader, writer) = pipe_with(PipeFlags::CLOEXEC).unwrap();
let epoch = AtomicU64::new(1);
let (changes, mut receiver) = tokio::sync::mpsc::channel(1);
let write = async {
epoch.store(2, Ordering::SeqCst);
pipe::write_all(writer, b"stale").await.unwrap();
};
tokio::join!(
publish(vec![("text/plain", reader)], false, &epoch, 1, &changes),
write,
);
assert!(matches!(
receiver.try_recv(),
Err(tokio::sync::mpsc::error::TryRecvError::Empty)
));
}
}