use std::io::{self, Write};
use crate::{ClipboardError, MimeType, Selection};
use super::Backend;
use crate::osc52::{OSC52_MAX, is_in_tmux, write_osc52};
#[inline]
fn base64_encoded_len(n: usize) -> usize {
n.div_ceil(3) * 4
}
pub struct Osc52Backend;
impl Osc52Backend {
#[cfg_attr(any(target_os = "macos", target_os = "windows"), allow(dead_code))]
pub(crate) fn new() -> Self {
Self
}
pub(crate) fn set_inner(
&self,
sel: Selection,
mime: MimeType,
bytes: &[u8],
out: &mut impl Write,
) -> Result<(), ClipboardError> {
if sel != Selection::Clipboard {
return Err(ClipboardError::UnsupportedMime);
}
match mime {
MimeType::Text => {}
_ => return Err(ClipboardError::UnsupportedMime),
}
let text = std::str::from_utf8(bytes).map_err(|_| ClipboardError::UnsupportedMime)?;
if base64_encoded_len(text.len()) > OSC52_MAX {
return Err(ClipboardError::PayloadTooLarge);
}
write_osc52(out, text, is_in_tmux()).map_err(ClipboardError::io)
}
pub(crate) fn clear_inner(
&self,
sel: Selection,
out: &mut impl Write,
) -> Result<(), ClipboardError> {
if sel != Selection::Clipboard {
return Err(ClipboardError::UnsupportedMime);
}
write_osc52(out, "", is_in_tmux()).map_err(ClipboardError::io)
}
}
impl Backend for Osc52Backend {
fn kind(&self) -> crate::BackendKind {
crate::BackendKind::Osc52
}
fn capabilities(&self) -> crate::Capabilities {
crate::Capabilities::WRITE | crate::Capabilities::CLEAR
}
fn set(&self, sel: Selection, mime: MimeType, bytes: &[u8]) -> Result<(), ClipboardError> {
self.set_inner(sel, mime, bytes, &mut io::stdout().lock())
}
fn get(&self, _sel: Selection, _mime: MimeType) -> Result<Vec<u8>, ClipboardError> {
Err(ClipboardError::UnsupportedMime)
}
fn clear(&self, sel: Selection) -> Result<(), ClipboardError> {
self.clear_inner(sel, &mut io::stdout().lock())
}
fn available(&self, _sel: Selection) -> Result<Vec<MimeType>, ClipboardError> {
Ok(vec![])
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::base64::base64_encode;
fn backend() -> Osc52Backend {
Osc52Backend::new()
}
#[test]
fn base64_encoded_len_matches_encoder() {
for n in [0usize, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] {
let bytes = vec![b'x'; n];
assert_eq!(
base64_encoded_len(n),
base64_encode(&bytes).len(),
"mismatch at n = {n}"
);
}
let max_raw = OSC52_MAX / 4 * 3; for &n in &[
max_raw.saturating_sub(2),
max_raw.saturating_sub(1),
max_raw,
max_raw + 1,
max_raw + 2,
] {
let bytes = vec![b'x'; n];
assert_eq!(
base64_encoded_len(n),
base64_encode(&bytes).len(),
"mismatch at n = {n}"
);
}
}
#[test]
fn set_text_clipboard_ok() {
let b = backend();
let mut buf = Vec::new();
let result = b.set_inner(Selection::Clipboard, MimeType::Text, b"hello", &mut buf);
assert!(result.is_ok());
assert!(!buf.is_empty(), "expected bytes written to sink");
}
#[test]
fn set_html_unsupported() {
let b = backend();
let mut buf = Vec::new();
let err = b
.set_inner(Selection::Clipboard, MimeType::Html, b"<b>hi</b>", &mut buf)
.unwrap_err();
assert!(matches!(err, ClipboardError::UnsupportedMime));
}
#[test]
fn set_rtf_unsupported() {
let b = backend();
let mut buf = Vec::new();
let err = b
.set_inner(Selection::Clipboard, MimeType::Rtf, b"{\\rtf1}", &mut buf)
.unwrap_err();
assert!(matches!(err, ClipboardError::UnsupportedMime));
}
#[test]
fn set_png_unsupported() {
let b = backend();
let mut buf = Vec::new();
let err = b
.set_inner(Selection::Clipboard, MimeType::Png, b"\x89PNG", &mut buf)
.unwrap_err();
assert!(matches!(err, ClipboardError::UnsupportedMime));
}
#[test]
fn set_uri_list_unsupported() {
let b = backend();
let mut buf = Vec::new();
let err = b
.set_inner(
Selection::Clipboard,
MimeType::UriList,
b"file:///tmp/x",
&mut buf,
)
.unwrap_err();
assert!(matches!(err, ClipboardError::UnsupportedMime));
}
#[test]
fn set_custom_unsupported() {
let b = backend();
let mut buf = Vec::new();
let err = b
.set_inner(
Selection::Clipboard,
MimeType::Custom("application/json".into()),
b"{}",
&mut buf,
)
.unwrap_err();
assert!(matches!(err, ClipboardError::UnsupportedMime));
}
#[test]
fn set_non_utf8_unsupported() {
let b = backend();
let mut buf = Vec::new();
let invalid_utf8 = b"\xff\xfe";
let err = b
.set_inner(Selection::Clipboard, MimeType::Text, invalid_utf8, &mut buf)
.unwrap_err();
assert!(matches!(err, ClipboardError::UnsupportedMime));
}
#[test]
fn set_primary_unsupported() {
let b = backend();
let mut buf = Vec::new();
let err = b
.set_inner(Selection::Primary, MimeType::Text, b"hi", &mut buf)
.unwrap_err();
assert!(matches!(err, ClipboardError::UnsupportedMime));
}
#[test]
fn get_clipboard_text_unsupported() {
let b = backend();
let err = b.get(Selection::Clipboard, MimeType::Text).unwrap_err();
assert!(matches!(err, ClipboardError::UnsupportedMime));
}
#[test]
fn get_primary_unsupported() {
let b = backend();
let err = b.get(Selection::Primary, MimeType::Html).unwrap_err();
assert!(matches!(err, ClipboardError::UnsupportedMime));
}
#[test]
fn clear_clipboard_ok() {
let b = backend();
let mut buf = Vec::new();
let result = b.clear_inner(Selection::Clipboard, &mut buf);
assert!(result.is_ok());
assert!(!buf.is_empty());
}
#[test]
fn clear_primary_unsupported() {
let b = backend();
let mut buf = Vec::new();
let err = b.clear_inner(Selection::Primary, &mut buf).unwrap_err();
assert!(matches!(err, ClipboardError::UnsupportedMime));
}
#[test]
fn set_text_over_cap_returns_payload_too_large() {
let big = "x".repeat(55_501);
let b = backend();
let mut buf = Vec::new();
let err = b
.set_inner(
Selection::Clipboard,
MimeType::Text,
big.as_bytes(),
&mut buf,
)
.unwrap_err();
assert!(
matches!(err, ClipboardError::PayloadTooLarge),
"expected PayloadTooLarge, got: {err:?}"
);
assert!(
buf.is_empty(),
"nothing should be written for oversized payload"
);
}
#[test]
fn available_returns_empty() {
let b = backend();
let mimes = b.available(Selection::Clipboard).unwrap();
assert!(mimes.is_empty());
}
#[test]
fn available_primary_returns_empty() {
let b = backend();
let mimes = b.available(Selection::Primary).unwrap();
assert!(mimes.is_empty());
}
}