#![allow(dead_code)]
use std::path::{Path, PathBuf};
use crate::ClipboardError;
const HEADER_LEN: usize = 20;
pub fn build(paths: &[&Path]) -> Result<Vec<u8>, ClipboardError> {
let mut encoded: Vec<Vec<u16>> = Vec::with_capacity(paths.len());
for &path in paths {
let s = path.to_str().ok_or(ClipboardError::InvalidUri)?;
if s.is_empty() {
return Err(ClipboardError::InvalidUri);
}
if s.contains('\0') {
return Err(ClipboardError::InvalidUri);
}
let units: Vec<u16> = s.encode_utf16().chain(std::iter::once(0u16)).collect();
encoded.push(units);
}
let total_utf16_units: usize = encoded.iter().map(|v| v.len()).sum::<usize>() + 1;
let total_bytes = HEADER_LEN + total_utf16_units * 2;
let mut out = Vec::with_capacity(total_bytes);
out.extend_from_slice(&20u32.to_le_bytes());
out.extend_from_slice(&0i32.to_le_bytes());
out.extend_from_slice(&0i32.to_le_bytes());
out.extend_from_slice(&0i32.to_le_bytes());
out.extend_from_slice(&1i32.to_le_bytes());
debug_assert_eq!(out.len(), HEADER_LEN, "header must be exactly 20 bytes");
for units in &encoded {
for &unit in units {
out.extend_from_slice(&unit.to_le_bytes());
}
}
out.extend_from_slice(&0u16.to_le_bytes());
Ok(out)
}
pub fn parse(bytes: &[u8]) -> Result<Vec<PathBuf>, ClipboardError> {
let bad = || ClipboardError::io_other("malformed CF_HDROP");
if bytes.len() < HEADER_LEN {
return Err(bad());
}
let p_files = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
if p_files > bytes.len() {
return Err(bad());
}
let f_wide = i32::from_le_bytes(bytes[16..20].try_into().unwrap());
if f_wide == 0 {
return Err(bad());
}
let list_bytes = &bytes[p_files..];
if !list_bytes.len().is_multiple_of(2) {
return Err(bad());
}
let units: Vec<u16> = list_bytes
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
if units.is_empty() {
return Err(bad());
}
let mut paths = Vec::new();
let mut pos = 0;
loop {
if pos >= units.len() {
return Err(bad());
}
if units[pos] == 0 {
break;
}
let end = units[pos..]
.iter()
.position(|&u| u == 0)
.map(|rel| pos + rel)
.ok_or_else(bad)?;
let path_units = &units[pos..end];
let path_str = String::from_utf16(path_units).map_err(|_| bad())?;
paths.push(PathBuf::from(path_str));
pos = end + 1; }
Ok(paths)
}
#[cfg(test)]
mod tests {
use super::*;
fn round_trip(path_strs: &[&str]) {
let paths: Vec<&Path> = path_strs.iter().map(Path::new).collect();
let bytes = build(&paths).expect("build failed");
let recovered = parse(&bytes).expect("parse failed");
let recovered_strs: Vec<&str> = recovered.iter().map(|p| p.to_str().unwrap()).collect();
assert_eq!(
recovered_strs, path_strs,
"round-trip mismatch for {path_strs:?}"
);
}
#[test]
fn round_trip_single_drive_letter() {
round_trip(&["C:\\foo\\bar.txt"]);
}
#[test]
fn round_trip_single_unc() {
round_trip(&["\\\\server\\share\\file.txt"]);
}
#[test]
fn round_trip_multiple_mixed() {
round_trip(&[
"C:\\foo\\bar.txt",
"\\\\server\\share\\file.txt",
"D:\\Program Files\\app.exe",
]);
}
#[test]
fn round_trip_path_with_spaces() {
round_trip(&["D:\\Program Files\\app.exe"]);
}
#[test]
fn round_trip_non_ascii() {
round_trip(&["E:\\café\\naïve.txt"]);
}
#[test]
fn byte_layout_header_fields() {
let bytes = build(&[Path::new("C:\\foo")]).unwrap();
let p_files = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
assert_eq!(p_files, 20, "pFiles must be 20");
let pt_x = i32::from_le_bytes(bytes[4..8].try_into().unwrap());
assert_eq!(pt_x, 0, "pt.x must be 0");
let pt_y = i32::from_le_bytes(bytes[8..12].try_into().unwrap());
assert_eq!(pt_y, 0, "pt.y must be 0");
let f_nc = i32::from_le_bytes(bytes[12..16].try_into().unwrap());
assert_eq!(f_nc, 0, "fNC must be 0");
let f_wide = i32::from_le_bytes(bytes[16..20].try_into().unwrap());
assert_eq!(f_wide, 1, "fWide must be 1");
}
#[test]
fn byte_layout_utf16_content() {
let bytes = build(&[Path::new("A")]).unwrap();
assert_eq!(bytes[20], 0x41, "first byte of 'A' in UTF-16 LE");
assert_eq!(bytes[21], 0x00, "second byte of 'A' in UTF-16 LE");
assert_eq!(bytes[22], 0x00);
assert_eq!(bytes[23], 0x00);
assert_eq!(bytes[24], 0x00);
assert_eq!(bytes[25], 0x00);
assert_eq!(bytes.len(), 26, "total length for single 'A' path");
}
#[test]
fn build_rejects_empty_path() {
let result = build(&[Path::new("")]);
assert!(result.is_err(), "build should reject empty path");
}
#[test]
fn build_rejects_interior_null() {
let s = "foo\0bar";
let result = build(&[Path::new(s)]);
assert!(
result.is_err(),
"build should reject path with interior null"
);
}
#[test]
fn parse_rejects_too_short() {
let short = vec![0u8; 10]; assert!(parse(&short).is_err(), "parse should reject < 20 bytes");
}
#[test]
fn parse_rejects_f_wide_zero() {
let mut header = vec![0u8; 20];
header[0..4].copy_from_slice(&20u32.to_le_bytes());
header.extend_from_slice(&[0u8, 0u8]);
assert!(
parse(&header).is_err(),
"parse should reject fWide == 0 (ANSI)"
);
}
#[test]
fn parse_rejects_offset_out_of_bounds() {
let mut header = vec![0u8; 20];
header[0..4].copy_from_slice(&9999u32.to_le_bytes());
header[16..20].copy_from_slice(&1i32.to_le_bytes());
assert!(
parse(&header).is_err(),
"parse should reject pFiles out of bounds"
);
}
#[test]
fn parse_rejects_missing_list_terminator() {
let mut bytes = Vec::new();
bytes.extend_from_slice(&20u32.to_le_bytes()); bytes.extend_from_slice(&0i32.to_le_bytes()); bytes.extend_from_slice(&0i32.to_le_bytes()); bytes.extend_from_slice(&0i32.to_le_bytes()); bytes.extend_from_slice(&1i32.to_le_bytes()); bytes.extend_from_slice(&[0x41u8, 0x00u8]);
assert!(
parse(&bytes).is_err(),
"parse should error on missing list terminator"
);
}
}