From 95ce22a4e8b8b10341fa8cd15d3759866df5b892 Mon Sep 17 00:00:00 2001 From: DoumanAsh Date: Tue, 20 Feb 2024 00:47:54 +0900 Subject: [PATCH 1/6] Implement HTML reading --- src/formats.rs | 39 ++++++++++++++++++++++++++++ src/html.rs | 3 +++ src/lib.rs | 1 + src/raw.rs | 63 +++++++++++++++++++++++++++++++++++++++++++--- tests/test_clip.rs | 14 ++++++++++- 5 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 src/html.rs diff --git a/src/formats.rs b/src/formats.rs index e58a87e..8e2697d 100644 --- a/src/formats.rs +++ b/src/formats.rs @@ -8,6 +8,8 @@ use crate::{SysResult, Getter, Setter}; use crate::types::c_uint; +use core::num::NonZeroU32; + ///A handle to a bitmap (HBITMAP). pub const CF_BITMAP: c_uint = 2; ///A memory object containing a BITMAPINFO structure followed by the bitmap bits. @@ -166,3 +168,40 @@ impl> Setter for Bitmap { crate::raw::set_bitmap(data.as_ref()) } } + +///HTML Foramt +/// +///Reference: https://learn.microsoft.com/en-us/windows/win32/dataxchg/html-clipboard-format +pub struct Html(NonZeroU32); + +impl Html { + #[inline(always)] + ///Creates new instance, if possible + pub fn new() -> Option { + //utf-16 "HTML Format" + const NAME: [u16; 12] = [72, 84, 77, 76, 32, 70, 111, 114, 109, 97, 116, 0]; + unsafe { + crate::raw::register_raw_format(&NAME).map(Self) + } + } + + #[inline(always)] + ///Gets raw format code + pub fn code(&self) -> u32 { + self.0.get() + } +} + +impl Getter> for Html { + #[inline(always)] + fn read_clipboard(&self, out: &mut alloc::vec::Vec) -> SysResult { + crate::raw::get_html(self.0.get(), out) + } +} + +impl Getter for Html { + #[inline(always)] + fn read_clipboard(&self, out: &mut alloc::string::String) -> SysResult { + crate::raw::get_html(self.0.get(), unsafe { out.as_mut_vec() }) + } +} diff --git a/src/html.rs b/src/html.rs new file mode 100644 index 0000000..63d3220 --- /dev/null +++ b/src/html.rs @@ -0,0 +1,3 @@ +pub const SEP: char = ':'; +pub const START_FRAGMENT: &str = "StartFragment"; +pub const END_FRAGMENT: &str = "EndFragment"; diff --git a/src/lib.rs b/src/lib.rs index 50c36f3..1ed9746 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -91,6 +91,7 @@ extern crate alloc; mod sys; pub mod types; pub mod formats; +mod html; pub mod raw; #[cfg(feature = "monitor")] pub mod monitor; diff --git a/src/raw.rs b/src/raw.rs index dd62f2b..ed7976b 100644 --- a/src/raw.rs +++ b/src/raw.rs @@ -22,14 +22,14 @@ const CP_UTF8: DWORD = 65001; use error_code::ErrorCode; -use core::{slice, mem, ptr, cmp}; +use core::{slice, mem, ptr, cmp, str, hint}; use core::num::{NonZeroUsize, NonZeroU32}; use alloc::string::String; use alloc::borrow::ToOwned; use alloc::format; -use crate::{SysResult, formats}; +use crate::{SysResult, html, formats}; use crate::utils::{unlikely_empty_size_result, RawMem}; #[inline(always)] @@ -264,6 +264,63 @@ pub fn get_vec(format: u32, out: &mut alloc::vec::Vec) -> SysResult { Ok(result) } +///Retrieves HTML using format code created by `register_raw_format` or `register_format` with +///argument `HTML Format` +pub fn get_html(format: u32, out: &mut alloc::vec::Vec) -> SysResult { + let ptr = RawMem::from_borrowed(get_clipboard_data(format)?); + + let result = unsafe { + let (data_ptr, _lock) = ptr.lock()?; + let data_size = GlobalSize(ptr.get()) as usize; + + let data = str::from_utf8_unchecked( + slice::from_raw_parts(data_ptr.as_ptr() as *const u8, data_size) + ); + + let mut start_idx = 0usize; + let mut end_idx = data.len(); + for line in data.lines() { + let mut split = line.split(html::SEP); + let key = match split.next() { + Some(key) => key, + None => hint::unreachable_unchecked(), + }; + let value = match split.next() { + Some(value) => value, + //Reached HTML + None => break + }; + match key { + html::START_FRAGMENT => match value.trim_start_matches('0').parse() { + Ok(value) => { + start_idx = value; + continue; + } + //Should not really happen + Err(_) => break, + }, + html::END_FRAGMENT => match value.trim_start_matches('0').parse() { + Ok(value) => { + end_idx = value; + continue; + } + //Should not really happen + Err(_) => break, + }, + _ => continue, + } + } + let size = end_idx - start_idx; + out.reserve(size); + let out_cursor = out.len(); + ptr::copy_nonoverlapping(data.as_ptr().add(start_idx), out.spare_capacity_mut().as_mut_ptr().add(out_cursor) as _, size); + out.set_len(out_cursor + size); + size + }; + + Ok(result) +} + /// Copies raw bytes onto clipboard with specified `format`, returning whether it was successful. /// /// This function empties the clipboard before setting the data. @@ -886,7 +943,7 @@ pub fn register_format(name: &str) -> Option { register_raw_format(&buffer) } } else { - let mut buffer = mem::MaybeUninit::<[u16; 52]>::uninit(); + let mut buffer = mem::MaybeUninit::<[u16; 52]>::zeroed(); let size = unsafe { MultiByteToWideChar(CP_UTF8, 0, name.as_ptr() as *const _, name.len() as c_int, buffer.as_mut_ptr() as *mut u16, 51) }; diff --git a/tests/test_clip.rs b/tests/test_clip.rs index 28b01b9..7f73a8f 100644 --- a/tests/test_clip.rs +++ b/tests/test_clip.rs @@ -1,5 +1,5 @@ use clipboard_win::{Getter, Setter, Clipboard, is_format_avail, types}; -use clipboard_win::formats::{RawData, Unicode, Bitmap, CF_TEXT, CF_UNICODETEXT, CF_BITMAP, FileList, CF_HDROP}; +use clipboard_win::formats::{Html, RawData, Unicode, Bitmap, CF_TEXT, CF_UNICODETEXT, CF_BITMAP, FileList, CF_HDROP}; fn should_set_file_list() { let _clip = Clipboard::new_attempts(10).expect("Open clipboard"); @@ -120,6 +120,17 @@ fn should_set_owner() { } } +fn should_set_get_html() { + let html1 = Html::new().expect("Create html1"); + let html2 = Html::new().expect("Create html2"); + assert_eq!(html1.code(), html2.code()); + + let _clip = Clipboard::new_attempts(10).expect("Open clipboard"); + let mut out = String::new(); + //TODO: need setter for this test to work + html1.read_clipboard(&mut out).expect("read clipboard"); +} + macro_rules! run { ($name:ident) => { println!("Clipboard test: {}...", stringify!($name)); @@ -139,4 +150,5 @@ fn clipboard_should_work() { run!(should_work_with_bytes); run!(should_work_with_set_empty_string); run!(should_set_owner); + run!(should_set_get_html); } From ac3d1b111870533043688ee85ba8ac2a2b242d2d Mon Sep 17 00:00:00 2001 From: DoumanAsh Date: Tue, 20 Feb 2024 00:50:10 +0900 Subject: [PATCH 2/6] Make CI trigger better --- .github/workflows/rust.yml | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index d439b67..9e62191 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -1,10 +1,28 @@ name: Rust -on: [push, pull_request] +on: + push: + branches: + - master + paths: + - '.github/workflows/rust.yml' + - 'src/**.rs' + - 'tests/**' + - 'Cargo.toml' + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + branches: + - '**' + paths: + - '.github/workflows/rust.yml' + - 'src/**.rs' + - 'tests/**' + - 'Cargo.toml' jobs: build: runs-on: windows-latest + if: github.event.pull_request.draft == false steps: - uses: actions/checkout@v4 From 92caef9ae983785fde72780a30b3863eeaad9c9e Mon Sep 17 00:00:00 2001 From: DoumanAsh Date: Tue, 20 Feb 2024 22:02:51 +0900 Subject: [PATCH 3/6] Implement html setting --- src/formats.rs | 7 ++++ src/html.rs | 62 ++++++++++++++++++++++++++++++ src/raw.rs | 94 +++++++++++++++++++++++++++++++++++++++++++--- tests/test_clip.rs | 11 +++++- 4 files changed, 168 insertions(+), 6 deletions(-) diff --git a/src/formats.rs b/src/formats.rs index 8e2697d..06eaf0b 100644 --- a/src/formats.rs +++ b/src/formats.rs @@ -205,3 +205,10 @@ impl Getter for Html { crate::raw::get_html(self.0.get(), unsafe { out.as_mut_vec() }) } } + +impl> Setter for Html { + #[inline(always)] + fn write_clipboard(&self, data: &T) -> SysResult<()> { + crate::raw::set_html(self.code(), data.as_ref()) + } +} diff --git a/src/html.rs b/src/html.rs index 63d3220..a3363d1 100644 --- a/src/html.rs +++ b/src/html.rs @@ -1,3 +1,65 @@ +use core::{cmp, fmt}; + pub const SEP: char = ':'; +pub const NEWLINE: &str = "\r\n"; +pub const LEN_SIZE: usize = 10; +pub const VERSION: &str = "Version"; pub const START_FRAGMENT: &str = "StartFragment"; pub const END_FRAGMENT: &str = "EndFragment"; +pub const START_HTML: &str = "StartHTML"; +pub const END_HTML: &str = "EndHTML"; +pub const BODY_HEADER: &str = "\r\n\r\n"; +pub const BODY_FOOTER: &str = "\r\n\r\n"; + +pub struct LengthBuffer([u8; LEN_SIZE]); + +impl LengthBuffer { + #[inline(always)] + pub const fn new() -> Self { + Self([b'0'; LEN_SIZE]) + } + + #[inline(always)] + pub const fn len(&self) -> usize { + self.0.len() + } + + #[inline(always)] + pub const fn as_ptr(&self) -> *const u8 { + self.0.as_ptr() + } +} + +impl AsRef<[u8]> for LengthBuffer { + #[inline(always)] + fn as_ref(&self) -> &[u8] { + self.0.as_slice() + } +} + +impl fmt::Write for LengthBuffer { + fn write_str(&mut self, input: &str) -> fmt::Result { + debug_assert!(input.len() <= self.0.len()); + let size = cmp::min(input.len(), self.0.len()); + + self.0[10-size..].copy_from_slice(&input.as_bytes()[..size]); + + Ok(()) + } +} + +//Samples +//DATA=Version:0.9 +//StartHTML:0000000187 +//EndHTML:0000001902 +//StartFragment:0000000223 +//EndFragment:0000001866 +// +// +// +//
+// +// +// + +//Version:0.9\r\nStartHTML:0000000105\r\nEndHTML:0000000187\r\nStartFragment:0000000141\r\nEndFragment:0000000151\r\n\r\n\r\n1\r\n\r\n diff --git a/src/raw.rs b/src/raw.rs index ed7976b..24c09db 100644 --- a/src/raw.rs +++ b/src/raw.rs @@ -264,8 +264,7 @@ pub fn get_vec(format: u32, out: &mut alloc::vec::Vec) -> SysResult { Ok(result) } -///Retrieves HTML using format code created by `register_raw_format` or `register_format` with -///argument `HTML Format` +///Retrieves HTML using format code created by `register_raw_format` or `register_format` with argument `HTML Format` pub fn get_html(format: u32, out: &mut alloc::vec::Vec) -> SysResult { let ptr = RawMem::from_borrowed(get_clipboard_data(format)?); @@ -321,6 +320,91 @@ pub fn get_html(format: u32, out: &mut alloc::vec::Vec) -> SysResult Ok(result) } +///Sets HTML using format code created by `register_raw_format` or `register_format` with argument `HTML Format` +pub fn set_html(format: u32, html: &str) -> SysResult<()> { + const VERSION_VALUE: &str = ":0.9"; + const HEADER_SIZE: usize = html::VERSION.len() + VERSION_VALUE.len() + html::NEWLINE.len() + + html::START_HTML.len() + html::LEN_SIZE + 1 + html::NEWLINE.len() + + html::END_HTML.len() + html::LEN_SIZE + 1 + html::NEWLINE.len() + + html::START_FRAGMENT.len() + html::LEN_SIZE + 1 + html::NEWLINE.len() + + html::END_FRAGMENT.len() + html::LEN_SIZE + 1 + html::NEWLINE.len(); + const FRAGMENT_OFFSET: usize = HEADER_SIZE + html::BODY_HEADER.len(); + + let total_size = FRAGMENT_OFFSET + html::BODY_FOOTER.len() + html.len(); + + let mut len_buffer = html::LengthBuffer::new(); + let mem = RawMem::new_global_mem(total_size)?; + + unsafe { + use core::fmt::Write; + let (ptr, _lock) = mem.lock()?; + let out = slice::from_raw_parts_mut(ptr.as_ptr() as *mut mem::MaybeUninit, total_size); + + let mut cursor = 0; + macro_rules! write_out { + ($input:expr) => { + let input = $input; + ptr::copy_nonoverlapping(input.as_ptr() as *const u8, out.as_mut_ptr().add(cursor) as _, input.len()); + cursor += input.len(); + }; + } + + write_out!(html::VERSION); + write_out!(VERSION_VALUE); + write_out!(html::NEWLINE); + + let _ = write!(&mut len_buffer, "{:0>10}", HEADER_SIZE); + write_out!(html::START_HTML); + write_out!([html::SEP as u8]); + write_out!(&len_buffer); + write_out!(html::NEWLINE); + + let _ = write!(&mut len_buffer, "{:0>10}", total_size); + write_out!(html::END_HTML); + write_out!([html::SEP as u8]); + write_out!(&len_buffer); + write_out!(html::NEWLINE); + + let _ = write!(&mut len_buffer, "{:0>10}", FRAGMENT_OFFSET); + write_out!(html::START_FRAGMENT); + write_out!([html::SEP as u8]); + write_out!(&len_buffer); + write_out!(html::NEWLINE); + + let _ = write!(&mut len_buffer, "{:0>10}", total_size - html::BODY_FOOTER.len()); + write_out!(html::END_FRAGMENT); + write_out!([html::SEP as u8]); + write_out!(&len_buffer); + write_out!(html::NEWLINE); + + //Verify StartHTML is correct + debug_assert_eq!(HEADER_SIZE, cursor); + + write_out!(html::BODY_HEADER); + + //Verify StartFragment is correct + debug_assert_eq!(FRAGMENT_OFFSET, cursor); + + write_out!(html); + + //Verify EndFragment is correct + debug_assert_eq!(total_size - html::BODY_FOOTER.len(), cursor); + + write_out!(html::BODY_FOOTER); + + //Verify EndHTML is correct + debug_assert_eq!(cursor, total_size); + } + + if unsafe { !SetClipboardData(format, mem.get()).is_null() } { + //SetClipboardData takes ownership + mem.release(); + Ok(()) + } else { + Err(ErrorCode::last_system()) + } +} + /// Copies raw bytes onto clipboard with specified `format`, returning whether it was successful. /// /// This function empties the clipboard before setting the data. @@ -710,12 +794,12 @@ pub fn set_file_list(paths: &[impl AsRef]) -> SysResult<()> { if unsafe { !SetClipboardData(formats::CF_HDROP, mem.get()).is_null() } { //SetClipboardData now has ownership of `mem`. mem.release(); - return Ok(()); + Ok(()) + } else { + Err(ErrorCode::last_system()) } - return Err(ErrorCode::last_system()); } - ///Enumerator over available clipboard formats. /// ///# Pre-conditions: diff --git a/tests/test_clip.rs b/tests/test_clip.rs index 7f73a8f..e8cb312 100644 --- a/tests/test_clip.rs +++ b/tests/test_clip.rs @@ -121,14 +121,23 @@ fn should_set_owner() { } fn should_set_get_html() { + const HTML: &str = "1"; let html1 = Html::new().expect("Create html1"); let html2 = Html::new().expect("Create html2"); assert_eq!(html1.code(), html2.code()); let _clip = Clipboard::new_attempts(10).expect("Open clipboard"); + html1.write_clipboard(&HTML).expect("write clipboard"); + let mut out = String::new(); - //TODO: need setter for this test to work html1.read_clipboard(&mut out).expect("read clipboard"); + assert_eq!(out, HTML); + + //Check empty output works + html1.write_clipboard(&"").expect("write clipboard"); + out.clear(); + html1.read_clipboard(&mut out).expect("read clipboard"); + assert!(out.is_empty()); } macro_rules! run { From b5ff3ece7b0cdee211de8939e4c04c80ebc54542 Mon Sep 17 00:00:00 2001 From: DoumanAsh Date: Tue, 20 Feb 2024 22:32:49 +0900 Subject: [PATCH 4/6] Html's UX --- src/formats.rs | 8 ++++++++ tests/test_clip.rs | 5 +++++ 2 files changed, 13 insertions(+) diff --git a/src/formats.rs b/src/formats.rs index 06eaf0b..bb96bac 100644 --- a/src/formats.rs +++ b/src/formats.rs @@ -169,6 +169,7 @@ impl> Setter for Bitmap { } } +#[derive(Copy, Clone)] ///HTML Foramt /// ///Reference: https://learn.microsoft.com/en-us/windows/win32/dataxchg/html-clipboard-format @@ -212,3 +213,10 @@ impl> Setter for Html { crate::raw::set_html(self.code(), data.as_ref()) } } + +impl From for u32 { + #[inline(always)] + fn from(value: Html) -> Self { + value.code() + } +} diff --git a/tests/test_clip.rs b/tests/test_clip.rs index e8cb312..5d76347 100644 --- a/tests/test_clip.rs +++ b/tests/test_clip.rs @@ -126,15 +126,20 @@ fn should_set_get_html() { let html2 = Html::new().expect("Create html2"); assert_eq!(html1.code(), html2.code()); + assert!(!is_format_avail(html1.code())); let _clip = Clipboard::new_attempts(10).expect("Open clipboard"); html1.write_clipboard(&HTML).expect("write clipboard"); + assert!(is_format_avail(html1.code())); + let mut out = String::new(); html1.read_clipboard(&mut out).expect("read clipboard"); assert_eq!(out, HTML); //Check empty output works html1.write_clipboard(&"").expect("write clipboard"); + assert!(is_format_avail(html1.into())); + out.clear(); html1.read_clipboard(&mut out).expect("read clipboard"); assert!(out.is_empty()); From a4c20457306b3b0febfb8fa062d339d17a11beef Mon Sep 17 00:00:00 2001 From: DoumanAsh Date: Tue, 20 Feb 2024 22:45:33 +0900 Subject: [PATCH 5/6] Add offset validation to get_html --- src/raw.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/raw.rs b/src/raw.rs index 24c09db..58b8d0e 100644 --- a/src/raw.rs +++ b/src/raw.rs @@ -309,7 +309,16 @@ pub fn get_html(format: u32, out: &mut alloc::vec::Vec) -> SysResult _ => continue, } } - let size = end_idx - start_idx; + + //Make sure HTML writer didn't screw up offsets of fragment + let size = match end_idx.checked_sub(start_idx) { + Some(size) => size, + None => return Err(ErrorCode::new_system(13)), + }; + if size > data_size { + return Err(ErrorCode::new_system(13)); + } + out.reserve(size); let out_cursor = out.len(); ptr::copy_nonoverlapping(data.as_ptr().add(start_idx), out.spare_capacity_mut().as_mut_ptr().add(out_cursor) as _, size); From 91301460f8ef7de0890952216a40079aa344cf26 Mon Sep 17 00:00:00 2001 From: DoumanAsh Date: Tue, 20 Feb 2024 22:49:06 +0900 Subject: [PATCH 6/6] CI:adjust trigger --- .github/workflows/rust.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 9e62191..40cbe9a 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -6,8 +6,8 @@ on: - master paths: - '.github/workflows/rust.yml' - - 'src/**.rs' - - 'tests/**' + - 'src/**/*.rs' + - 'tests/**/*.rs' - 'Cargo.toml' pull_request: types: [opened, synchronize, reopened, ready_for_review] @@ -15,8 +15,8 @@ on: - '**' paths: - '.github/workflows/rust.yml' - - 'src/**.rs' - - 'tests/**' + - 'src/**/*.rs' + - 'tests/**/*.rs' - 'Cargo.toml' jobs: