#![allow(dead_code)]
use crate::ClipboardError;
const HDR_PREFIX: &str = "Version:0.9\r\n\
StartHTML:0000000000\r\n\
EndHTML:0000000000\r\n\
StartFragment:0000000000\r\n\
EndFragment:0000000000\r\n\
SourceURL:about:blank\r\n";
const BODY_OPEN: &str = "<html>\r\n<body>\r\n<!--StartFragment-->";
const BODY_CLOSE: &str = "<!--EndFragment-->\r\n</body>\r\n</html>";
pub fn wrap(html: &str) -> Vec<u8> {
let hdr_len = HDR_PREFIX.len();
let start_html: usize = hdr_len;
let start_fragment: usize = hdr_len + BODY_OPEN.len();
let end_fragment: usize = start_fragment + html.len();
let end_html: usize = end_fragment + BODY_CLOSE.len();
let header = format!(
"Version:0.9\r\n\
StartHTML:{start_html:010}\r\n\
EndHTML:{end_html:010}\r\n\
StartFragment:{start_fragment:010}\r\n\
EndFragment:{end_fragment:010}\r\n\
SourceURL:about:blank\r\n"
);
debug_assert_eq!(
header.len(),
hdr_len,
"cf_html: header length changed — update HDR_PREFIX"
);
let mut out =
String::with_capacity(header.len() + BODY_OPEN.len() + html.len() + BODY_CLOSE.len());
out.push_str(&header);
out.push_str(BODY_OPEN);
out.push_str(html);
out.push_str(BODY_CLOSE);
out.into_bytes()
}
pub fn unwrap(payload: &[u8]) -> Result<String, ClipboardError> {
let bad = || ClipboardError::io_other("malformed CF_HTML header");
let header_text = payload
.iter()
.position(|&b| b == b'<')
.map_or(payload, |pos| &payload[..pos]);
let header_str = std::str::from_utf8(header_text).map_err(|_| bad())?;
let mut start_fragment: Option<usize> = None;
let mut end_fragment: Option<usize> = None;
for line in header_str.lines() {
let line = line.trim_end_matches('\r');
if let Some(val) = line.strip_prefix("StartFragment:") {
start_fragment = Some(parse_offset(val).ok_or_else(bad)?);
} else if let Some(val) = line.strip_prefix("EndFragment:") {
end_fragment = Some(parse_offset(val).ok_or_else(bad)?);
}
}
let start = start_fragment.ok_or_else(bad)?;
let end = end_fragment.ok_or_else(bad)?;
if start > end {
return Err(bad());
}
if end > payload.len() {
return Err(bad());
}
let fragment_bytes = &payload[start..end];
let fragment = std::str::from_utf8(fragment_bytes).map_err(|_| bad())?;
Ok(fragment.to_owned())
}
fn parse_offset(s: &str) -> Option<usize> {
if s.is_empty() || !s.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
s.parse::<usize>().ok()
}
#[cfg(test)]
mod tests {
use super::*;
fn round_trip(html: &str) {
let payload = wrap(html);
let recovered = unwrap(&payload).expect("unwrap failed");
assert_eq!(recovered, html, "round-trip failed for: {html:?}");
}
#[test]
fn wrap_unwrap_round_trip_plain_text() {
round_trip("hello");
}
#[test]
fn wrap_unwrap_round_trip_inline_tags() {
round_trip("<b>hello</b>");
}
#[test]
fn wrap_unwrap_round_trip_multiline() {
round_trip("<p>one</p>\n<p>two</p>");
}
#[test]
fn wrap_unwrap_round_trip_non_ascii() {
round_trip("<p>café — naïve</p>");
}
#[test]
fn wrap_unwrap_round_trip_crlf_inside() {
round_trip("<p>line one\r\nline two</p>");
}
#[test]
fn wrap_unwrap_round_trip_empty() {
round_trip("");
}
#[test]
fn wrap_produces_valid_utf8() {
let cases = ["hello", "<b>bold</b>", "<p>café</p>", ""];
for html in cases {
let payload = wrap(html);
assert!(
std::str::from_utf8(&payload).is_ok(),
"wrap output is not valid UTF-8 for: {html:?}"
);
}
}
#[test]
fn wrap_offsets_are_correct() {
let html = "<b>test</b>";
let payload = wrap(html);
let text = std::str::from_utf8(&payload).unwrap();
let start_html = extract_offset(text, "StartHTML:");
let end_html = extract_offset(text, "EndHTML:");
let start_frag = extract_offset(text, "StartFragment:");
let end_frag = extract_offset(text, "EndFragment:");
assert_eq!(
&payload[start_html..start_html + 6],
b"<html>",
"StartHTML does not point at <html>"
);
assert_eq!(
&payload[end_html - 7..end_html],
b"</html>",
"EndHTML does not point past </html>"
);
assert_eq!(
&payload[start_frag..end_frag],
html.as_bytes(),
"fragment slice does not match input"
);
}
#[test]
fn unwrap_rejects_missing_start_fragment() {
let payload = wrap("<p>x</p>");
let text = String::from_utf8(payload).unwrap();
let stripped: String = text
.lines()
.filter(|l| !l.trim_start().starts_with("StartFragment:"))
.map(|l| format!("{l}\r\n"))
.collect();
assert!(
unwrap(stripped.as_bytes()).is_err(),
"expected error for missing StartFragment"
);
}
#[test]
fn unwrap_rejects_missing_end_fragment() {
let payload = wrap("<p>x</p>");
let text = String::from_utf8(payload).unwrap();
let stripped: String = text
.lines()
.filter(|l| !l.trim_start().starts_with("EndFragment:"))
.map(|l| format!("{l}\r\n"))
.collect();
assert!(
unwrap(stripped.as_bytes()).is_err(),
"expected error for missing EndFragment"
);
}
#[test]
fn unwrap_rejects_non_numeric_offset() {
let payload = wrap("<p>x</p>");
let corrupted = String::from_utf8(payload)
.unwrap()
.replace("StartFragment:0000", "StartFragment:XXXX");
assert!(
unwrap(corrupted.as_bytes()).is_err(),
"expected error for non-numeric offset"
);
}
#[test]
fn unwrap_rejects_offset_out_of_range() {
let payload = wrap("<p>x</p>");
let text = String::from_utf8(payload.clone()).unwrap();
let big = format!("{:010}", payload.len() + 9999);
let corrupted = replace_offset_value(&text, "EndFragment:", &big);
assert!(
unwrap(corrupted.as_bytes()).is_err(),
"expected error for EndFragment out of range"
);
}
#[test]
fn unwrap_rejects_start_greater_than_end() {
let payload = wrap("<p>hello world</p>");
let text = String::from_utf8(payload).unwrap();
let start = extract_offset(&text, "StartFragment:");
let end = extract_offset(&text, "EndFragment:");
let swapped = replace_offset_value(
&replace_offset_value(&text, "StartFragment:", &format!("{end:010}")),
"EndFragment:",
&format!("{start:010}"),
);
assert!(
unwrap(swapped.as_bytes()).is_err(),
"expected error when StartFragment > EndFragment"
);
}
fn extract_offset(text: &str, prefix: &str) -> usize {
for line in text.lines() {
let line = line.trim_end_matches('\r');
if let Some(val) = line.strip_prefix(prefix) {
return val.trim().parse().expect("offset is not a number");
}
}
panic!("offset {prefix:?} not found in header");
}
fn replace_offset_value(text: &str, prefix: &str, new_val: &str) -> String {
text.lines()
.map(|l| {
let bare = l.trim_end_matches('\r');
if bare.starts_with(prefix) {
format!("{prefix}{new_val}\r\n")
} else {
format!("{l}\r\n")
}
})
.collect()
}
}