Skip to main content

glass/browser/session/
clipboard.rs

1//! System clipboard read/write via CDP.
2//!
3//! Provides access to the system clipboard through browser permissions
4//! and JavaScript evaluation. All operations are bounded to 8 KiB.
5
6use super::*;
7
8/// Maximum bytes read or written in a single clipboard operation.
9const CLIPBOARD_MAX_BYTES: usize = 8192;
10
11impl BrowserSession {
12    /// Read text from the system clipboard.
13    ///
14    /// Grants the `clipboardReadWrite` permission temporarily, then
15    /// evaluates `navigator.clipboard.readText()`. Returns up to
16    /// 8 KiB of text.
17    pub async fn clipboard_read(&self) -> BrowserResult<String> {
18        self.cdp.with_current_route(async {
19            let _ = self.cdp.send("Browser.grantPermissions", Some(serde_json::json!({
20                "permissions": ["clipboardReadWrite"], "origin": "*"
21            }))).await;
22            let expr = format!(
23                "navigator.clipboard.readText().then(t => t.slice(0,{CLIPBOARD_MAX_BYTES})).catch(() => '')"
24            );
25            let raw = self.cdp.evaluate(&expr).await?;
26            let value = runtime_value(&raw)?;
27            Ok(value.as_str().unwrap_or("").to_string())
28        }).await
29    }
30
31    /// Write text to the system clipboard.
32    ///
33    /// Grants the `clipboardReadWrite` permission temporarily, then
34    /// writes via `navigator.clipboard.writeText()`. Text is truncated
35    /// to 8 KiB before writing.
36    pub async fn clipboard_write(&self, text: &str) -> BrowserResult<()> {
37        let bounded = if text.len() > CLIPBOARD_MAX_BYTES {
38            let mut end = CLIPBOARD_MAX_BYTES;
39            while end > 0 && !text.is_char_boundary(end) {
40                end -= 1;
41            }
42            &text[..end]
43        } else {
44            text
45        };
46        self.cdp
47            .with_current_route(async {
48                let _ = self
49                    .cdp
50                    .send(
51                        "Browser.grantPermissions",
52                        Some(serde_json::json!({
53                            "permissions": ["clipboardReadWrite"], "origin": "*"
54                        })),
55                    )
56                    .await;
57                let escaped = bounded
58                    .replace('\\', "\\\\")
59                    .replace('\'', "\\'")
60                    .replace('\n', "\\n")
61                    .replace('\r', "\\r")
62                    .replace('\u{2028}', "\\u2028")
63                    .replace('\u{2029}', "\\u2029");
64                let expr = format!(
65                    "navigator.clipboard.writeText('{escaped}').then(() => true).catch(() => false)"
66                );
67                self.cdp.evaluate(&expr).await?;
68                Ok(())
69            })
70            .await
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn clipboard_max_bytes_is_8k() {
80        // 8 KiB = 8192 bytes — the safety bound for read/write operations
81        assert_eq!(CLIPBOARD_MAX_BYTES, 8192);
82    }
83
84    #[test]
85    fn clipboard_max_bytes_is_power_of_two() {
86        // Power-of-two sizing aligns with typical memory page/buffer expectations
87        assert_eq!(CLIPBOARD_MAX_BYTES, 1 << 13);
88    }
89
90    #[test]
91    fn clipboard_max_bytes_is_reasonable_upper_bound() {
92        // Guard against accidental inflation; must stay <= 16384 (16 KiB)
93        const { assert!(CLIPBOARD_MAX_BYTES <= 16384) };
94        // Must be at least 4096 (4 KiB) to be useful
95        const { assert!(CLIPBOARD_MAX_BYTES >= 4096) };
96    }
97}