1use std::{
2	borrow::Cow,
3	os::unix::ffi::OsStrExt,
4	path::{Path, PathBuf},
5	time::Instant,
6};
7
8#[cfg(feature = "wayland-data-control")]
9use log::{trace, warn};
10use percent_encoding::{percent_decode, percent_encode, AsciiSet, CONTROLS};
11
12#[cfg(feature = "image-data")]
13use crate::ImageData;
14use crate::{common::private, Error};
15
16const KDE_EXCLUSION_MIME: &str = "x-kde-passwordManagerHint";
18const KDE_EXCLUSION_HINT: &[u8] = b"secret";
19
20mod x11;
21
22#[cfg(feature = "wayland-data-control")]
23mod wayland;
24
25fn into_unknown<E: std::fmt::Display>(error: E) -> Error {
26	Error::Unknown { description: error.to_string() }
27}
28
29#[cfg(feature = "image-data")]
30fn encode_as_png(image: &ImageData) -> Result<Vec<u8>, Error> {
31	use image::ImageEncoder as _;
32
33	if image.bytes.is_empty() || image.width == 0 || image.height == 0 {
34		return Err(Error::ConversionFailure);
35	}
36
37	let mut png_bytes = Vec::new();
38	let encoder = image::codecs::png::PngEncoder::new(&mut png_bytes);
39	encoder
40		.write_image(
41			image.bytes.as_ref(),
42			image.width as u32,
43			image.height as u32,
44			image::ExtendedColorType::Rgba8,
45		)
46		.map_err(|_| Error::ConversionFailure)?;
47
48	Ok(png_bytes)
49}
50
51fn paths_from_uri_list(uri_list: Vec<u8>) -> Vec<PathBuf> {
52	uri_list
53		.split(|char| *char == b'\n')
54		.filter_map(|line| line.strip_prefix(b"file://"))
55		.filter_map(|s| percent_decode(s).decode_utf8().ok())
56		.map(|decoded| PathBuf::from(decoded.as_ref()))
57		.collect()
58}
59
60fn paths_to_uri_list(file_list: &[impl AsRef<Path>]) -> Result<String, Error> {
61	const ASCII_SET: &AsciiSet = &CONTROLS
63		.add(b'#')
64		.add(b';')
65		.add(b'?')
66		.add(b'[')
67		.add(b']')
68		.add(b' ')
69		.add(b'\"')
70		.add(b'%')
71		.add(b'<')
72		.add(b'>')
73		.add(b'\\')
74		.add(b'^')
75		.add(b'`')
76		.add(b'{')
77		.add(b'|')
78		.add(b'}');
79
80	file_list
81		.iter()
82		.filter_map(|path| {
83			path.as_ref().canonicalize().ok().map(|path| {
84				format!("file://{}", percent_encode(path.as_os_str().as_bytes(), ASCII_SET))
85			})
86		})
87		.reduce(|uri_list, uri| uri_list + "\n" + &uri)
88		.ok_or(Error::ConversionFailure)
89}
90
91#[derive(Copy, Clone, Debug)]
101pub enum LinuxClipboardKind {
102	Clipboard,
105
106	Primary,
112
113	Secondary,
118}
119
120pub(crate) enum Clipboard {
121	X11(x11::Clipboard),
122
123	#[cfg(feature = "wayland-data-control")]
124	WlDataControl(wayland::Clipboard),
125}
126
127impl Clipboard {
128	pub(crate) fn new() -> Result<Self, Error> {
129		#[cfg(feature = "wayland-data-control")]
130		{
131			if std::env::var_os("WAYLAND_DISPLAY").is_some() {
132				match wayland::Clipboard::new() {
134					Ok(clipboard) => {
135						trace!("Successfully initialized the Wayland data control clipboard.");
136						return Ok(Self::WlDataControl(clipboard));
137					}
138					Err(e) => warn!(
139						"Tried to initialize the wayland data control protocol clipboard, but failed. Falling back to the X11 clipboard protocol. The error was: {}",
140						e
141					),
142				}
143			}
144		}
145		Ok(Self::X11(x11::Clipboard::new()?))
146	}
147}
148
149pub(crate) struct Get<'clipboard> {
150	clipboard: &'clipboard mut Clipboard,
151	selection: LinuxClipboardKind,
152}
153
154impl<'clipboard> Get<'clipboard> {
155	pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self {
156		Self { clipboard, selection: LinuxClipboardKind::Clipboard }
157	}
158
159	pub(crate) fn text(self) -> Result<String, Error> {
160		match self.clipboard {
161			Clipboard::X11(clipboard) => clipboard.get_text(self.selection),
162			#[cfg(feature = "wayland-data-control")]
163			Clipboard::WlDataControl(clipboard) => clipboard.get_text(self.selection),
164		}
165	}
166
167	#[cfg(feature = "image-data")]
168	pub(crate) fn image(self) -> Result<ImageData<'static>, Error> {
169		match self.clipboard {
170			Clipboard::X11(clipboard) => clipboard.get_image(self.selection),
171			#[cfg(feature = "wayland-data-control")]
172			Clipboard::WlDataControl(clipboard) => clipboard.get_image(self.selection),
173		}
174	}
175
176	pub(crate) fn html(self) -> Result<String, Error> {
177		match self.clipboard {
178			Clipboard::X11(clipboard) => clipboard.get_html(self.selection),
179			#[cfg(feature = "wayland-data-control")]
180			Clipboard::WlDataControl(clipboard) => clipboard.get_html(self.selection),
181		}
182	}
183
184	pub(crate) fn file_list(self) -> Result<Vec<PathBuf>, Error> {
185		match self.clipboard {
186			Clipboard::X11(clipboard) => clipboard.get_file_list(self.selection),
187			#[cfg(feature = "wayland-data-control")]
188			Clipboard::WlDataControl(clipboard) => clipboard.get_file_list(self.selection),
189		}
190	}
191}
192
193pub trait GetExtLinux: private::Sealed {
195	fn clipboard(self, selection: LinuxClipboardKind) -> Self;
200}
201
202impl GetExtLinux for crate::Get<'_> {
203	fn clipboard(mut self, selection: LinuxClipboardKind) -> Self {
204		self.platform.selection = selection;
205		self
206	}
207}
208
209#[derive(Default)]
211pub(crate) enum WaitConfig {
212	Until(Instant),
214
215	Forever,
217
218	#[default]
220	None,
221}
222
223pub(crate) struct Set<'clipboard> {
224	clipboard: &'clipboard mut Clipboard,
225	wait: WaitConfig,
226	selection: LinuxClipboardKind,
227	exclude_from_history: bool,
228}
229
230impl<'clipboard> Set<'clipboard> {
231	pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self {
232		Self {
233			clipboard,
234			wait: WaitConfig::default(),
235			selection: LinuxClipboardKind::Clipboard,
236			exclude_from_history: false,
237		}
238	}
239
240	pub(crate) fn text(self, text: Cow<'_, str>) -> Result<(), Error> {
241		match self.clipboard {
242			Clipboard::X11(clipboard) => {
243				clipboard.set_text(text, self.selection, self.wait, self.exclude_from_history)
244			}
245
246			#[cfg(feature = "wayland-data-control")]
247			Clipboard::WlDataControl(clipboard) => {
248				clipboard.set_text(text, self.selection, self.wait, self.exclude_from_history)
249			}
250		}
251	}
252
253	pub(crate) fn html(self, html: Cow<'_, str>, alt: Option<Cow<'_, str>>) -> Result<(), Error> {
254		match self.clipboard {
255			Clipboard::X11(clipboard) => {
256				clipboard.set_html(html, alt, self.selection, self.wait, self.exclude_from_history)
257			}
258
259			#[cfg(feature = "wayland-data-control")]
260			Clipboard::WlDataControl(clipboard) => {
261				clipboard.set_html(html, alt, self.selection, self.wait, self.exclude_from_history)
262			}
263		}
264	}
265
266	#[cfg(feature = "image-data")]
267	pub(crate) fn image(self, image: ImageData<'_>) -> Result<(), Error> {
268		match self.clipboard {
269			Clipboard::X11(clipboard) => {
270				clipboard.set_image(image, self.selection, self.wait, self.exclude_from_history)
271			}
272
273			#[cfg(feature = "wayland-data-control")]
274			Clipboard::WlDataControl(clipboard) => {
275				clipboard.set_image(image, self.selection, self.wait, self.exclude_from_history)
276			}
277		}
278	}
279
280	pub(crate) fn file_list(self, file_list: &[impl AsRef<Path>]) -> Result<(), Error> {
281		match self.clipboard {
282			Clipboard::X11(clipboard) => clipboard.set_file_list(
283				file_list,
284				self.selection,
285				self.wait,
286				self.exclude_from_history,
287			),
288
289			#[cfg(feature = "wayland-data-control")]
290			Clipboard::WlDataControl(clipboard) => clipboard.set_file_list(
291				file_list,
292				self.selection,
293				self.wait,
294				self.exclude_from_history,
295			),
296		}
297	}
298}
299
300pub trait SetExtLinux: private::Sealed {
302	fn wait(self) -> Self;
328
329	fn wait_until(self, deadline: Instant) -> Self;
338
339	fn clipboard(self, selection: LinuxClipboardKind) -> Self;
360
361	fn exclude_from_history(self) -> Self;
367}
368
369impl SetExtLinux for crate::Set<'_> {
370	fn wait(mut self) -> Self {
371		self.platform.wait = WaitConfig::Forever;
372		self
373	}
374
375	fn clipboard(mut self, selection: LinuxClipboardKind) -> Self {
376		self.platform.selection = selection;
377		self
378	}
379
380	fn wait_until(mut self, deadline: Instant) -> Self {
381		self.platform.wait = WaitConfig::Until(deadline);
382		self
383	}
384
385	fn exclude_from_history(mut self) -> Self {
386		self.platform.exclude_from_history = true;
387		self
388	}
389}
390
391pub(crate) struct Clear<'clipboard> {
392	clipboard: &'clipboard mut Clipboard,
393}
394
395impl<'clipboard> Clear<'clipboard> {
396	pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self {
397		Self { clipboard }
398	}
399
400	pub(crate) fn clear(self) -> Result<(), Error> {
401		self.clear_inner(LinuxClipboardKind::Clipboard)
402	}
403
404	fn clear_inner(self, selection: LinuxClipboardKind) -> Result<(), Error> {
405		match self.clipboard {
406			Clipboard::X11(clipboard) => clipboard.clear(selection),
407			#[cfg(feature = "wayland-data-control")]
408			Clipboard::WlDataControl(clipboard) => clipboard.clear(selection),
409		}
410	}
411}
412
413pub trait ClearExtLinux: private::Sealed {
415	fn clipboard(self, selection: LinuxClipboardKind) -> Result<(), Error>;
434}
435
436impl ClearExtLinux for crate::Clear<'_> {
437	fn clipboard(self, selection: LinuxClipboardKind) -> Result<(), Error> {
438		self.platform.clear_inner(selection)
439	}
440}
441
442#[cfg(test)]
443mod tests {
444	use super::*;
445
446	#[test]
447	fn test_decoding_uri_list() {
448		let file_list = [
451			"file:///tmp/bar.log",
452			"file:///tmp/test%5C.txt",
453			"file:///tmp/foo%3F.png",
454			"file:///tmp/white%20space.txt",
455		];
456
457		let paths = vec![
458			PathBuf::from("/tmp/bar.log"),
459			PathBuf::from("/tmp/test\\.txt"),
460			PathBuf::from("/tmp/foo?.png"),
461			PathBuf::from("/tmp/white space.txt"),
462		];
463		assert_eq!(paths_from_uri_list(file_list.join("\n").into()), paths);
464	}
465}