Skip to main content

plat/
cliprich.rs

1// cliprich -- bounded rich clipboard records and Windows CF_HTML framing.
2// New opt-in API by clipboard_platform; no assembly counterpart.
3// CF_HTML: learn.microsoft.com/windows/win32/dataxchg/html-clipboard-format
4
5use crate::{CLIP_DOCX, CLIP_HTML, CLIP_RTF, CLIP_TEXT};
6use alloc::vec::Vec;
7
8/// Maximum combined payload of an opt-in multi-format clipboard write (64 MiB).
9pub const CLIP_RICH_MAX: usize = 64 << 20;
10
11/// Validate a multi-format text clipboard before replacing the previous contents.
12/// Formats must be distinct TEXT/HTML/RTF/DOCX; text and HTML are NUL-free UTF-8.
13pub fn clip_many_valid(entries: &[(u32, &[u8])]) -> bool {
14    if entries.is_empty() || entries.len() > 4 {
15        return false;
16    }
17    let mut total = 0usize;
18    for (i, &(fmt, data)) in entries.iter().enumerate() {
19        if !matches!(fmt, CLIP_TEXT | CLIP_HTML | CLIP_RTF | CLIP_DOCX)
20            || entries[..i].iter().any(|e| e.0 == fmt)
21        {
22            return false;
23        }
24        let Some(n) = total.checked_add(data.len()) else {
25            return false;
26        };
27        total = n;
28        if total > CLIP_RICH_MAX {
29            return false;
30        }
31        if matches!(fmt, CLIP_TEXT | CLIP_HTML)
32            && (data.contains(&0) || core::str::from_utf8(data).is_err())
33        {
34            return false;
35        }
36    }
37    true
38}
39
40/// Wrap a UTF-8 HTML fragment in Windows CF_HTML, with byte offsets and final NUL.
41pub fn clip_html_encode(fragment: &[u8]) -> Option<Vec<u8>> {
42    if fragment.len() > CLIP_RICH_MAX
43        || fragment.contains(&0)
44        || core::str::from_utf8(fragment).is_err()
45    {
46        return None;
47    }
48    let prefix = b"<html><body><!--StartFragment-->";
49    let suffix = b"<!--EndFragment--></body></html>";
50    let header = b"Version:1.0\r\nStartHTML:0000000000\r\nEndHTML:0000000000\r\nStartFragment:0000000000\r\nEndFragment:0000000000\r\n";
51    let start = header.len();
52    let from = start + prefix.len();
53    let to = from + fragment.len();
54    let end = to + suffix.len();
55    let mut data = alloc::format!("Version:1.0\r\nStartHTML:{start:010}\r\nEndHTML:{end:010}\r\nStartFragment:{from:010}\r\nEndFragment:{to:010}\r\n").into_bytes();
56    data.extend_from_slice(prefix);
57    data.extend_from_slice(fragment);
58    data.extend_from_slice(suffix);
59    data.push(0);
60    Some(data)
61}
62
63/// Extract a UTF-8 fragment from CF_HTML. Reject invalid/overflowing offsets and UTF-8.
64pub fn clip_html_decode(data: &[u8]) -> Option<Vec<u8>> {
65    if data.len() > CLIP_RICH_MAX + 65536 {
66        return None;
67    }
68    let mut from = None;
69    let mut to = None;
70    let mut header_end = 0;
71    for line in data[..data.len().min(65536)].split_inclusive(|&b| b == b'\n' || b == b'\r') {
72        if line.starts_with(b"<") {
73            break;
74        }
75        header_end += line.len();
76        for (name, target) in [
77            (b"StartFragment:".as_slice(), &mut from),
78            (b"EndFragment:".as_slice(), &mut to),
79        ] {
80            if let Some(value) = line.strip_prefix(name) {
81                if target.is_some() {
82                    return None;
83                }
84                let text = core::str::from_utf8(value).ok()?.trim();
85                if text.is_empty() || !text.bytes().all(|b| b.is_ascii_digit()) {
86                    return None;
87                }
88                *target = Some(text.parse::<usize>().ok()?);
89            }
90        }
91        if from.is_some() && to.is_some() {
92            break;
93        }
94    }
95    let (from, to) = (from?, to?);
96    if from < header_end || from > to || to > data.len() {
97        return None;
98    }
99    let fragment = &data[from..to];
100    if fragment.len() > CLIP_RICH_MAX || fragment.contains(&0) || core::str::from_utf8(fragment).is_err() {
101        return None;
102    }
103    Some(fragment.to_vec())
104}