From 5a627e944576ab26db233babf89b36b6409d1841 Mon Sep 17 00:00:00 2001 From: daxpedda Date: Thu, 5 Oct 2023 20:48:28 +0200 Subject: [PATCH 01/82] Fix CI --- src/platform/windows.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 39bdc2e..ba124c3 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -273,8 +273,7 @@ fn flip_v(image: ImageData) -> ImageData<'static> { let mut bytes = image.bytes.into_owned(); let rowsize = w * 4; // each pixel is 4 bytes - let mut tmp_a = Vec::new(); - tmp_a.resize(rowsize, 0); + let mut tmp_a = vec![0; rowsize]; // I believe this could be done safely with `as_chunks_mut`, but that's not stable yet for a_row_id in 0..(h / 2) { let b_row_id = h - a_row_id - 1; From 1e7d475d6e6d13b3eda9b7e35f3b94921c73b0fd Mon Sep 17 00:00:00 2001 From: dAxpeDDa Date: Sat, 18 Mar 2023 11:07:22 +0100 Subject: [PATCH 02/82] Update `x11rb` to v0.12 --- Cargo.lock | 56 ++++++++++++++++++++++++++++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 43 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 982ab00..cd4cdac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -256,9 +256,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "gethostname" -version = "0.2.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e" +checksum = "bb65d4ba3173c56a500b555b532f72c42e8d1fe64962b518897f8959fae2c177" dependencies = [ "libc", "winapi", @@ -339,9 +339,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.133" +version = "0.2.144" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966" +checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" [[package]] name = "lock_api" @@ -386,6 +386,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -410,7 +419,20 @@ dependencies = [ "bitflags", "cfg-if", "libc", - "memoffset", + "memoffset 0.6.5", +] + +[[package]] +name = "nix" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" +dependencies = [ + "bitflags", + "cfg-if", + "libc", + "memoffset 0.7.1", + "static_assertions", ] [[package]] @@ -636,6 +658,12 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "str-buf" version = "1.0.6" @@ -754,7 +782,7 @@ dependencies = [ "bitflags", "downcast-rs", "libc", - "nix", + "nix 0.24.2", "wayland-commons", "wayland-scanner", "wayland-sys", @@ -766,7 +794,7 @@ version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8691f134d584a33a6606d9d717b95c4fa20065605f798a3f350d78dced02a902" dependencies = [ - "nix", + "nix 0.24.2", "once_cell", "smallvec", "wayland-sys", @@ -902,7 +930,7 @@ dependencies = [ "derive-new", "libc", "log", - "nix", + "nix 0.24.2", "os_pipe", "tempfile", "thiserror", @@ -913,12 +941,12 @@ dependencies = [ [[package]] name = "x11rb" -version = "0.10.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "592b4883219f345e712b3209c62654ebda0bb50887f330cbd018d0f654bfd507" +checksum = "b1641b26d4dec61337c35a1b1aaf9e3cba8f46f0b43636c609ab0291a648040a" dependencies = [ "gethostname", - "nix", + "nix 0.26.2", "winapi", "winapi-wsapoll", "x11rb-protocol", @@ -926,11 +954,11 @@ dependencies = [ [[package]] name = "x11rb-protocol" -version = "0.10.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56b245751c0ac9db0e006dc812031482784e434630205a93c73cfefcaabeac67" +checksum = "82d6c3f9a0fb6701fab8f6cea9b0c0bd5d6876f1f89f7fada07e558077c344bc" dependencies = [ - "nix", + "nix 0.26.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 567a5d5..b0be037 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,7 +39,7 @@ image = { version = "0.24", optional = true, default-features = false, features [target.'cfg(all(unix, not(any(target_os="macos", target_os="android", target_os="emscripten"))))'.dependencies] log = "0.4" -x11rb = { version = "0.10" } +x11rb = { version = "0.12" } wl-clipboard-rs = { version = "0.7", optional = true } image = { version = "0.24", optional = true, default-features = false, features = ["png"] } parking_lot = "0.12" From 48117d9688d344131900926941d024b1e66f95cd Mon Sep 17 00:00:00 2001 From: dAxpeDDa Date: Sat, 24 Jun 2023 13:08:03 +0200 Subject: [PATCH 03/82] Update `wl-clipboard-rs` to v0.8 --- Cargo.lock | 327 ++++++++++++++++++++++++++++++++--------------------- Cargo.toml | 2 +- 2 files changed, 201 insertions(+), 128 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cd4cdac..06b50e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -60,6 +60,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" + [[package]] name = "block" version = "0.1.6" @@ -84,6 +90,12 @@ version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + [[package]] name = "cfg-if" version = "1.0.0" @@ -140,7 +152,7 @@ version = "0.22.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" dependencies = [ - "bitflags", + "bitflags 1.3.2", "core-foundation", "core-graphics-types", "foreign-types", @@ -153,7 +165,7 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a68b68b3446082644c91ac778bf50cd4104bfb002b5a6a7c44cca5a2c70788b" dependencies = [ - "bitflags", + "bitflags 1.3.2", "core-foundation", "foreign-types", "libc", @@ -179,6 +191,15 @@ dependencies = [ "syn", ] +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", +] + [[package]] name = "downcast-rs" version = "1.2.0" @@ -198,6 +219,27 @@ dependencies = [ "termcolor", ] +[[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "error-code" version = "2.3.1" @@ -210,12 +252,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "1.8.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" -dependencies = [ - "instant", -] +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "fixedbitset" @@ -310,15 +349,6 @@ dependencies = [ "hashbrown", ] -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] - [[package]] name = "itoa" version = "1.0.3" @@ -339,9 +369,25 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.144" +version = "0.2.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" +checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" + +[[package]] +name = "libloading" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d580318f95776505201b28cf98eb1fa5e4be3b689633ba6a3e6cd880ff22d8cb" +dependencies = [ + "cfg-if", + "windows-sys", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3852614a3bd9ca9804678ba6be5e3b8ce76dfc902cae004e3e0c44051b6e88db" [[package]] name = "lock_api" @@ -355,12 +401,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.17" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "malloc_buf" @@ -377,15 +420,6 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" -[[package]] -name = "memoffset" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" -dependencies = [ - "autocfg", -] - [[package]] name = "memoffset" version = "0.7.1" @@ -412,27 +446,15 @@ dependencies = [ [[package]] name = "nix" -version = "0.24.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "195cdbc1741b8134346d515b3a56a1c94b0912758009cfd53f99ea0f57b065fc" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cfg-if", "libc", - "memoffset 0.6.5", -] - -[[package]] -name = "nix" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" -dependencies = [ - "bitflags", - "cfg-if", - "libc", - "memoffset 0.7.1", - "static_assertions", + "memoffset", + "pin-utils", ] [[package]] @@ -521,12 +543,12 @@ checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" [[package]] name = "os_pipe" -version = "1.0.1" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c92f2b54f081d635c77e7120862d48db8e91f7f21cef23ab1b4fe9971c59f55" +checksum = "0ae859aa07428ca9a929b936690f8b12dc5f11dd8c6992a18ca93919f28bc177" dependencies = [ "libc", - "winapi", + "windows-sys", ] [[package]] @@ -541,15 +563,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.3" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-sys", + "windows-targets", ] [[package]] @@ -562,6 +584,12 @@ dependencies = [ "indexmap", ] +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pkg-config" version = "0.3.25" @@ -574,7 +602,7 @@ version = "0.17.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f0e7f4c94ec26ff209cee506314212639d6c91b80afb82984819fafce9df01c" dependencies = [ - "bitflags", + "bitflags 1.3.2", "crc32fast", "flate2", "miniz_oxide", @@ -589,6 +617,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-xml" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.21" @@ -600,11 +637,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.16" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -625,14 +662,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" [[package]] -name = "remove_dir_all" -version = "0.5.3" +name = "rustix" +version = "0.38.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +checksum = "f25469e9ae0f3d0047ca8b93fc56843f38e6774f0914a107ff8b41be8be8e0b7" dependencies = [ - "winapi", + "bitflags 2.4.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.1.0" @@ -658,12 +705,6 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - [[package]] name = "str-buf" version = "1.0.6" @@ -683,16 +724,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.3.0" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ "cfg-if", "fastrand", - "libc", "redox_syscall", - "remove_dir_all", - "winapi", + "rustix", + "windows-sys", ] [[package]] @@ -774,61 +814,75 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" [[package]] -name = "wayland-client" -version = "0.29.5" +name = "wayland-backend" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f3b068c05a039c9f755f881dc50f01732214f5685e379829759088967c46715" +checksum = "19152ddd73f45f024ed4534d9ca2594e0ef252c1847695255dae47f34df9fbe4" dependencies = [ - "bitflags", + "cc", "downcast-rs", - "libc", - "nix 0.24.2", - "wayland-commons", - "wayland-scanner", - "wayland-sys", -] - -[[package]] -name = "wayland-commons" -version = "0.29.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8691f134d584a33a6606d9d717b95c4fa20065605f798a3f350d78dced02a902" -dependencies = [ - "nix 0.24.2", - "once_cell", + "nix", + "scoped-tls", "smallvec", "wayland-sys", ] [[package]] -name = "wayland-protocols" -version = "0.29.5" +name = "wayland-client" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b950621f9354b322ee817a23474e479b34be96c2e909c14f7bc0100e9a970bc6" +checksum = "1ca7d52347346f5473bf2f56705f360e8440873052e575e55890c4fa57843ed3" dependencies = [ - "bitflags", + "bitflags 2.4.0", + "nix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e253d7107ba913923dc253967f35e8561a3c65f914543e46843c88ddd729e21c" +dependencies = [ + "bitflags 2.4.0", + "wayland-backend", "wayland-client", - "wayland-commons", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" +dependencies = [ + "bitflags 2.4.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", "wayland-scanner", ] [[package]] name = "wayland-scanner" -version = "0.29.5" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f4303d8fa22ab852f789e75a967f0a2cdc430a607751c0499bada3e451cbd53" +checksum = "fb8e28403665c9f9513202b7e1ed71ec56fde5c107816843fb14057910b2c09c" dependencies = [ "proc-macro2", + "quick-xml", "quote", - "xml-rs", ] [[package]] name = "wayland-sys" -version = "0.29.5" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be12ce1a3c39ec7dba25594b97b42cb3195d54953ddb9d3d95a7c3902bc6e9d4" +checksum = "15a0c8eaff5216d07f226cb7a549159267f3467b289d9a2e52fd3ef5aae2b7af" dependencies = [ + "dlib", + "log", "pkg-config", ] @@ -880,63 +934,88 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" -version = "0.36.1" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +dependencies = [ + "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_msvc", "windows_x86_64_gnu", + "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] -name = "windows_aarch64_msvc" -version = "0.36.1" +name = "windows_aarch64_gnullvm" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" [[package]] name = "windows_i686_gnu" -version = "0.36.1" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" [[package]] name = "windows_i686_msvc" -version = "0.36.1" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" [[package]] name = "windows_x86_64_gnu" -version = "0.36.1" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" [[package]] name = "windows_x86_64_msvc" -version = "0.36.1" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "wl-clipboard-rs" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "981a303dfbb75d659f6612d05a14b2e363c103d24f676a2d44a00d18507a1ad9" +checksum = "57af79e973eadf08627115c73847392e6b766856ab8e3844a59245354b23d2fa" dependencies = [ "derive-new", "libc", "log", - "nix 0.24.2", + "nix", "os_pipe", "tempfile", "thiserror", "tree_magic_mini", + "wayland-backend", "wayland-client", "wayland-protocols", + "wayland-protocols-wlr", ] [[package]] @@ -946,7 +1025,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1641b26d4dec61337c35a1b1aaf9e3cba8f46f0b43636c609ab0291a648040a" dependencies = [ "gethostname", - "nix 0.26.2", + "nix", "winapi", "winapi-wsapoll", "x11rb-protocol", @@ -958,11 +1037,5 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82d6c3f9a0fb6701fab8f6cea9b0c0bd5d6876f1f89f7fada07e558077c344bc" dependencies = [ - "nix 0.26.2", + "nix", ] - -[[package]] -name = "xml-rs" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" diff --git a/Cargo.toml b/Cargo.toml index b0be037..3f8e3fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,7 @@ image = { version = "0.24", optional = true, default-features = false, features [target.'cfg(all(unix, not(any(target_os="macos", target_os="android", target_os="emscripten"))))'.dependencies] log = "0.4" x11rb = { version = "0.12" } -wl-clipboard-rs = { version = "0.7", optional = true } +wl-clipboard-rs = { version = "0.8", optional = true } image = { version = "0.24", optional = true, default-features = false, features = ["png"] } parking_lot = "0.12" From d755392b562f3f1b161d9368d2931e354b2b6246 Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Sun, 8 Oct 2023 16:20:51 -0500 Subject: [PATCH 04/82] Unify logging dependencies by using env_logger everywhere There is no reason to depend on two different dev dependency loggers --- Cargo.lock | 58 ----------------------------------------- Cargo.toml | 1 - examples/daemonize.rs | 3 +-- examples/hello_world.rs | 3 +-- examples/set_html.rs | 3 +-- 5 files changed, 3 insertions(+), 65 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 06b50e3..c28d433 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,7 +30,6 @@ dependencies = [ "objc-foundation", "objc_id", "parking_lot", - "simple_logger", "thiserror", "winapi", "wl-clipboard-rs", @@ -119,17 +118,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" -[[package]] -name = "colored" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" -dependencies = [ - "atty", - "lazy_static", - "winapi", -] - [[package]] name = "core-foundation" version = "0.9.3" @@ -349,12 +337,6 @@ dependencies = [ "hashbrown", ] -[[package]] -name = "itoa" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" - [[package]] name = "jpeg-decoder" version = "0.2.6" @@ -497,15 +479,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "num_threads" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" -dependencies = [ - "libc", -] - [[package]] name = "objc" version = "0.2.7" @@ -686,19 +659,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" -[[package]] -name = "simple_logger" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48047e77b528151aaf841a10a9025f9459da80ba820e425ff7eb005708a76dc7" -dependencies = [ - "atty", - "colored", - "log", - "time", - "winapi", -] - [[package]] name = "smallvec" version = "1.9.0" @@ -775,24 +735,6 @@ dependencies = [ "weezl", ] -[[package]] -name = "time" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3f9a28b618c3a6b9251b6908e9c99e04b9e5c02e6581ccbb67d59c34ef7f9b" -dependencies = [ - "itoa", - "libc", - "num_threads", - "time-macros", -] - -[[package]] -name = "time-macros" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" - [[package]] name = "tree_magic_mini" version = "3.0.3" diff --git a/Cargo.toml b/Cargo.toml index 3f8e3fa..41371b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,6 @@ wayland-data-control = ["wl-clipboard-rs"] thiserror = "1.0" [dev-dependencies] -simple_logger = "2.1" env_logger = "0.9.0" [target.'cfg(windows)'.dependencies] diff --git a/examples/daemonize.rs b/examples/daemonize.rs index b657ff6..94986dd 100644 --- a/examples/daemonize.rs +++ b/examples/daemonize.rs @@ -4,7 +4,6 @@ use arboard::Clipboard; #[cfg(target_os = "linux")] use arboard::SetExtLinux; -use simple_logger::SimpleLogger; use std::{env, error::Error, process}; // An argument that can be passed into the program to signal that it should daemonize itself. This @@ -18,7 +17,7 @@ fn main() -> Result<(), Box> { return Ok(()); } - SimpleLogger::new().init().unwrap(); + env_logger::init(); if cfg!(target_os = "linux") { process::Command::new(env::current_exe()?) diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 07f8c19..efc4ebf 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -1,8 +1,7 @@ use arboard::Clipboard; -use simple_logger::SimpleLogger; fn main() { - SimpleLogger::new().init().unwrap(); + env_logger::init(); let mut clipboard = Clipboard::new().unwrap(); println!("Clipboard text was: {:?}", clipboard.get_text()); diff --git a/examples/set_html.rs b/examples/set_html.rs index 2662d31..125f6b1 100644 --- a/examples/set_html.rs +++ b/examples/set_html.rs @@ -1,9 +1,8 @@ use arboard::Clipboard; -use simple_logger::SimpleLogger; use std::{thread, time::Duration}; fn main() { - SimpleLogger::new().init().unwrap(); + env_logger::init(); let mut ctx = Clipboard::new().unwrap(); let html = r#"

Hello, World!

From 9bc6fcd3f8dd20560f7d0615948c39429e24ef84 Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Sun, 8 Oct 2023 16:25:59 -0500 Subject: [PATCH 05/82] Return a more clear error message when X11 connections timeout --- src/platform/linux/mod.rs | 2 +- src/platform/linux/x11.rs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/platform/linux/mod.rs b/src/platform/linux/mod.rs index c51d4ed..79264e4 100644 --- a/src/platform/linux/mod.rs +++ b/src/platform/linux/mod.rs @@ -269,7 +269,7 @@ pub trait ClearExtLinux: private::Sealed { /// ### Example /// /// ```no_run - /// # use arboard::{ Clipboard, LinuxClipboardKind, ClearExtLinux, Error }; + /// # use arboard::{Clipboard, LinuxClipboardKind, ClearExtLinux, Error}; /// # fn main() -> Result<(), Error> { /// let mut clipboard = Clipboard::new()?; /// diff --git a/src/platform/linux/x11.rs b/src/platform/linux/x11.rs index 21f79a0..c983ff1 100644 --- a/src/platform/linux/x11.rs +++ b/src/platform/linux/x11.rs @@ -139,7 +139,9 @@ impl XContext { thread::spawn(move || { tx.send(RustConnection::connect(None)).ok(); // disregard error sending on channel as main thread has timed out. }); - let patient_conn = rx.recv_timeout(SHORT_TIMEOUT_DUR).map_err(into_unknown)?; + let patient_conn = rx.recv_timeout(SHORT_TIMEOUT_DUR).map_err(|_| Error::Unknown { + description: String::from("X11 server connection timed out because it was unreachable"), + })?; let (conn, screen_num): (RustConnection, _) = patient_conn.map_err(into_unknown)?; let screen = conn From f801e67e707fa64eca114906375bc050e600b2d4 Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Sun, 8 Oct 2023 16:35:34 -0500 Subject: [PATCH 06/82] Bump Miri toolchain version and resolve used warnings --- .github/workflows/test.yml | 2 +- src/platform/windows.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 90911a4..cecba4e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -107,7 +107,7 @@ jobs: steps: - uses: actions-rs/toolchain@v1 with: - toolchain: nightly-2022-10-15 + toolchain: nightly-2023-10-08 override: true components: miri diff --git a/src/platform/windows.rs b/src/platform/windows.rs index ba124c3..dacb6f3 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -675,12 +675,12 @@ mod tests { #[test] fn check_win_to_rgba_conversion() { let mut data = DATA; - unsafe { win_to_rgba(&mut data) }; + let _converted = unsafe { win_to_rgba(&mut data) }; } #[test] fn check_rgba_to_win_conversion() { let mut data = DATA; - unsafe { rgba_to_win(&mut data) }; + let _converted = unsafe { rgba_to_win(&mut data) }; } } From b2b0809632c7af3d801c7f9b5b513d94bdba9f58 Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Mon, 20 Nov 2023 15:40:30 -0600 Subject: [PATCH 07/82] Add support for explicit clipboard monitor excluding on Windows --- CHANGELOG.md | 3 +++ src/platform/windows.rs | 50 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac55179..d3eb0cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## Unreleased +- Add support for `ExcludeClipboardContentFromMonitorProcessing` on Windows platforms. + ## 3.2.1 on 2023-28-11 ### Fixed diff --git a/src/platform/windows.rs b/src/platform/windows.rs index dacb6f3..c17d2d6 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -493,13 +493,19 @@ impl<'clipboard> Get<'clipboard> { pub(crate) struct Set<'clipboard> { clipboard: Result, Error>, + exclude_from_monitoring: bool, exclude_from_cloud: bool, exclude_from_history: bool, } impl<'clipboard> Set<'clipboard> { pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self { - Self { clipboard: clipboard.open(), exclude_from_cloud: false, exclude_from_history: false } + Self { + clipboard: clipboard.open(), + exclude_from_monitoring: false, + exclude_from_cloud: false, + exclude_from_history: false, + } } pub(crate) fn text(self, data: Cow<'_, str>) -> Result<(), Error> { @@ -509,7 +515,12 @@ impl<'clipboard> Set<'clipboard> { description: "Could not place the specified text to the clipboard".into(), })?; - add_clipboard_exclusions(open_clipboard, self.exclude_from_cloud, self.exclude_from_history) + add_clipboard_exclusions( + open_clipboard, + self.exclude_from_monitoring, + self.exclude_from_cloud, + self.exclude_from_history, + ) } pub(crate) fn html(self, html: Cow<'_, str>, alt: Option>) -> Result<(), Error> { @@ -529,7 +540,12 @@ impl<'clipboard> Set<'clipboard> { .map_err(|e| Error::Unknown { description: e.to_string() })?; } - add_clipboard_exclusions(open_clipboard, self.exclude_from_cloud, self.exclude_from_history) + add_clipboard_exclusions( + open_clipboard, + self.exclude_from_monitoring, + self.exclude_from_cloud, + self.exclude_from_history, + ) } #[cfg(feature = "image-data")] @@ -548,6 +564,7 @@ impl<'clipboard> Set<'clipboard> { fn add_clipboard_exclusions( _open_clipboard: OpenClipboard<'_>, + exclude_from_monitoring: bool, exclude_from_cloud: bool, exclude_from_history: bool, ) -> Result<(), Error> { @@ -556,10 +573,24 @@ fn add_clipboard_exclusions( /// See https://docs.microsoft.com/en-us/windows/win32/dataxchg/clipboard-formats#cloud-clipboard-and-clipboard-history-formats const CLIPBOARD_EXCLUSION_DATA: &[u8] = &0u32.to_ne_bytes(); - // Clipboard exclusions are applied retroactively to the item that is currently in the clipboard. + // Clipboard exclusions are applied retroactively (we still have the clipboard lock) to the item that is currently in the clipboard. // See the MS docs on `CLIPBOARD_EXCLUSION_DATA` for specifics. Once the item is added to the clipboard, // tell Windows to remove it from cloud syncing and history. + if exclude_from_monitoring { + if let Some(format) = + clipboard_win::register_format("ExcludeClipboardContentFromMonitorProcessing") + { + // The documentation states "place any data on the clipboard in this format to prevent...", and using the zero bytes + // like the others for consistency works. + clipboard_win::raw::set_without_clear(format.get(), CLIPBOARD_EXCLUSION_DATA).map_err( + |_| Error::Unknown { + description: "Failed to exclude data from clipboard monitoring".into(), + }, + )?; + } + } + if exclude_from_cloud { if let Some(format) = clipboard_win::register_format("CanUploadToCloudClipboard") { // We believe that it would be a logic error if this call failed, since we've validated the format is supported, @@ -589,6 +620,12 @@ fn add_clipboard_exclusions( /// Windows-specific extensions to the [`Set`](crate::Set) builder. pub trait SetExtWindows: private::Sealed { + /// Exclude the data which will be set on the clipboard from being processed + /// at all, either in the local clipboard history or getting uploaded to the cloud. + /// + /// If this is set, it is not recommended to call [exclude_from_cloud](SetExtWindows::exclude_from_cloud) or [exclude_from_history](SetExtWindows::exclude_from_history). + fn exclude_from_monitoring(self) -> Self; + /// Excludes the data which will be set on the clipboard from being uploaded to /// the Windows 10/11 [cloud clipboard]. /// @@ -603,6 +640,11 @@ pub trait SetExtWindows: private::Sealed { } impl SetExtWindows for crate::Set<'_> { + fn exclude_from_monitoring(mut self) -> Self { + self.platform.exclude_from_monitoring = true; + self + } + fn exclude_from_cloud(mut self) -> Self { self.platform.exclude_from_cloud = true; self From 11e395c6d5fbaad0e5ac74286daf569f1fb9cc3a Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Mon, 20 Nov 2023 16:13:11 -0600 Subject: [PATCH 08/82] Release 3.3.0 --- CHANGELOG.md | 15 +++++++++++---- Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3eb0cb..a41f6af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,16 @@ # Changelog -## Unreleased +## 3.3.0 on 2023-20-11 + +### Added - Add support for `ExcludeClipboardContentFromMonitorProcessing` on Windows platforms. -## 3.2.1 on 2023-28-11 +### Changed +- Improved timeout error messaging. +- Update `wl-clipboard-rs` to `0.8`. +- Update `x11rb` to `0.12`. + +## 3.2.1 on 2023-29-08 ### Fixed - Removed all leaks from the macOS clipboard code. Previously, both the `get` and `set` methods leaked data. @@ -32,13 +39,13 @@ platform implementations again. - Updated minimum `clipboard-win` version to `4.4`. - Updated `wl-clipboard-rs` to the version `0.7`. -## 3.1 on 2022-20-09 +## 3.1.0 on 2022-20-09 ### Changed - Updated `image` to the version `0.24`. - Lowered Wayland clipboard initialization log level. -## 3.0 on 2022-19-09 +## 3.0.0 on 2022-19-09 ### Added - Support for clearing the clipboard. diff --git a/Cargo.lock b/Cargo.lock index c28d433..170dc9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,7 +19,7 @@ dependencies = [ [[package]] name = "arboard" -version = "3.2.1" +version = "3.3.0" dependencies = [ "clipboard-win", "core-graphics", diff --git a/Cargo.toml b/Cargo.toml index 41371b6..44cb224 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "arboard" -version = "3.2.1" +version = "3.3.0" authors = ["Artur Kovacs ", "Avi Weinstock ", "Arboard contributors"] description = "Image and text handling for the OS clipboard." repository = "https://github.com/1Password/arboard" From 8c475cfd1420fe63f9fe21e62b4bf3d0b42f4175 Mon Sep 17 00:00:00 2001 From: rhysd Date: Tue, 21 Nov 2023 10:57:21 +0900 Subject: [PATCH 09/82] Make winapi crate optional --- Cargo.toml | 2 +- src/platform/windows.rs | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 44cb224..fd77e4b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ thiserror = "1.0" env_logger = "0.9.0" [target.'cfg(windows)'.dependencies] -winapi = { version = "0.3.9", features = [ +winapi = { version = "0.3.9", optional = true, features = [ "basetsd", "winuser", "winbase", diff --git a/src/platform/windows.rs b/src/platform/windows.rs index c17d2d6..60dfa1f 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -8,7 +8,7 @@ the Apache 2.0 or the MIT license at the licensee's choice. The terms and conditions of the chosen license apply to this file. */ -use std::{borrow::Cow, marker::PhantomData}; +use std::{borrow::Cow, marker::PhantomData, thread, time::Duration}; #[cfg(feature = "image-data")] use std::{convert::TryInto, mem::size_of}; @@ -400,8 +400,7 @@ impl Clipboard { } // The default value matches Chromium's implementation, but could be tweaked later. - // Safety: This is safe to call with any integer. - unsafe { winapi::um::synchapi::Sleep(5) }; + thread::sleep(Duration::from_millis(5)); } .map_err(|_| Error::ClipboardOccupied)?; From e3f54c30498f0bc3d7d716a1353e2f14a31bfa73 Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Tue, 21 Nov 2023 21:30:06 -0600 Subject: [PATCH 10/82] Document MSRV of 1.61 --- .github/workflows/test.yml | 6 ++++++ CHANGELOG.md | 1 + Cargo.toml | 1 + README.md | 1 + 4 files changed, 9 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cecba4e..0c92da7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,6 +28,9 @@ jobs: strategy: matrix: os: [macos-latest, windows-latest, ubuntu-latest] + # Latest stable and MSRV. We only run checks with all features enabled + # for the MSRV build to keep CI fast, since other configurations should also work. + rust_version: [stable, "1.61"] steps: - uses: actions-rs/toolchain@v1 with: @@ -37,18 +40,21 @@ jobs: - uses: actions/checkout@v2 - name: Run `cargo clippy` with no features + if: ${{ matrix.rust_version == 'stable' }} uses: actions-rs/cargo@v1 with: command: clippy args: --verbose --no-default-features -- -D warnings -D clippy::dbg_macro - name: Run `cargo clippy` with `image-data` feature + if: ${{ matrix.rust_version == 'stable' }} uses: actions-rs/cargo@v1 with: command: clippy args: --verbose --no-default-features --features image-data -- -D warnings -D clippy::dbg_macro - name: Run `cargo clippy` with `wayland-data-control` feature + if: ${{ matrix.rust_version == 'stable' }} uses: actions-rs/cargo@v1 with: command: clippy diff --git a/CHANGELOG.md b/CHANGELOG.md index a41f6af..afa2117 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Improved timeout error messaging. - Update `wl-clipboard-rs` to `0.8`. - Update `x11rb` to `0.12`. +- `arboard`'s MSRV is now 1.61. ## 3.2.1 on 2023-29-08 diff --git a/Cargo.toml b/Cargo.toml index fd77e4b..ff9e33d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ license = "MIT OR Apache-2.0" readme = "README.md" keywords = ["clipboard", "image"] edition = "2018" +rust-version = "1.61" [features] default = ["image-data"] diff --git a/README.md b/README.md index cbef596..0eddd54 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![Latest version](https://img.shields.io/crates/v/arboard?color=mediumvioletred)](https://crates.io/crates/arboard) [![Documentation](https://docs.rs/arboard/badge.svg)](https://docs.rs/arboard) +![MSRV](https://img.shields.io/badge/rustc-1.61+-blue.svg) ## General From 1b8df75ee234dc8ae13785528bd34e4cb1aba8c3 Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Tue, 21 Nov 2023 21:31:11 -0600 Subject: [PATCH 11/82] Bump to Rust 2021 edition --- Cargo.toml | 2 +- src/platform/linux/mod.rs | 2 +- src/platform/linux/wayland.rs | 4 ++-- src/platform/osx.rs | 3 +-- src/platform/windows.rs | 2 +- 5 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ff9e33d..7c2e11d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ repository = "https://github.com/1Password/arboard" license = "MIT OR Apache-2.0" readme = "README.md" keywords = ["clipboard", "image"] -edition = "2018" +edition = "2021" rust-version = "1.61" [features] diff --git a/src/platform/linux/mod.rs b/src/platform/linux/mod.rs index 79264e4..5bb28f5 100644 --- a/src/platform/linux/mod.rs +++ b/src/platform/linux/mod.rs @@ -13,7 +13,7 @@ mod x11; mod wayland; fn into_unknown(error: E) -> Error { - Error::Unknown { description: format!("{}", error) } + Error::Unknown { description: error.to_string() } } #[cfg(feature = "image-data")] diff --git a/src/platform/linux/wayland.rs b/src/platform/linux/wayland.rs index 955a736..c2b316d 100644 --- a/src/platform/linux/wayland.rs +++ b/src/platform/linux/wayland.rs @@ -71,7 +71,7 @@ impl Clipboard { Err(PasteError::PrimarySelectionUnsupported) => Err(Error::ClipboardNotSupported), - Err(err) => Err(Error::Unknown { description: format!("{}", err) }), + Err(err) => Err(Error::Unknown { description: err.to_string() }), } } @@ -154,7 +154,7 @@ impl Clipboard { Err(Error::ContentNotAvailable) } - Err(err) => Err(Error::Unknown { description: format!("{}", err) }), + Err(err) => Err(Error::Unknown { description: err.to_string() }), } } diff --git a/src/platform/osx.rs b/src/platform/osx.rs index cc7a4e7..c48df73 100644 --- a/src/platform/osx.rs +++ b/src/platform/osx.rs @@ -282,8 +282,7 @@ impl<'clipboard> Set<'clipboard> { // https://bugzilla.mozilla.org/show_bug.cgi?id=466599 // https://bugs.chromium.org/p/chromium/issues/detail?id=11957 let html = format!( - r#"{}"#, - html + r#"{html}"#, ); let html_nss = NSString::from_str(&html); // Make sure that we pass a pointer to the string and not the object itself. diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 60dfa1f..bffa524 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -553,7 +553,7 @@ impl<'clipboard> Set<'clipboard> { if let Err(e) = clipboard_win::raw::empty() { return Err(Error::Unknown { - description: format!("Failed to empty the clipboard. Got error code: {}", e), + description: format!("Failed to empty the clipboard. Got error code: {e}"), }); }; From a100f2d77c5b2877257220e9144179d1b78784fe Mon Sep 17 00:00:00 2001 From: Linda_pp Date: Tue, 5 Dec 2023 05:52:14 +0900 Subject: [PATCH 12/82] Update clipboard-win to v5 and replace winapi with windows-sys (#123) * Refactor Windows code to be more idiomatic --- Cargo.lock | 22 +- Cargo.toml | 14 +- src/common.rs | 7 + src/platform/windows.rs | 757 ++++++++++++++++++++-------------------- 4 files changed, 408 insertions(+), 392 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 170dc9a..4f90e16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31,7 +31,7 @@ dependencies = [ "objc_id", "parking_lot", "thiserror", - "winapi", + "windows-sys", "wl-clipboard-rs", "x11rb", ] @@ -103,13 +103,11 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clipboard-win" -version = "4.4.2" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4ab1b92798304eedc095b53942963240037c0516452cb11aeba709d420b2219" +checksum = "c57002a5d9be777c1ef967e33674dac9ebd310d8893e4e3437b14d5f0f6372cc" dependencies = [ "error-code", - "str-buf", - "winapi", ] [[package]] @@ -230,13 +228,9 @@ dependencies = [ [[package]] name = "error-code" -version = "2.3.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" -dependencies = [ - "libc", - "str-buf", -] +checksum = "281e452d3bad4005426416cdba5ccfd4f5c1280e10099e21db27f7c1c28347fc" [[package]] name = "fastrand" @@ -665,12 +659,6 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" -[[package]] -name = "str-buf" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" - [[package]] name = "syn" version = "1.0.100" diff --git a/Cargo.toml b/Cargo.toml index 7c2e11d..f18e976 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ rust-version = "1.61" [features] default = ["image-data"] -image-data = ["core-graphics", "image", "winapi/minwindef", "winapi/wingdi", "winapi/winnt"] +image-data = ["core-graphics", "image", "windows-sys"] wayland-data-control = ["wl-clipboard-rs"] [dependencies] @@ -22,12 +22,14 @@ thiserror = "1.0" env_logger = "0.9.0" [target.'cfg(windows)'.dependencies] -winapi = { version = "0.3.9", optional = true, features = [ - "basetsd", - "winuser", - "winbase", +windows-sys = { version = "0.48.0", optional = true, features = [ + "Win32_Foundation", + "Win32_Graphics_Gdi", + "Win32_System_DataExchange", + "Win32_System_Memory", + "Win32_System_Ole", ]} -clipboard-win = "4.4.2" +clipboard-win = "5.0.0" log = "0.4" [target.'cfg(target_os = "macos")'.dependencies] diff --git a/src/common.rs b/src/common.rs index 95c63f6..3b51c82 100644 --- a/src/common.rs +++ b/src/common.rs @@ -85,6 +85,13 @@ impl std::fmt::Debug for Error { } } +impl Error { + #[cfg(windows)] + pub(crate) fn unknown>(message: M) -> Self { + Error::Unknown { description: message.into() } + } +} + /// Stores pixel data of an image. /// /// Each element in `bytes` stores the value of a channel of a single pixel. diff --git a/src/platform/windows.rs b/src/platform/windows.rs index bffa524..d229e94 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -8,345 +8,399 @@ the Apache 2.0 or the MIT license at the licensee's choice. The terms and conditions of the chosen license apply to this file. */ -use std::{borrow::Cow, marker::PhantomData, thread, time::Duration}; #[cfg(feature = "image-data")] -use std::{convert::TryInto, mem::size_of}; - -#[cfg(feature = "image-data")] -use winapi::{ - shared::minwindef::DWORD, - um::{ - errhandlingapi::GetLastError, - winbase::{GlobalLock, GlobalUnlock}, - wingdi::{ - CreateDIBitmap, DeleteObject, GetDIBits, LCS_sRGB, BITMAPINFO, BITMAPINFOHEADER, - BITMAPV5HEADER, BI_RGB, CBM_INIT, DIB_RGB_COLORS, LCS_GM_IMAGES, PROFILE_EMBEDDED, - PROFILE_LINKED, RGBQUAD, - }, - winnt::LONG, - winuser::{GetDC, SetClipboardData}, - }, -}; - +use crate::common::ImageData; use crate::common::{private, Error}; +use std::{borrow::Cow, marker::PhantomData, thread, time::Duration}; #[cfg(feature = "image-data")] -use crate::common::{ImageData, ScopeGuard}; - -#[cfg(feature = "image-data")] -fn add_cf_dibv5(_open_clipboard: OpenClipboard, image: ImageData) -> Result<(), Error> { - use std::intrinsics::copy_nonoverlapping; - use winapi::um::{ - winbase::{GlobalAlloc, GHND}, - wingdi::BI_BITFIELDS, - winuser::CF_DIBV5, +mod image_data { + use super::*; + use crate::common::ScopeGuard; + use std::{convert::TryInto, ffi::c_void, io, mem::size_of, ptr::copy_nonoverlapping}; + use windows_sys::Win32::{ + Foundation::HGLOBAL, + Graphics::Gdi::{ + CreateDIBitmap, DeleteObject, GetDC, GetDIBits, BITMAPINFO, BITMAPINFOHEADER, + BITMAPV5HEADER, BI_BITFIELDS, BI_RGB, CBM_INIT, DIB_RGB_COLORS, HBITMAP, HDC, + LCS_GM_IMAGES, RGBQUAD, + }, + System::{ + DataExchange::SetClipboardData, + Memory::{GlobalAlloc, GlobalLock, GlobalUnlock, GHND}, + Ole::CF_DIBV5, + }, }; - let header_size = size_of::(); - let header = BITMAPV5HEADER { - bV5Size: header_size as u32, - bV5Width: image.width as LONG, - bV5Height: image.height as LONG, - bV5Planes: 1, - bV5BitCount: 32, - bV5Compression: BI_BITFIELDS, - bV5SizeImage: (4 * image.width * image.height) as DWORD, - bV5XPelsPerMeter: 0, - bV5YPelsPerMeter: 0, - bV5ClrUsed: 0, - bV5ClrImportant: 0, - bV5RedMask: 0x00ff0000, - bV5GreenMask: 0x0000ff00, - bV5BlueMask: 0x000000ff, - bV5AlphaMask: 0xff000000, - bV5CSType: LCS_sRGB as u32, - // SAFETY: Windows ignores this field because `bV5CSType` is not set to `LCS_CALIBRATED_RGB`. - bV5Endpoints: unsafe { std::mem::zeroed() }, - bV5GammaRed: 0, - bV5GammaGreen: 0, - bV5GammaBlue: 0, - bV5Intent: LCS_GM_IMAGES as u32, // I'm not sure about this. - bV5ProfileData: 0, - bV5ProfileSize: 0, - bV5Reserved: 0, - }; - - // In theory we don't need to flip the image because we could just specify - // a negative height in the header, which according to the documentation, indicates that the - // image rows are in top-to-bottom order. HOWEVER: MS Word (and WordPad) cannot paste an image - // that has a negative height in its header. - let image = flip_v(image); - - let data_size = header_size + image.bytes.len(); - let hdata = unsafe { GlobalAlloc(GHND, data_size) }; - if hdata.is_null() { - return Err(Error::Unknown { - description: format!( - "Could not allocate global memory object. GlobalAlloc returned null at line {}.", - line!() - ), - }); - } - unsafe { - let data_ptr = GlobalLock(hdata) as *mut u8; - if data_ptr.is_null() { - return Err(Error::Unknown { - description: format!("Could not lock the global memory object at line {}", line!()), - }); - } - - let _unlock = ScopeGuard::new(|| { - let retval = GlobalUnlock(hdata); - if retval == 0 { - let lasterr = GetLastError(); - if lasterr != 0 { - log::error!("Failed calling GlobalUnlock when writing dibv5 data. Error code was 0x{:X}", lasterr); - } - } - }); - - copy_nonoverlapping::((&header) as *const _ as *const u8, data_ptr, header_size); - - // Not using the `add` function, because that has a restriction, that the result cannot overflow isize - let pixels_dst = (data_ptr as usize + header_size) as *mut u8; - copy_nonoverlapping::(image.bytes.as_ptr(), pixels_dst, image.bytes.len()); - - let dst_pixels_slice = std::slice::from_raw_parts_mut(pixels_dst, image.bytes.len()); - - // If the non-allocating version of the function failed, we need to assign the new bytes to - // the global allocation. - if let Cow::Owned(new_pixels) = rgba_to_win(dst_pixels_slice) { - // SAFETY: `data_ptr` is valid to write to and has no outstanding mutable borrows, and - // `new_pixels` will be the same length as the original bytes. - copy_nonoverlapping::(new_pixels.as_ptr(), data_ptr, new_pixels.len()) - } + fn last_error(message: &str) -> Error { + let os_error = io::Error::last_os_error(); + Error::unknown(format!("{}: {}", message, os_error)) } - unsafe { - if SetClipboardData(CF_DIBV5, hdata as _).is_null() { - DeleteObject(hdata as _); - return Err(Error::Unknown { - description: format!( - "Call to `SetClipboardData` returned NULL at line {}", - line!() - ), - }); - } - } + pub fn add_cf_dibv5(_open_clipboard: OpenClipboard, image: ImageData) -> Result<(), Error> { + // This constant is missing in windows-rs + // https://github.com/microsoft/windows-rs/issues/2711 + #[allow(non_upper_case_globals)] + const LCS_sRGB: u32 = 0x7352_4742; - Ok(()) -} - -#[cfg(feature = "image-data")] -fn read_cf_dibv5(dibv5: &[u8]) -> Result, Error> { - // The DIBV5 format is a BITMAPV5HEADER followed by the pixel data according to - // https://docs.microsoft.com/en-us/windows/win32/dataxchg/standard-clipboard-formats - - // so first let's get a pointer to the header - let header_size = size_of::(); - if dibv5.len() < header_size { - return Err(Error::Unknown { - description: "When reading the DIBV5 data, it contained fewer bytes than the BITMAPV5HEADER size. This is invalid.".into() - }); - } - let header = unsafe { &*(dibv5.as_ptr() as *const BITMAPV5HEADER) }; - - let has_profile = - header.bV5CSType as i32 == PROFILE_LINKED || header.bV5CSType as i32 == PROFILE_EMBEDDED; - - let pixel_data_start = if has_profile { - header.bV5ProfileData as isize + header.bV5ProfileSize as isize - } else { - header_size as isize - }; - - unsafe { - let image_bytes = dibv5.as_ptr().offset(pixel_data_start) as *const _; - let hdc = GetDC(std::ptr::null_mut()); - let hbitmap = CreateDIBitmap( - hdc, - header as *const BITMAPV5HEADER as *const _, - CBM_INIT, - image_bytes, - header as *const BITMAPV5HEADER as *const _, - DIB_RGB_COLORS, - ); - if hbitmap.is_null() { - return Err(Error::Unknown { - description: - "Failed to create the HBITMAP while reading DIBV5. CreateDIBitmap returned null" - .into(), - }); - } - // Now extract the pixels in a desired format - let w = header.bV5Width; - let h = header.bV5Height.abs(); - let result_size = w as usize * h as usize * 4; - - let mut result_bytes = Vec::::with_capacity(result_size); - - let mut output_header = BITMAPINFO { - bmiColors: [RGBQUAD { rgbRed: 0, rgbGreen: 0, rgbBlue: 0, rgbReserved: 0 }], - bmiHeader: BITMAPINFOHEADER { - biSize: size_of::() as u32, - biWidth: w, - biHeight: -h, - biBitCount: 32, - biPlanes: 1, - biCompression: BI_RGB, - biSizeImage: 0, - biXPelsPerMeter: 0, - biYPelsPerMeter: 0, - biClrUsed: 0, - biClrImportant: 0, - }, + let header_size = size_of::(); + let header = BITMAPV5HEADER { + bV5Size: header_size as u32, + bV5Width: image.width as i32, + bV5Height: image.height as i32, + bV5Planes: 1, + bV5BitCount: 32, + bV5Compression: BI_BITFIELDS, + bV5SizeImage: (4 * image.width * image.height) as u32, + bV5XPelsPerMeter: 0, + bV5YPelsPerMeter: 0, + bV5ClrUsed: 0, + bV5ClrImportant: 0, + bV5RedMask: 0x00ff0000, + bV5GreenMask: 0x0000ff00, + bV5BlueMask: 0x000000ff, + bV5AlphaMask: 0xff000000, + bV5CSType: LCS_sRGB, + // SAFETY: Windows ignores this field because `bV5CSType` is not set to `LCS_CALIBRATED_RGB`. + bV5Endpoints: unsafe { std::mem::zeroed() }, + bV5GammaRed: 0, + bV5GammaGreen: 0, + bV5GammaBlue: 0, + bV5Intent: LCS_GM_IMAGES as u32, // I'm not sure about this. + bV5ProfileData: 0, + bV5ProfileSize: 0, + bV5Reserved: 0, }; - let result = GetDIBits( + // In theory we don't need to flip the image because we could just specify + // a negative height in the header, which according to the documentation, indicates that the + // image rows are in top-to-bottom order. HOWEVER: MS Word (and WordPad) cannot paste an image + // that has a negative height in its header. + let image = flip_v(image); + + let data_size = header_size + image.bytes.len(); + let hdata = unsafe { global_alloc(data_size)? }; + unsafe { + let data_ptr = global_lock(hdata)?; + let _unlock = ScopeGuard::new(|| { + // If the memory object is unlocked after decrementing the lock count, the function + // returns zero and GetLastError returns NO_ERROR. If it fails, the return value is + // zero and GetLastError returns a value other than NO_ERROR. + if GlobalUnlock(hdata) == 0 { + let err = io::Error::last_os_error(); + if err.raw_os_error() != Some(0) { + log::error!("Failed calling GlobalUnlock when writing dibv5 data: {}", err); + } + } + }); + + copy_nonoverlapping::((&header) as *const _ as *const u8, data_ptr, header_size); + + // Not using the `add` function, because that has a restriction, that the result cannot overflow isize + let pixels_dst = (data_ptr as usize + header_size) as *mut u8; + copy_nonoverlapping::(image.bytes.as_ptr(), pixels_dst, image.bytes.len()); + + let dst_pixels_slice = std::slice::from_raw_parts_mut(pixels_dst, image.bytes.len()); + + // If the non-allocating version of the function failed, we need to assign the new bytes to + // the global allocation. + if let Cow::Owned(new_pixels) = rgba_to_win(dst_pixels_slice) { + // SAFETY: `data_ptr` is valid to write to and has no outstanding mutable borrows, and + // `new_pixels` will be the same length as the original bytes. + copy_nonoverlapping::(new_pixels.as_ptr(), data_ptr, new_pixels.len()) + } + } + + if unsafe { SetClipboardData(CF_DIBV5 as u32, hdata as _) } == 0 { + unsafe { DeleteObject(hdata as _) }; + Err(last_error("SetClipboardData failed with error")) + } else { + Ok(()) + } + } + + unsafe fn global_alloc(bytes: usize) -> Result { + let hdata = GlobalAlloc(GHND, bytes); + if hdata == 0 { + Err(last_error("Could not allocate global memory object")) + } else { + Ok(hdata) + } + } + + unsafe fn global_lock(hmem: HGLOBAL) -> Result<*mut u8, Error> { + let data_ptr = GlobalLock(hmem) as *mut u8; + if data_ptr.is_null() { + Err(last_error("Could not lock the global memory object")) + } else { + Ok(data_ptr) + } + } + + pub fn read_cf_dibv5(dibv5: &[u8]) -> Result, Error> { + // The DIBV5 format is a BITMAPV5HEADER followed by the pixel data according to + // https://docs.microsoft.com/en-us/windows/win32/dataxchg/standard-clipboard-formats + + // These constants are missing in windows-rs + const PROFILE_EMBEDDED: u32 = 0x4D42_4544; + const PROFILE_LINKED: u32 = 0x4C49_4E4B; + + // so first let's get a pointer to the header + let header_size = size_of::(); + if dibv5.len() < header_size { + return Err(Error::unknown("When reading the DIBV5 data, it contained fewer bytes than the BITMAPV5HEADER size. This is invalid.")); + } + let header = unsafe { &*(dibv5.as_ptr() as *const BITMAPV5HEADER) }; + + let has_profile = + header.bV5CSType == PROFILE_LINKED || header.bV5CSType == PROFILE_EMBEDDED; + + let pixel_data_start = if has_profile { + header.bV5ProfileData as isize + header.bV5ProfileSize as isize + } else { + header_size as isize + }; + + unsafe { + let image_bytes = dibv5.as_ptr().offset(pixel_data_start) as *const _; + let hdc = get_screen_device_context()?; + let hbitmap = create_bitmap_from_dib(hdc, header as _, image_bytes)?; + // Now extract the pixels in a desired format + let w = header.bV5Width; + let h = header.bV5Height.abs(); + let result_size = w as usize * h as usize * 4; + + let mut result_bytes = Vec::::with_capacity(result_size); + + let mut output_header = BITMAPINFO { + bmiColors: [RGBQUAD { rgbRed: 0, rgbGreen: 0, rgbBlue: 0, rgbReserved: 0 }], + bmiHeader: BITMAPINFOHEADER { + biSize: size_of::() as u32, + biWidth: w, + biHeight: -h, + biBitCount: 32, + biPlanes: 1, + biCompression: BI_RGB as u32, + biSizeImage: 0, + biXPelsPerMeter: 0, + biYPelsPerMeter: 0, + biClrUsed: 0, + biClrImportant: 0, + }, + }; + + let lines = convert_bitmap_to_rgb( + hdc, + hbitmap, + h as _, + result_bytes.as_mut_ptr() as _, + &mut output_header as _, + )?; + let read_len = lines as usize * w as usize * 4; + assert!( + read_len <= result_bytes.capacity(), + "Segmentation fault. Read more bytes than allocated to pixel buffer", + ); + result_bytes.set_len(read_len); + + let result_bytes = win_to_rgba(&mut result_bytes); + + let result = ImageData { + bytes: Cow::Owned(result_bytes), + width: w as usize, + height: h as usize, + }; + Ok(result) + } + } + + fn get_screen_device_context() -> Result { + // SAFETY: Calling `GetDC` with `NULL` is safe. + let hdc = unsafe { GetDC(0) }; + if hdc == 0 { + Err(Error::unknown("Failed to get the device context. GetDC returned null")) + } else { + Ok(hdc) + } + } + + unsafe fn create_bitmap_from_dib( + hdc: HDC, + header: *const BITMAPV5HEADER, + image_bytes: *const c_void, + ) -> Result { + let hbitmap = CreateDIBitmap( hdc, - hbitmap, - 0, - h as u32, - result_bytes.as_mut_ptr() as *mut _, - &mut output_header as *mut _, + header as _, + CBM_INIT as u32, + image_bytes, + header as _, DIB_RGB_COLORS, ); - if result == 0 { - return Err(Error::Unknown { - description: "Could not get the bitmap bits, GetDIBits returned 0".into(), - }); - } - let read_len = result as usize * w as usize * 4; - if read_len > result_bytes.capacity() { - panic!("Segmentation fault. Read more bytes than allocated to pixel buffer"); - } - result_bytes.set_len(read_len); - - let result_bytes = win_to_rgba(&mut result_bytes); - - let result = - ImageData { bytes: Cow::Owned(result_bytes), width: w as usize, height: h as usize }; - Ok(result) - } -} - -/// Converts the RGBA (u8) pixel data into the bitmap-native ARGB (u32) format in-place -/// -/// Safety: the `bytes` slice must have a length that's a multiple of 4 -#[cfg(feature = "image-data")] -#[allow(clippy::identity_op, clippy::erasing_op)] -#[must_use] -unsafe fn rgba_to_win(bytes: &mut [u8]) -> Cow<'_, [u8]> { - // Check safety invariants to catch obvious bugs. - debug_assert_eq!(bytes.len() % 4, 0); - - let mut u32pixels_buffer = convert_bytes_to_u32s(bytes); - let u32pixels = match u32pixels_buffer { - ImageDataCow::Borrowed(ref mut b) => b, - ImageDataCow::Owned(ref mut b) => b.as_mut_slice(), - }; - - for p in u32pixels.iter_mut() { - let [mut r, mut g, mut b, mut a] = p.to_ne_bytes().map(u32::from); - r <<= 2 * 8; - g <<= 1 * 8; - b <<= 0 * 8; - a <<= 3 * 8; - - *p = r | g | b | a; - } - - match u32pixels_buffer { - ImageDataCow::Borrowed(_) => Cow::Borrowed(bytes), - ImageDataCow::Owned(bytes) => { - Cow::Owned(bytes.into_iter().flat_map(|b| b.to_ne_bytes()).collect()) + if hbitmap == 0 { + Err(Error::unknown( + "Failed to create the HBITMAP while reading DIBV5. CreateDIBitmap returned null", + )) + } else { + Ok(hbitmap) } } -} -/// Vertically flips the image pixels in memory -#[cfg(feature = "image-data")] -fn flip_v(image: ImageData) -> ImageData<'static> { - let w = image.width; - let h = image.height; - - let mut bytes = image.bytes.into_owned(); - - let rowsize = w * 4; // each pixel is 4 bytes - let mut tmp_a = vec![0; rowsize]; - // I believe this could be done safely with `as_chunks_mut`, but that's not stable yet - for a_row_id in 0..(h / 2) { - let b_row_id = h - a_row_id - 1; - - // swap rows `first_id` and `second_id` - let a_byte_start = a_row_id * rowsize; - let a_byte_end = a_byte_start + rowsize; - let b_byte_start = b_row_id * rowsize; - let b_byte_end = b_byte_start + rowsize; - tmp_a.copy_from_slice(&bytes[a_byte_start..a_byte_end]); - bytes.copy_within(b_byte_start..b_byte_end, a_byte_start); - bytes[b_byte_start..b_byte_end].copy_from_slice(&tmp_a); + /// Copies the bitmap image into given buffer with DIB RGB format and + /// returns the number of scan lines copied from the bitmap. + unsafe fn convert_bitmap_to_rgb( + hdc: HDC, + hbitmap: HBITMAP, + lines: u32, + dst: *mut c_void, + header: *mut BITMAPINFO, + ) -> Result { + let lines = GetDIBits(hdc, hbitmap, 0, lines, dst, header, DIB_RGB_COLORS); + if lines == 0 { + Err(Error::unknown("Could not get the bitmap bits, GetDIBits returned 0")) + } else { + Ok(lines) + } } - ImageData { width: image.width, height: image.height, bytes: bytes.into() } -} + /// Converts the RGBA (u8) pixel data into the bitmap-native ARGB (u32) + /// format in-place. + /// + /// Safety: the `bytes` slice must have a length that's a multiple of 4 + #[allow(clippy::identity_op, clippy::erasing_op)] + #[must_use] + unsafe fn rgba_to_win(bytes: &mut [u8]) -> Cow<'_, [u8]> { + // Check safety invariants to catch obvious bugs. + debug_assert_eq!(bytes.len() % 4, 0); -/// Converts the ARGB (u32) pixel data into the RGBA (u8) format in-place -/// -/// Safety: the `bytes` slice must have a length that's a multiple of 4 -#[cfg(feature = "image-data")] -#[allow(clippy::identity_op, clippy::erasing_op)] -#[must_use] -unsafe fn win_to_rgba(bytes: &mut [u8]) -> Vec { - // Check safety invariants to catch obvious bugs. - debug_assert_eq!(bytes.len() % 4, 0); + let mut u32pixels_buffer = convert_bytes_to_u32s(bytes); + let u32pixels = match u32pixels_buffer { + ImageDataCow::Borrowed(ref mut b) => b, + ImageDataCow::Owned(ref mut b) => b.as_mut_slice(), + }; - let mut u32pixels_buffer = convert_bytes_to_u32s(bytes); - let u32pixels = match u32pixels_buffer { - ImageDataCow::Borrowed(ref mut b) => b, - ImageDataCow::Owned(ref mut b) => b.as_mut_slice(), - }; + for p in u32pixels.iter_mut() { + let [mut r, mut g, mut b, mut a] = p.to_ne_bytes().map(u32::from); + r <<= 2 * 8; + g <<= 1 * 8; + b <<= 0 * 8; + a <<= 3 * 8; - for p in u32pixels { - let mut bytes = p.to_ne_bytes(); - bytes[0] = (*p >> (2 * 8)) as u8; - bytes[1] = (*p >> (1 * 8)) as u8; - bytes[2] = (*p >> (0 * 8)) as u8; - bytes[3] = (*p >> (3 * 8)) as u8; - *p = u32::from_ne_bytes(bytes); + *p = r | g | b | a; + } + + match u32pixels_buffer { + ImageDataCow::Borrowed(_) => Cow::Borrowed(bytes), + ImageDataCow::Owned(bytes) => { + Cow::Owned(bytes.into_iter().flat_map(|b| b.to_ne_bytes()).collect()) + } + } } - match u32pixels_buffer { - ImageDataCow::Borrowed(_) => bytes.to_vec(), - ImageDataCow::Owned(bytes) => bytes.into_iter().flat_map(|b| b.to_ne_bytes()).collect(), + /// Vertically flips the image pixels in memory + fn flip_v(image: ImageData) -> ImageData<'static> { + let w = image.width; + let h = image.height; + + let mut bytes = image.bytes.into_owned(); + + let rowsize = w * 4; // each pixel is 4 bytes + let mut tmp_a = vec![0; rowsize]; + // I believe this could be done safely with `as_chunks_mut`, but that's not stable yet + for a_row_id in 0..(h / 2) { + let b_row_id = h - a_row_id - 1; + + // swap rows `first_id` and `second_id` + let a_byte_start = a_row_id * rowsize; + let a_byte_end = a_byte_start + rowsize; + let b_byte_start = b_row_id * rowsize; + let b_byte_end = b_byte_start + rowsize; + tmp_a.copy_from_slice(&bytes[a_byte_start..a_byte_end]); + bytes.copy_within(b_byte_start..b_byte_end, a_byte_start); + bytes[b_byte_start..b_byte_end].copy_from_slice(&tmp_a); + } + + ImageData { width: image.width, height: image.height, bytes: bytes.into() } } -} -#[cfg(feature = "image-data")] -// XXX: std's Cow is not usable here because it does not allow mutably -// borrowing data. -enum ImageDataCow<'a> { - Borrowed(&'a mut [u32]), - Owned(Vec), -} + /// Converts the ARGB (u32) pixel data into the RGBA (u8) format in-place + /// + /// Safety: the `bytes` slice must have a length that's a multiple of 4 + #[allow(clippy::identity_op, clippy::erasing_op)] + #[must_use] + unsafe fn win_to_rgba(bytes: &mut [u8]) -> Vec { + // Check safety invariants to catch obvious bugs. + debug_assert_eq!(bytes.len() % 4, 0); -/// Safety: the `bytes` slice must have a length that's a multiple of 4 -#[cfg(feature = "image-data")] -unsafe fn convert_bytes_to_u32s(bytes: &mut [u8]) -> ImageDataCow<'_> { - // When the correct conditions are upheld, `std` should return everything in the well-aligned slice. - let (prefix, _, suffix) = bytes.align_to::(); + let mut u32pixels_buffer = convert_bytes_to_u32s(bytes); + let u32pixels = match u32pixels_buffer { + ImageDataCow::Borrowed(ref mut b) => b, + ImageDataCow::Owned(ref mut b) => b.as_mut_slice(), + }; - // Check if `align_to` gave us the optimal result. - // - // If it didn't, use the slow path with more allocations - if prefix.is_empty() && suffix.is_empty() { - // We know that the newly-aligned slice will contain all the values - ImageDataCow::Borrowed(bytes.align_to_mut::().1) - } else { - // XXX: Use `as_chunks` when it stabilizes. - let u32pixels_buffer = - bytes.chunks(4).map(|chunk| u32::from_ne_bytes(chunk.try_into().unwrap())).collect(); - ImageDataCow::Owned(u32pixels_buffer) + for p in u32pixels { + let mut bytes = p.to_ne_bytes(); + bytes[0] = (*p >> (2 * 8)) as u8; + bytes[1] = (*p >> (1 * 8)) as u8; + bytes[2] = (*p >> (0 * 8)) as u8; + bytes[3] = (*p >> (3 * 8)) as u8; + *p = u32::from_ne_bytes(bytes); + } + + match u32pixels_buffer { + ImageDataCow::Borrowed(_) => bytes.to_vec(), + ImageDataCow::Owned(bytes) => bytes.into_iter().flat_map(|b| b.to_ne_bytes()).collect(), + } + } + + // XXX: std's Cow is not usable here because it does not allow mutably + // borrowing data. + enum ImageDataCow<'a> { + Borrowed(&'a mut [u32]), + Owned(Vec), + } + + /// Safety: the `bytes` slice must have a length that's a multiple of 4 + unsafe fn convert_bytes_to_u32s(bytes: &mut [u8]) -> ImageDataCow<'_> { + // When the correct conditions are upheld, `std` should return everything in the well-aligned slice. + let (prefix, _, suffix) = bytes.align_to::(); + + // Check if `align_to` gave us the optimal result. + // + // If it didn't, use the slow path with more allocations + if prefix.is_empty() && suffix.is_empty() { + // We know that the newly-aligned slice will contain all the values + ImageDataCow::Borrowed(bytes.align_to_mut::().1) + } else { + // XXX: Use `as_chunks` when it stabilizes. + let u32pixels_buffer = bytes + .chunks(4) + .map(|chunk| u32::from_ne_bytes(chunk.try_into().unwrap())) + .collect(); + ImageDataCow::Owned(u32pixels_buffer) + } + } + + #[test] + fn conversion_between_win_and_rgba() { + const DATA: [u8; 16] = + [100, 100, 255, 100, 0, 0, 0, 255, 255, 100, 100, 255, 100, 255, 100, 100]; + + let mut data = DATA; + let _converted = unsafe { win_to_rgba(&mut data) }; + + let mut data = DATA; + let _converted = unsafe { rgba_to_win(&mut data) }; + + let mut data = DATA; + let _converted = unsafe { win_to_rgba(&mut data) }; + let _converted = unsafe { rgba_to_win(&mut data) }; + assert_eq!(data, DATA); + + let mut data = DATA; + let _converted = unsafe { rgba_to_win(&mut data) }; + let _converted = unsafe { win_to_rgba(&mut data) }; + assert_eq!(data, DATA); } } @@ -436,9 +490,8 @@ impl<'clipboard> Get<'clipboard> { return Err(Error::ContentNotAvailable); } - let text_size = clipboard_win::raw::size(FORMAT).ok_or_else(|| Error::Unknown { - description: "failed to read clipboard text size".into(), - })?; + let text_size = clipboard_win::raw::size(FORMAT) + .ok_or_else(|| Error::unknown("failed to read clipboard text size"))?; // Allocate the specific number of WTF-16 characters we need to receive. // This division is always accurate because Windows uses 16-bit characters. @@ -449,9 +502,8 @@ impl<'clipboard> Get<'clipboard> { let out: &mut [u8] = unsafe { std::slice::from_raw_parts_mut(out.as_mut_ptr().cast(), out.len() * 2) }; - let mut bytes_read = clipboard_win::raw::get(FORMAT, out).map_err(|_| { - Error::Unknown { description: "failed to read clipboard string".into() } - })?; + let mut bytes_read = clipboard_win::raw::get(FORMAT, out) + .map_err(|_| Error::unknown("failed to read clipboard string"))?; // Convert the number of bytes read to the number of `u16`s bytes_read /= 2; @@ -482,11 +534,10 @@ impl<'clipboard> Get<'clipboard> { let mut data = Vec::new(); - clipboard_win::raw::get_vec(FORMAT, &mut data).map_err(|_| Error::Unknown { - description: "failed to read clipboard image data".into(), - })?; + clipboard_win::raw::get_vec(FORMAT, &mut data) + .map_err(|_| Error::unknown("failed to read clipboard image data"))?; - read_cf_dibv5(&data) + image_data::read_cf_dibv5(&data) } } @@ -510,9 +561,8 @@ impl<'clipboard> Set<'clipboard> { pub(crate) fn text(self, data: Cow<'_, str>) -> Result<(), Error> { let open_clipboard = self.clipboard?; - clipboard_win::raw::set_string(&data).map_err(|_| Error::Unknown { - description: "Could not place the specified text to the clipboard".into(), - })?; + clipboard_win::raw::set_string(&data) + .map_err(|_| Error::unknown("Could not place the specified text to the clipboard"))?; add_clipboard_exclusions( open_clipboard, @@ -529,14 +579,13 @@ impl<'clipboard> Set<'clipboard> { Some(s) => s.into(), None => String::new(), }; - clipboard_win::raw::set_string(&alt).map_err(|_| Error::Unknown { - description: "Could not place the specified text to the clipboard".into(), - })?; + clipboard_win::raw::set_string(&alt) + .map_err(|_| Error::unknown("Could not place the specified text to the clipboard"))?; if let Some(format) = clipboard_win::register_format("HTML Format") { let html = wrap_html(&html); clipboard_win::raw::set_without_clear(format.get(), html.as_bytes()) - .map_err(|e| Error::Unknown { description: e.to_string() })?; + .map_err(|e| Error::unknown(e.to_string()))?; } add_clipboard_exclusions( @@ -552,12 +601,12 @@ impl<'clipboard> Set<'clipboard> { let open_clipboard = self.clipboard?; if let Err(e) = clipboard_win::raw::empty() { - return Err(Error::Unknown { - description: format!("Failed to empty the clipboard. Got error code: {e}"), - }); + return Err(Error::unknown(format!( + "Failed to empty the clipboard. Got error code: {e}" + ))); }; - add_cf_dibv5(open_clipboard, image) + image_data::add_cf_dibv5(open_clipboard, image) } } @@ -582,11 +631,8 @@ fn add_clipboard_exclusions( { // The documentation states "place any data on the clipboard in this format to prevent...", and using the zero bytes // like the others for consistency works. - clipboard_win::raw::set_without_clear(format.get(), CLIPBOARD_EXCLUSION_DATA).map_err( - |_| Error::Unknown { - description: "Failed to exclude data from clipboard monitoring".into(), - }, - )?; + clipboard_win::raw::set_without_clear(format.get(), CLIPBOARD_EXCLUSION_DATA) + .map_err(|_| Error::unknown("Failed to exclude data from clipboard monitoring"))?; } } @@ -595,22 +641,16 @@ fn add_clipboard_exclusions( // We believe that it would be a logic error if this call failed, since we've validated the format is supported, // we still have full ownership of the clipboard and aren't moving it to another thread, and this is a well-documented operation. // Due to these reasons, `Error::Unknown` is used because we never expect the error path to be taken. - clipboard_win::raw::set_without_clear(format.get(), CLIPBOARD_EXCLUSION_DATA).map_err( - |_| Error::Unknown { - description: "Failed to exclude data from cloud clipboard".into(), - }, - )?; + clipboard_win::raw::set_without_clear(format.get(), CLIPBOARD_EXCLUSION_DATA) + .map_err(|_| Error::unknown("Failed to exclude data from cloud clipboard"))?; } } if exclude_from_history { if let Some(format) = clipboard_win::register_format("CanIncludeInClipboardHistory") { // See above for reasoning about using `Error::Unknown`. - clipboard_win::raw::set_without_clear(format.get(), CLIPBOARD_EXCLUSION_DATA).map_err( - |_| Error::Unknown { - description: "Failed to exclude data from clipboard history".into(), - }, - )?; + clipboard_win::raw::set_without_clear(format.get(), CLIPBOARD_EXCLUSION_DATA) + .map_err(|_| Error::unknown("Failed to exclude data from clipboard history"))?; } } @@ -666,8 +706,7 @@ impl<'clipboard> Clear<'clipboard> { pub(crate) fn clear(self) -> Result<(), Error> { let _clipboard_assertion = self.clipboard?; - clipboard_win::empty() - .map_err(|_| Error::Unknown { description: "failed to clear clipboard".into() }) + clipboard_win::empty().map_err(|_| Error::unknown("failed to clear clipboard")) } } @@ -705,23 +744,3 @@ fn wrap_html(ctn: &str) -> String { c_end_frag, ) } - -#[cfg(all(test, feature = "image-data"))] -mod tests { - use super::{rgba_to_win, win_to_rgba}; - - const DATA: [u8; 16] = - [100, 100, 255, 100, 0, 0, 0, 255, 255, 100, 100, 255, 100, 255, 100, 100]; - - #[test] - fn check_win_to_rgba_conversion() { - let mut data = DATA; - let _converted = unsafe { win_to_rgba(&mut data) }; - } - - #[test] - fn check_rgba_to_win_conversion() { - let mut data = DATA; - let _converted = unsafe { rgba_to_win(&mut data) }; - } -} From 0d6725d97f91064e90a7c238ac6a311c0a06ee29 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Fri, 29 Dec 2023 18:03:00 +0000 Subject: [PATCH 13/82] Spell check docs --- src/common.rs | 2 +- src/lib.rs | 10 +++++----- src/platform/linux/x11.rs | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/common.rs b/src/common.rs index 3b51c82..f115c16 100644 --- a/src/common.rs +++ b/src/common.rs @@ -50,7 +50,7 @@ pub enum Error { /// /// - When returned from `set_image`: the image going to the clipboard cannot be converted to the appropriate format. /// - When returned from `get_image`: the image coming from the clipboard could not be converted into the `ImageData` struct. - /// - When returned from `get_text`: the text coming from the clipboard is not valid utf-8 or cannot be converted to utf-8. + /// - When returned from `get_text`: the text coming from the clipboard is not valid UTF-8 or cannot be converted to UTF-8. #[error("The image or the text that was about the be transferred to/from the clipboard could not be converted to the appropriate format.")] ConversionFailure, diff --git a/src/lib.rs b/src/lib.rs index fbbd0b8..48c5750 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -56,7 +56,7 @@ pub use platform::SetExtWindows; /// The clipboard on Windows is a global object, which may only be opened on one thread at once. /// This means that `arboard` only truly opens the clipboard during each operation to ensure that /// multiple `Clipboard`'s may exist at once. This also means that attempting operations in parallel -/// has a high likelyhood to return an error instead. +/// has a high likelihood to return an error instead. #[allow(rustdoc::broken_intra_doc_links)] pub struct Clipboard { pub(crate) platform: platform::Clipboard, @@ -68,19 +68,19 @@ impl Clipboard { Ok(Clipboard { platform: platform::Clipboard::new()? }) } - /// Fetches utf-8 text from the clipboard and returns it. + /// Fetches UTF-8 text from the clipboard and returns it. pub fn get_text(&mut self) -> Result { self.get().text() } - /// Places the text onto the clipboard. Any valid utf-8 string is accepted. + /// Places the text onto the clipboard. Any valid UTF-8 string is accepted. pub fn set_text<'a, T: Into>>(&mut self, text: T) -> Result<(), Error> { self.set().text(text) } /// Places the HTML as well as a plain-text alternative onto the clipboard. /// - /// Any valid utf-8 string is accepted. + /// Any valid UTF-8 string is accepted. pub fn set_html<'a, T: Into>>( &mut self, html: T, @@ -304,7 +304,7 @@ mod tests { ctx.set_text("clipboard test").unwrap(); assert!(matches!(ctx.get_image(), Err(Error::ContentNotAvailable))); - // Test if we get the same image that we put onto the clibboard + // Test if we get the same image that we put onto the clipboard ctx.set_image(img_data.clone()).unwrap(); let got = ctx.get_image().unwrap(); assert_eq!(img_data.bytes, got.bytes); diff --git a/src/platform/linux/x11.rs b/src/platform/linux/x11.rs index c983ff1..8f3750c 100644 --- a/src/platform/linux/x11.rs +++ b/src/platform/linux/x11.rs @@ -724,7 +724,7 @@ fn serve_requests(context: Arc) -> Result<(), Box> log::trace!("Finishing clipboard manager handover."); *handover_state = ManagerHandoverState::Finished; - // Not sure if unlocking the mutext is necessary here but better safe than sorry. + // Not sure if unlocking the mutex is necessary here but better safe than sorry. drop(handover_state); clip.handover_cv.notify_all(); From a648570ce9c8d7051546f93df4b06cd6e82e9e01 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Fri, 29 Dec 2023 17:55:18 +0000 Subject: [PATCH 14/82] Update CI actions --- .github/workflows/test.yml | 70 ++++++++++---------------------------- 1 file changed, 18 insertions(+), 52 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0c92da7..1953531 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,17 +10,13 @@ jobs: rustfmt: runs-on: ubuntu-20.04 steps: - - uses: actions-rs/toolchain@v1 + - uses: actions-rust-lang/setup-rust-toolchain@v1 with: toolchain: stable - override: true components: rustfmt - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Check formatting - uses: actions-rs/cargo@v1 - with: - command: fmt - args: --all -- --check + run: cargo fmt --all -- --check clippy: needs: rustfmt @@ -32,39 +28,26 @@ jobs: # for the MSRV build to keep CI fast, since other configurations should also work. rust_version: [stable, "1.61"] steps: - - uses: actions-rs/toolchain@v1 + - uses: actions-rust-lang/setup-rust-toolchain@v1 with: toolchain: stable - override: true components: clippy - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Run `cargo clippy` with no features if: ${{ matrix.rust_version == 'stable' }} - uses: actions-rs/cargo@v1 - with: - command: clippy - args: --verbose --no-default-features -- -D warnings -D clippy::dbg_macro + run: cargo clippy --verbose --no-default-features -- -D warnings -D clippy::dbg_macro - name: Run `cargo clippy` with `image-data` feature if: ${{ matrix.rust_version == 'stable' }} - uses: actions-rs/cargo@v1 - with: - command: clippy - args: --verbose --no-default-features --features image-data -- -D warnings -D clippy::dbg_macro + run: cargo clippy --verbose --no-default-features --features image-data -- -D warnings -D clippy::dbg_macro - name: Run `cargo clippy` with `wayland-data-control` feature if: ${{ matrix.rust_version == 'stable' }} - uses: actions-rs/cargo@v1 - with: - command: clippy - args: --verbose --no-default-features --features wayland-data-control -- -D warnings -D clippy::dbg_macro + run: cargo clippy --verbose --no-default-features --features wayland-data-control -- -D warnings -D clippy::dbg_macro - name: Run `cargo clippy` with all features - uses: actions-rs/cargo@v1 - with: - command: clippy - args: --verbose --all-features -- -D warnings -D clippy::dbg_macro + run: cargo clippy --verbose --all-features -- -D warnings -D clippy::dbg_macro test: needs: clippy @@ -74,32 +57,19 @@ jobs: # No Linux test for now as it just fails due to not having a desktop environment. os: [macos-latest, windows-latest] steps: - - uses: actions-rs/toolchain@v1 + - uses: actions-rust-lang/setup-rust-toolchain@v1 with: toolchain: stable - override: true - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Run tests with no features - uses: actions-rs/cargo@v1 - with: - command: test - args: --no-default-features + run: cargo test --no-default-features - name: Run tests with `image-data` feature - uses: actions-rs/cargo@v1 - with: - command: test - args: --no-default-features --features image-data + run: cargo test --no-default-features --features image-data - name: Run tests with `wayland-data-control` feature - uses: actions-rs/cargo@v1 - with: - command: test - args: --no-default-features --features wayland-data-control + run: cargo test --no-default-features --features wayland-data-control - name: Run tests with all features - uses: actions-rs/cargo@v1 - with: - command: test - args: --all-features + run: cargo test --all-features miri: needs: clippy @@ -111,17 +81,13 @@ jobs: # Currently, only Windows has soundness tests. os: [windows-latest] steps: - - uses: actions-rs/toolchain@v1 + - uses: actions-rust-lang/setup-rust-toolchain@v1 with: toolchain: nightly-2023-10-08 - override: true components: miri - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Check soundness - uses: actions-rs/cargo@v1 - with: - command: miri - args: test windows --features image-data + run: cargo miri test windows --features image-data From bd91f9c438294cf894dce45051fb48c0759705dc Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Fri, 29 Dec 2023 18:40:51 +0000 Subject: [PATCH 15/82] Increase error documentation on Clipboard type --- src/common.rs | 7 ++----- src/lib.rs | 43 ++++++++++++++++++++++++++++++++++++++----- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/src/common.rs b/src/common.rs index f115c16..2a96706 100644 --- a/src/common.rs +++ b/src/common.rs @@ -46,11 +46,8 @@ pub enum Error { #[error("The native clipboard is not accessible due to being held by an other party.")] ClipboardOccupied, - /// This can happen in either of the following cases. - /// - /// - When returned from `set_image`: the image going to the clipboard cannot be converted to the appropriate format. - /// - When returned from `get_image`: the image coming from the clipboard could not be converted into the `ImageData` struct. - /// - When returned from `get_text`: the text coming from the clipboard is not valid UTF-8 or cannot be converted to UTF-8. + /// The image or the text that was about the be transferred to/from the clipboard could not be + /// converted to the appropriate format. #[error("The image or the text that was about the be transferred to/from the clipboard could not be converted to the appropriate format.")] ConversionFailure, diff --git a/src/lib.rs b/src/lib.rs index 48c5750..e3a7a45 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,7 +42,7 @@ pub use platform::SetExtWindows; /// /// `arboard` does its best to abstract over different platforms, but sometimes the platform-specific /// behavior leaks through unsolvably. These differences, depending on which platforms are being targeted, -/// may affect your app's clipboard architecture (ex, opening and closing a [Clipboard] every time +/// may affect your app's clipboard architecture (ex, opening and closing a [`Clipboard`] every time /// or keeping one open in some application/global state). /// /// ## Linux @@ -54,26 +54,41 @@ pub use platform::SetExtWindows; /// ## Windows /// /// The clipboard on Windows is a global object, which may only be opened on one thread at once. -/// This means that `arboard` only truly opens the clipboard during each operation to ensure that -/// multiple `Clipboard`'s may exist at once. This also means that attempting operations in parallel -/// has a high likelihood to return an error instead. +/// This means that `arboard` only truly opens the clipboard during each operation to prevent +/// multiple `Clipboard`s from existing at once. +/// +/// This means that attempting operations in parallel has a high likelihood to return an error or +/// deadlock. As such, it is recommended to avoid creating/operating clipboard objects on >1 thread. #[allow(rustdoc::broken_intra_doc_links)] pub struct Clipboard { pub(crate) platform: platform::Clipboard, } impl Clipboard { - /// Creates an instance of the clipboard + /// Creates an instance of the clipboard. + /// + /// # Errors + /// + /// On some platforms or desktop environments, an error can be returned if clipboards are not + /// supported. This may be retried. pub fn new() -> Result { Ok(Clipboard { platform: platform::Clipboard::new()? }) } /// Fetches UTF-8 text from the clipboard and returns it. + /// + /// # Errors + /// + /// Returns error if clipboard is empty or contents are not UTF-8 text. pub fn get_text(&mut self) -> Result { self.get().text() } /// Places the text onto the clipboard. Any valid UTF-8 string is accepted. + /// + /// # Errors + /// + /// Returns error if `text` failed to be stored on the clipboard. pub fn set_text<'a, T: Into>>(&mut self, text: T) -> Result<(), Error> { self.set().text(text) } @@ -81,6 +96,10 @@ impl Clipboard { /// Places the HTML as well as a plain-text alternative onto the clipboard. /// /// Any valid UTF-8 string is accepted. + /// + /// # Errors + /// + /// Returns error if both `html` and `alt_text` failed to be stored on the clipboard. pub fn set_html<'a, T: Into>>( &mut self, html: T, @@ -94,6 +113,11 @@ impl Clipboard { /// Any image data placed on the clipboard with `set_image` will be possible read back, using /// this function. However it's of not guaranteed that an image placed on the clipboard by any /// other application will be of a supported format. + /// + /// # Errors + /// + /// Returns error if clipboard is empty, contents are not an image, or the contents cannot be + /// converted to an appropriate format and stored in the [`ImageData`] type. #[cfg(feature = "image-data")] pub fn get_image(&mut self) -> Result, Error> { self.get().image() @@ -106,6 +130,11 @@ impl Clipboard { /// - On macOS: `NSImage` object /// - On Linux: PNG, under the atom `image/png` /// - On Windows: In order of priority `CF_DIB` and `CF_BITMAP` + /// + /// # Errors + /// + /// Returns error if `image` cannot be converted to an appropriate format or if it failed to be + /// stored on the clipboard. #[cfg(feature = "image-data")] pub fn set_image(&mut self, image: ImageData) -> Result<(), Error> { self.set().image(image) @@ -113,6 +142,10 @@ impl Clipboard { /// Clears any contents that may be present from the platform's default clipboard, /// regardless of the format of the data. + /// + /// # Errors + /// + /// Returns error on Windows or Linux if clipboard cannot be cleared. pub fn clear(&mut self) -> Result<(), Error> { self.clear_with().default() } From 409bd98978940b1a14a2280bea4b68800f353668 Mon Sep 17 00:00:00 2001 From: Magnus Larsen Date: Thu, 8 Feb 2024 13:13:50 -0800 Subject: [PATCH 16/82] Update x11rb to 0.13 and core-graphics to 0.23 --- Cargo.lock | 92 +++++++++++++++++++++++++++++------------------------- Cargo.toml | 4 +-- 2 files changed, 52 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4f90e16..fdf356a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -118,9 +118,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "core-foundation" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", @@ -128,15 +128,15 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "core-graphics" -version = "0.22.3" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" +checksum = "970a29baf4110c26fedbc7f82107d42c23f7e88e404c4577ed73fe99ff85a212" dependencies = [ "bitflags 1.3.2", "core-foundation", @@ -147,13 +147,12 @@ dependencies = [ [[package]] name = "core-graphics-types" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a68b68b3446082644c91ac778bf50cd4104bfb002b5a6a7c44cca5a2c70788b" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" dependencies = [ "bitflags 1.3.2", "core-foundation", - "foreign-types", "libc", ] @@ -174,7 +173,7 @@ checksum = "3418329ca0ad70234b9735dc4ceed10af4df60eff9c8e7b06cb5e520d92c3535" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.100", ] [[package]] @@ -262,27 +261,39 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foreign-types" -version = "0.3.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ + "foreign-types-macros", "foreign-types-shared", ] [[package]] -name = "foreign-types-shared" -version = "0.1.1" +name = "foreign-types-macros" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" [[package]] name = "gethostname" -version = "0.3.0" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb65d4ba3173c56a500b555b532f72c42e8d1fe64962b518897f8959fae2c177" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" dependencies = [ "libc", - "winapi", + "windows-targets", ] [[package]] @@ -577,9 +588,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.43" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] @@ -595,9 +606,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.21" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -670,6 +681,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "tempfile" version = "3.8.0" @@ -709,7 +731,7 @@ checksum = "f8b463991b4eab2d801e724172285ec4195c650e8ec79b149e6c2a8e6dd3f783" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.100", ] [[package]] @@ -847,15 +869,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "winapi-wsapoll" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c17110f57155602a80dca10be03852116403c9ff3cd25b079d666f2aa3df6e" -dependencies = [ - "winapi", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -950,22 +963,17 @@ dependencies = [ [[package]] name = "x11rb" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1641b26d4dec61337c35a1b1aaf9e3cba8f46f0b43636c609ab0291a648040a" +checksum = "f8f25ead8c7e4cba123243a6367da5d3990e0d3affa708ea19dce96356bd9f1a" dependencies = [ "gethostname", - "nix", - "winapi", - "winapi-wsapoll", + "rustix", "x11rb-protocol", ] [[package]] name = "x11rb-protocol" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d6c3f9a0fb6701fab8f6cea9b0c0bd5d6876f1f89f7fada07e558077c344bc" -dependencies = [ - "nix", -] +checksum = "e63e71c4b8bd9ffec2c963173a4dc4cbde9ee96961d4fcb4429db9929b606c34" diff --git a/Cargo.toml b/Cargo.toml index f18e976..0334bff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,12 +36,12 @@ log = "0.4" objc = "0.2" objc_id = "0.1" objc-foundation = "0.1" -core-graphics = { version = "0.22", optional = true } +core-graphics = { version = "0.23", optional = true } image = { version = "0.24", optional = true, default-features = false, features = ["tiff"] } [target.'cfg(all(unix, not(any(target_os="macos", target_os="android", target_os="emscripten"))))'.dependencies] log = "0.4" -x11rb = { version = "0.12" } +x11rb = { version = "0.13" } wl-clipboard-rs = { version = "0.8", optional = true } image = { version = "0.24", optional = true, default-features = false, features = ["png"] } parking_lot = "0.12" From 77e0e078eb460ac2fa0eda96124163c11ef6b2d1 Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Mon, 12 Feb 2024 17:41:32 -0600 Subject: [PATCH 17/82] Release 3.3.1 --- CHANGELOG.md | 9 +++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index afa2117..338ca74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 3.3.1 on 2024-12-02 + +### Changed +- Updated Windows clipboard and migrated from `winapi` to `windows-sys`. +- Internally migrated to Rust 2021 edition. +- Significantly improved the crate's error documentation. +- Updated `core-graphics` to `0.23` +- Updated `x11rb` to `0.13` + ## 3.3.0 on 2023-20-11 ### Added diff --git a/Cargo.lock b/Cargo.lock index fdf356a..fc6f8a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,7 +19,7 @@ dependencies = [ [[package]] name = "arboard" -version = "3.3.0" +version = "3.3.1" dependencies = [ "clipboard-win", "core-graphics", diff --git a/Cargo.toml b/Cargo.toml index 0334bff..69f8c3e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "arboard" -version = "3.3.0" +version = "3.3.1" authors = ["Artur Kovacs ", "Avi Weinstock ", "Arboard contributors"] description = "Image and text handling for the OS clipboard." repository = "https://github.com/1Password/arboard" From 3f21b88baacfbedc11c2aebd1e20c62c1d354065 Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Fri, 1 Mar 2024 19:23:29 -0600 Subject: [PATCH 18/82] Correctly mark windows::image_data functions as pub(super) and not pub --- src/platform/windows.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/platform/windows.rs b/src/platform/windows.rs index d229e94..0e0e0e6 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -37,7 +37,10 @@ mod image_data { Error::unknown(format!("{}: {}", message, os_error)) } - pub fn add_cf_dibv5(_open_clipboard: OpenClipboard, image: ImageData) -> Result<(), Error> { + pub(super) fn add_cf_dibv5( + _open_clipboard: OpenClipboard, + image: ImageData, + ) -> Result<(), Error> { // This constant is missing in windows-rs // https://github.com/microsoft/windows-rs/issues/2711 #[allow(non_upper_case_globals)] @@ -137,7 +140,7 @@ mod image_data { } } - pub fn read_cf_dibv5(dibv5: &[u8]) -> Result, Error> { + pub(super) fn read_cf_dibv5(dibv5: &[u8]) -> Result, Error> { // The DIBV5 format is a BITMAPV5HEADER followed by the pixel data according to // https://docs.microsoft.com/en-us/windows/win32/dataxchg/standard-clipboard-formats From d1ef0918deb396f8e2949e80125a79380b060375 Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Fri, 1 Mar 2024 19:28:54 -0600 Subject: [PATCH 19/82] Enable unreachable_pub lint as a warning CI will make these a hard error in the future --- src/common.rs | 3 +++ src/lib.rs | 1 + 2 files changed, 4 insertions(+) diff --git a/src/common.rs b/src/common.rs index 2a96706..808d842 100644 --- a/src/common.rs +++ b/src/common.rs @@ -167,6 +167,9 @@ impl Drop for ScopeGuard { /// Common trait for sealing platform extension traits. pub(crate) mod private { + // This is currently unused on macOS, so silence the warning which appears + // since there's no extension traits making use of this trait sealing structure. + #[cfg_attr(target_vendor = "apple", allow(unreachable_pub))] pub trait Sealed {} impl Sealed for crate::Get<'_> {} diff --git a/src/lib.rs b/src/lib.rs index e3a7a45..57256d5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ The project to which this file belongs is licensed under either of the Apache 2.0 or the MIT license at the licensee's choice. The terms and conditions of the chosen license apply to this file. */ +#![warn(unreachable_pub)] mod common; use std::borrow::Cow; From 2d77eee554474eea0576335e8b637900291e5b74 Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Fri, 1 Mar 2024 19:40:48 -0600 Subject: [PATCH 20/82] Add cargo-semver-checks to CI --- .github/workflows/test.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1953531..e891c0a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -91,3 +91,10 @@ jobs: - name: Check soundness run: cargo miri test windows --features image-data + + semver: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v4 + - name: Check semver + uses: obi1kenobi/cargo-semver-checks-action@v2 From f716441fe61f81a5d6950857f9859128de4aa4a5 Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Fri, 1 Mar 2024 19:48:31 -0600 Subject: [PATCH 21/82] Bump Ubuntu version used in CI Ubuntu 20 isn't the latest LTS version anymore --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e891c0a..14138e5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ on: jobs: rustfmt: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions-rust-lang/setup-rust-toolchain@v1 with: @@ -93,7 +93,7 @@ jobs: run: cargo miri test windows --features image-data semver: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - name: Check semver From dc8a4bd4f28cf096f898e123ad5a90b5763d1ed4 Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Sat, 2 Mar 2024 13:33:25 -0600 Subject: [PATCH 22/82] Release 3.3.2 --- CHANGELOG.md | 5 +++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 338ca74..339dd6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 3.3.2 on 2024-12-02 + +### Fixed +- Fixed compilation on Windows when using the `image-data` feature combined with older Rust compilers. + ## 3.3.1 on 2024-12-02 ### Changed diff --git a/Cargo.lock b/Cargo.lock index fc6f8a2..fafa7c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,7 +19,7 @@ dependencies = [ [[package]] name = "arboard" -version = "3.3.1" +version = "3.3.2" dependencies = [ "clipboard-win", "core-graphics", diff --git a/Cargo.toml b/Cargo.toml index 69f8c3e..553bd16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "arboard" -version = "3.3.1" +version = "3.3.2" authors = ["Artur Kovacs ", "Avi Weinstock ", "Arboard contributors"] description = "Image and text handling for the OS clipboard." repository = "https://github.com/1Password/arboard" From f6fc4ea691a854a1a01bba1d2fc616fbe01fa837 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=96R=C3=96K=20Attila?= Date: Sun, 24 Mar 2024 17:19:26 +0100 Subject: [PATCH 23/82] Update image to 0.25.1 --- Cargo.lock | 41 ++++++--------------------------------- Cargo.toml | 4 ++-- src/platform/linux/mod.rs | 2 +- 3 files changed, 9 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fafa7c1..5417e66 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -110,12 +110,6 @@ dependencies = [ "error-code", ] -[[package]] -name = "color_quant" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" - [[package]] name = "core-foundation" version = "0.9.4" @@ -319,14 +313,12 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "image" -version = "0.24.3" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e30ca2ecf7666107ff827a8e481de6a132a9b687ed3bb20bb1c144a36c00964" +checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11" dependencies = [ "bytemuck", "byteorder", - "color_quant", - "num-rational", "num-traits", "png", "tiff", @@ -344,9 +336,9 @@ dependencies = [ [[package]] name = "jpeg-decoder" -version = "0.2.6" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9478aa10f73e7528198d75109c8be5cd7d15fb530238040148d5f9a22d4c5b3b" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" [[package]] name = "lazy_static" @@ -454,27 +446,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "num-integer" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" -dependencies = [ - "autocfg", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.15" @@ -736,9 +707,9 @@ dependencies = [ [[package]] name = "tiff" -version = "0.7.3" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7259662e32d1e219321eb309d5f9d898b779769d81b76e762c07c8e5d38fcb65" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" dependencies = [ "flate2", "jpeg-decoder", diff --git a/Cargo.toml b/Cargo.toml index 553bd16..3e3a271 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,13 +37,13 @@ objc = "0.2" objc_id = "0.1" objc-foundation = "0.1" core-graphics = { version = "0.23", optional = true } -image = { version = "0.24", optional = true, default-features = false, features = ["tiff"] } +image = { version = "0.25", optional = true, default-features = false, features = ["tiff"] } [target.'cfg(all(unix, not(any(target_os="macos", target_os="android", target_os="emscripten"))))'.dependencies] log = "0.4" x11rb = { version = "0.13" } wl-clipboard-rs = { version = "0.8", optional = true } -image = { version = "0.24", optional = true, default-features = false, features = ["png"] } +image = { version = "0.25", optional = true, default-features = false, features = ["png"] } parking_lot = "0.12" [[example]] diff --git a/src/platform/linux/mod.rs b/src/platform/linux/mod.rs index 5bb28f5..97ef369 100644 --- a/src/platform/linux/mod.rs +++ b/src/platform/linux/mod.rs @@ -31,7 +31,7 @@ fn encode_as_png(image: &ImageData) -> Result, Error> { image.bytes.as_ref(), image.width as u32, image.height as u32, - image::ColorType::Rgba8, + image::ExtendedColorType::Rgba8, ) .map_err(|_| Error::ConversionFailure)?; From e5d3df70172f68d77f7295276d6ae996f7e254ff Mon Sep 17 00:00:00 2001 From: rhysd Date: Mon, 4 Mar 2024 12:26:53 +0900 Subject: [PATCH 24/82] Fix CI for Rust 1.61 doesn't use rustc 1.61 compiler --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 14138e5..5dc2c66 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,7 +30,7 @@ jobs: steps: - uses: actions-rust-lang/setup-rust-toolchain@v1 with: - toolchain: stable + toolchain: ${{ matrix.rust_version }} components: clippy - uses: actions/checkout@v4 From bb2e898d839e657ba40d06e332c67da4b6c51df5 Mon Sep 17 00:00:00 2001 From: rhysd Date: Thu, 11 Apr 2024 20:24:37 +0900 Subject: [PATCH 25/82] Bump up MSRV to 1.67.1 --- .github/workflows/test.yml | 2 +- Cargo.toml | 2 +- README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5dc2c66..27adbf9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,7 +26,7 @@ jobs: os: [macos-latest, windows-latest, ubuntu-latest] # Latest stable and MSRV. We only run checks with all features enabled # for the MSRV build to keep CI fast, since other configurations should also work. - rust_version: [stable, "1.61"] + rust_version: [stable, "1.67.1"] steps: - uses: actions-rust-lang/setup-rust-toolchain@v1 with: diff --git a/Cargo.toml b/Cargo.toml index 3e3a271..b9febae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT OR Apache-2.0" readme = "README.md" keywords = ["clipboard", "image"] edition = "2021" -rust-version = "1.61" +rust-version = "1.67.1" [features] default = ["image-data"] diff --git a/README.md b/README.md index 0eddd54..b64ef8e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Latest version](https://img.shields.io/crates/v/arboard?color=mediumvioletred)](https://crates.io/crates/arboard) [![Documentation](https://docs.rs/arboard/badge.svg)](https://docs.rs/arboard) -![MSRV](https://img.shields.io/badge/rustc-1.61+-blue.svg) +![MSRV](https://img.shields.io/badge/rustc-1.67.1+-blue.svg) ## General From c5c798b3a17e5e237160f744a66dde476fffc788 Mon Sep 17 00:00:00 2001 From: Noel Date: Thu, 22 Feb 2024 10:25:37 -0800 Subject: [PATCH 26/82] Add `SetExtLinux#wait_until(Instant)` This was added to allow to wait until the contents of the clipboard were updated but won't block forever. The `wait_until` method will wait until the deadline was reached. --- src/platform/linux/mod.rs | 38 +++++++++++++++++++++++++++++++++----- src/platform/linux/x11.rs | 18 ++++++++++++++---- 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/src/platform/linux/mod.rs b/src/platform/linux/mod.rs index 97ef369..80a67b9 100644 --- a/src/platform/linux/mod.rs +++ b/src/platform/linux/mod.rs @@ -1,4 +1,4 @@ -use std::borrow::Cow; +use std::{borrow::Cow, time::Instant}; #[cfg(feature = "wayland-data-control")] use log::{trace, warn}; @@ -143,17 +143,21 @@ impl GetExtLinux for crate::Get<'_> { pub(crate) struct Set<'clipboard> { clipboard: &'clipboard mut Clipboard, wait: bool, + wait_until: Option, selection: LinuxClipboardKind, } impl<'clipboard> Set<'clipboard> { pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self { - Self { clipboard, wait: false, selection: LinuxClipboardKind::Clipboard } + Self { clipboard, wait: false, wait_until: None, selection: LinuxClipboardKind::Clipboard } } pub(crate) fn text(self, text: Cow<'_, str>) -> Result<(), Error> { match self.clipboard { - Clipboard::X11(clipboard) => clipboard.set_text(text, self.selection, self.wait), + Clipboard::X11(clipboard) => { + clipboard.set_text(text, self.selection, self.wait, self.wait_until) + } + #[cfg(feature = "wayland-data-control")] Clipboard::WlDataControl(clipboard) => clipboard.set_text(text, self.selection, self.wait), } @@ -161,7 +165,10 @@ impl<'clipboard> Set<'clipboard> { pub(crate) fn html(self, html: Cow<'_, str>, alt: Option>) -> Result<(), Error> { match self.clipboard { - Clipboard::X11(clipboard) => clipboard.set_html(html, alt, self.selection, self.wait), + Clipboard::X11(clipboard) => { + clipboard.set_html(html, alt, self.selection, self.wait, self.wait_until) + } + #[cfg(feature = "wayland-data-control")] Clipboard::WlDataControl(clipboard) => clipboard.set_html(html, alt, self.selection, self.wait), } @@ -170,7 +177,10 @@ impl<'clipboard> Set<'clipboard> { #[cfg(feature = "image-data")] pub(crate) fn image(self, image: ImageData<'_>) -> Result<(), Error> { match self.clipboard { - Clipboard::X11(clipboard) => clipboard.set_image(image, self.selection, self.wait), + Clipboard::X11(clipboard) => { + clipboard.set_image(image, self.selection, self.wait, self.wait_until) + } + #[cfg(feature = "wayland-data-control")] Clipboard::WlDataControl(clipboard) => clipboard.set_image(image, self.selection, self.wait), } @@ -227,6 +237,15 @@ pub trait SetExtLinux: private::Sealed { /// # } /// ``` fn clipboard(self, selection: LinuxClipboardKind) -> Self; + + /// Whether or not to wait for the clipboard's content to be replaced after setting it. This waits until the + /// `deadline` has exceeded. + /// + /// This is useful for short-lived programs so that it doesn't block until new contents on the clipboard + /// were added and will exit as so. + /// + /// Note: this will call [`wait()`][SetExtLinux::wait]. + fn wait_until(self, deadline: Instant) -> Self; } impl SetExtLinux for crate::Set<'_> { @@ -239,6 +258,15 @@ impl SetExtLinux for crate::Set<'_> { self.platform.selection = selection; self } + + fn wait_until(mut self, deadline: Instant) -> Self { + self.platform.wait_until = Some(deadline); + if !self.platform.wait { + self.platform.wait = true; + } + + self + } } pub(crate) struct Clear<'clipboard> { diff --git a/src/platform/linux/x11.rs b/src/platform/linux/x11.rs index 8f3750c..e5c0654 100644 --- a/src/platform/linux/x11.rs +++ b/src/platform/linux/x11.rs @@ -228,6 +228,7 @@ impl Inner { data: Vec, selection: LinuxClipboardKind, wait: bool, + until: Option, ) -> Result<()> { if self.serve_stopped.load(Ordering::Relaxed) { return Err(Error::Unknown { @@ -264,7 +265,13 @@ impl Inner { drop(data_guard); // Wait for the clipboard's content to be changed. - selection.data_changed.wait(&mut guard); + match until { + Some(deadline) => { + selection.data_changed.wait_until(&mut guard, deadline); + } + + None => selection.data_changed.wait(&mut guard), + } } Ok(()) @@ -876,12 +883,13 @@ impl Clipboard { message: Cow<'_, str>, selection: LinuxClipboardKind, wait: bool, + until: Option, ) -> Result<()> { let data = vec![ClipboardData { bytes: message.into_owned().into_bytes(), format: self.inner.atoms.UTF8_STRING, }]; - self.inner.write(data, selection, wait) + self.inner.write(data, selection, wait, until) } pub(crate) fn set_html( @@ -890,6 +898,7 @@ impl Clipboard { alt: Option>, selection: LinuxClipboardKind, wait: bool, + until: Option, ) -> Result<()> { let mut data = vec![]; if let Some(alt_text) = alt { @@ -902,7 +911,7 @@ impl Clipboard { bytes: html.into_owned().into_bytes(), format: self.inner.atoms.HTML, }); - self.inner.write(data, selection, wait) + self.inner.write(data, selection, wait, until) } #[cfg(feature = "image-data")] @@ -929,10 +938,11 @@ impl Clipboard { image: ImageData, selection: LinuxClipboardKind, wait: bool, + until: Option, ) -> Result<()> { let encoded = encode_as_png(&image)?; let data = vec![ClipboardData { bytes: encoded, format: self.inner.atoms.PNG_MIME }]; - self.inner.write(data, selection, wait) + self.inner.write(data, selection, wait, until) } } From bc9fd24915bde9191d5ccc7ee0b3367d187b8f8f Mon Sep 17 00:00:00 2001 From: Noel Date: Thu, 22 Feb 2024 10:34:59 -0800 Subject: [PATCH 27/82] update docs --- src/platform/linux/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/linux/mod.rs b/src/platform/linux/mod.rs index 80a67b9..4d8fc33 100644 --- a/src/platform/linux/mod.rs +++ b/src/platform/linux/mod.rs @@ -244,7 +244,7 @@ pub trait SetExtLinux: private::Sealed { /// This is useful for short-lived programs so that it doesn't block until new contents on the clipboard /// were added and will exit as so. /// - /// Note: this will call [`wait()`][SetExtLinux::wait]. + /// Note: this will call [`wait()`][SetExtLinux::wait] if it wasn't previously set. fn wait_until(self, deadline: Instant) -> Self; } From eabb191df0da5bd07b7a16829af0044127fc8c19 Mon Sep 17 00:00:00 2001 From: Noel Date: Thu, 22 Feb 2024 10:36:05 -0800 Subject: [PATCH 28/82] add notice for X11 in `SetExtLinux#wait_until` docs --- src/platform/linux/mod.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/platform/linux/mod.rs b/src/platform/linux/mod.rs index 4d8fc33..2dbc569 100644 --- a/src/platform/linux/mod.rs +++ b/src/platform/linux/mod.rs @@ -244,6 +244,9 @@ pub trait SetExtLinux: private::Sealed { /// This is useful for short-lived programs so that it doesn't block until new contents on the clipboard /// were added and will exit as so. /// + /// For X11, this will wait until it either had new contents available in the clipboard or if the + /// `deadline` was exceeded. This isn't available for Wayland and will not do anything. + /// /// Note: this will call [`wait()`][SetExtLinux::wait] if it wasn't previously set. fn wait_until(self, deadline: Instant) -> Self; } From 6cf324cc44c56ff2835923388e7fbbdf47c27bbd Mon Sep 17 00:00:00 2001 From: Noel Date: Tue, 9 Apr 2024 17:07:00 -0700 Subject: [PATCH 29/82] Added `WaitConfig`, fix `wait_until` note in docs --- src/platform/linux/mod.rs | 47 ++++++++++++++++++++++++------------ src/platform/linux/x11.rs | 50 +++++++++++++++++++++++---------------- 2 files changed, 62 insertions(+), 35 deletions(-) diff --git a/src/platform/linux/mod.rs b/src/platform/linux/mod.rs index 2dbc569..fc5c892 100644 --- a/src/platform/linux/mod.rs +++ b/src/platform/linux/mod.rs @@ -154,9 +154,15 @@ impl<'clipboard> Set<'clipboard> { pub(crate) fn text(self, text: Cow<'_, str>) -> Result<(), Error> { match self.clipboard { - Clipboard::X11(clipboard) => { - clipboard.set_text(text, self.selection, self.wait, self.wait_until) - } + Clipboard::X11(clipboard) => clipboard.set_text( + text, + self.selection, + match (self.wait, self.wait_until) { + (_, Some(deadline)) => x11::WaitConfig::Until(deadline), + (true, None) => x11::WaitConfig::Forever, + (false, None) => x11::WaitConfig::None, + }, + ), #[cfg(feature = "wayland-data-control")] Clipboard::WlDataControl(clipboard) => clipboard.set_text(text, self.selection, self.wait), @@ -165,9 +171,16 @@ impl<'clipboard> Set<'clipboard> { pub(crate) fn html(self, html: Cow<'_, str>, alt: Option>) -> Result<(), Error> { match self.clipboard { - Clipboard::X11(clipboard) => { - clipboard.set_html(html, alt, self.selection, self.wait, self.wait_until) - } + Clipboard::X11(clipboard) => clipboard.set_html( + html, + alt, + self.selection, + match (self.wait, self.wait_until) { + (_, Some(deadline)) => x11::WaitConfig::Until(deadline), + (true, None) => x11::WaitConfig::Forever, + (false, None) => x11::WaitConfig::None, + }, + ), #[cfg(feature = "wayland-data-control")] Clipboard::WlDataControl(clipboard) => clipboard.set_html(html, alt, self.selection, self.wait), @@ -177,9 +190,15 @@ impl<'clipboard> Set<'clipboard> { #[cfg(feature = "image-data")] pub(crate) fn image(self, image: ImageData<'_>) -> Result<(), Error> { match self.clipboard { - Clipboard::X11(clipboard) => { - clipboard.set_image(image, self.selection, self.wait, self.wait_until) - } + Clipboard::X11(clipboard) => clipboard.set_image( + image, + self.selection, + match (self.wait, self.wait_until) { + (_, Some(deadline)) => x11::WaitConfig::Until(deadline), + (true, None) => x11::WaitConfig::Forever, + (false, None) => x11::WaitConfig::None, + }, + ), #[cfg(feature = "wayland-data-control")] Clipboard::WlDataControl(clipboard) => clipboard.set_image(image, self.selection, self.wait), @@ -241,13 +260,11 @@ pub trait SetExtLinux: private::Sealed { /// Whether or not to wait for the clipboard's content to be replaced after setting it. This waits until the /// `deadline` has exceeded. /// - /// This is useful for short-lived programs so that it doesn't block until new contents on the clipboard - /// were added and will exit as so. + /// This is useful for short-lived programs so it won't block until new contents on the clipboard + /// were added. /// - /// For X11, this will wait until it either had new contents available in the clipboard or if the - /// `deadline` was exceeded. This isn't available for Wayland and will not do anything. - /// - /// Note: this will call [`wait()`][SetExtLinux::wait] if it wasn't previously set. + /// Note: this is a superset of [`wait()`][SetExtLinux::wait] and will overwrite any state + /// that was previously set using it. fn wait_until(self, deadline: Instant) -> Self; } diff --git a/src/platform/linux/x11.rs b/src/platform/linux/x11.rs index e5c0654..68545bc 100644 --- a/src/platform/linux/x11.rs +++ b/src/platform/linux/x11.rs @@ -205,6 +205,20 @@ enum ReadSelNotifyResult { EventNotRecognized, } +/// Configuration on how long to wait for a new X11 copy event is emitted. +#[derive(Default)] +pub enum WaitConfig { + /// Waits until the given [`Instant`] has reached. + Until(Instant), + + /// Waits forever until a new event is reached. + Forever, + + /// It shouldn't wait. + #[default] + None, +} + impl Inner { fn new() -> Result { let server = XContext::new()?; @@ -227,8 +241,7 @@ impl Inner { &self, data: Vec, selection: LinuxClipboardKind, - wait: bool, - until: Option, + wait: WaitConfig, ) -> Result<()> { if self.serve_stopped.load(Ordering::Relaxed) { return Err(Error::Unknown { @@ -261,16 +274,16 @@ impl Inner { // It is important that the mutex is locked to prevent this notification getting lost. selection.data_changed.notify_all(); - if wait { - drop(data_guard); + match wait { + WaitConfig::None => {} + WaitConfig::Forever => { + drop(data_guard); + selection.data_changed.wait(&mut guard); + } - // Wait for the clipboard's content to be changed. - match until { - Some(deadline) => { - selection.data_changed.wait_until(&mut guard, deadline); - } - - None => selection.data_changed.wait(&mut guard), + WaitConfig::Until(deadline) => { + drop(data_guard); + selection.data_changed.wait_until(&mut guard, deadline); } } @@ -882,14 +895,13 @@ impl Clipboard { &self, message: Cow<'_, str>, selection: LinuxClipboardKind, - wait: bool, - until: Option, + wait: WaitConfig, ) -> Result<()> { let data = vec![ClipboardData { bytes: message.into_owned().into_bytes(), format: self.inner.atoms.UTF8_STRING, }]; - self.inner.write(data, selection, wait, until) + self.inner.write(data, selection, wait) } pub(crate) fn set_html( @@ -897,8 +909,7 @@ impl Clipboard { html: Cow<'_, str>, alt: Option>, selection: LinuxClipboardKind, - wait: bool, - until: Option, + wait: WaitConfig, ) -> Result<()> { let mut data = vec![]; if let Some(alt_text) = alt { @@ -911,7 +922,7 @@ impl Clipboard { bytes: html.into_owned().into_bytes(), format: self.inner.atoms.HTML, }); - self.inner.write(data, selection, wait, until) + self.inner.write(data, selection, wait) } #[cfg(feature = "image-data")] @@ -937,12 +948,11 @@ impl Clipboard { &self, image: ImageData, selection: LinuxClipboardKind, - wait: bool, - until: Option, + wait: WaitConfig, ) -> Result<()> { let encoded = encode_as_png(&image)?; let data = vec![ClipboardData { bytes: encoded, format: self.inner.atoms.PNG_MIME }]; - self.inner.write(data, selection, wait, until) + self.inner.write(data, selection, wait) } } From 2f4b50250816bd91ed7d1e2629ac4191534f15a0 Mon Sep 17 00:00:00 2001 From: Noel Date: Thu, 11 Apr 2024 12:00:24 -0700 Subject: [PATCH 30/82] Move `WaitConfig` to src/platform/linux/mod.rs, use `WaitConfig` inside `struct Set` --- src/platform/linux/mod.rs | 78 ++++++++++++++--------------------- src/platform/linux/wayland.rs | 26 ++++++++---- src/platform/linux/x11.rs | 16 +------ 3 files changed, 51 insertions(+), 69 deletions(-) diff --git a/src/platform/linux/mod.rs b/src/platform/linux/mod.rs index fc5c892..d8207e3 100644 --- a/src/platform/linux/mod.rs +++ b/src/platform/linux/mod.rs @@ -140,29 +140,34 @@ impl GetExtLinux for crate::Get<'_> { } } +/// Configuration on how long to wait for a new X11 copy event is emitted. +#[derive(Default)] +pub(crate) enum WaitConfig { + /// Waits until the given [`Instant`] has reached. + Until(Instant), + + /// Waits forever until a new event is reached. + Forever, + + /// It shouldn't wait. + #[default] + None, +} + pub(crate) struct Set<'clipboard> { clipboard: &'clipboard mut Clipboard, - wait: bool, - wait_until: Option, + wait: WaitConfig, selection: LinuxClipboardKind, } impl<'clipboard> Set<'clipboard> { pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self { - Self { clipboard, wait: false, wait_until: None, selection: LinuxClipboardKind::Clipboard } + Self { clipboard, wait: WaitConfig::default(), selection: LinuxClipboardKind::Clipboard } } pub(crate) fn text(self, text: Cow<'_, str>) -> Result<(), Error> { match self.clipboard { - Clipboard::X11(clipboard) => clipboard.set_text( - text, - self.selection, - match (self.wait, self.wait_until) { - (_, Some(deadline)) => x11::WaitConfig::Until(deadline), - (true, None) => x11::WaitConfig::Forever, - (false, None) => x11::WaitConfig::None, - }, - ), + Clipboard::X11(clipboard) => clipboard.set_text(text, self.selection, self.wait), #[cfg(feature = "wayland-data-control")] Clipboard::WlDataControl(clipboard) => clipboard.set_text(text, self.selection, self.wait), @@ -171,16 +176,7 @@ impl<'clipboard> Set<'clipboard> { pub(crate) fn html(self, html: Cow<'_, str>, alt: Option>) -> Result<(), Error> { match self.clipboard { - Clipboard::X11(clipboard) => clipboard.set_html( - html, - alt, - self.selection, - match (self.wait, self.wait_until) { - (_, Some(deadline)) => x11::WaitConfig::Until(deadline), - (true, None) => x11::WaitConfig::Forever, - (false, None) => x11::WaitConfig::None, - }, - ), + Clipboard::X11(clipboard) => clipboard.set_html(html, alt, self.selection, self.wait), #[cfg(feature = "wayland-data-control")] Clipboard::WlDataControl(clipboard) => clipboard.set_html(html, alt, self.selection, self.wait), @@ -190,15 +186,7 @@ impl<'clipboard> Set<'clipboard> { #[cfg(feature = "image-data")] pub(crate) fn image(self, image: ImageData<'_>) -> Result<(), Error> { match self.clipboard { - Clipboard::X11(clipboard) => clipboard.set_image( - image, - self.selection, - match (self.wait, self.wait_until) { - (_, Some(deadline)) => x11::WaitConfig::Until(deadline), - (true, None) => x11::WaitConfig::Forever, - (false, None) => x11::WaitConfig::None, - }, - ), + Clipboard::X11(clipboard) => clipboard.set_image(image, self.selection, self.wait), #[cfg(feature = "wayland-data-control")] Clipboard::WlDataControl(clipboard) => clipboard.set_image(image, self.selection, self.wait), @@ -235,6 +223,16 @@ pub trait SetExtLinux: private::Sealed { /// [daemonize example]: https://github.com/1Password/arboard/blob/master/examples/daemonize.rs fn wait(self) -> Self; + /// Whether or not to wait for the clipboard's content to be replaced after setting it. This waits until the + /// `deadline` has exceeded. + /// + /// This is useful for short-lived programs so it won't block until new contents on the clipboard + /// were added. + /// + /// Note: this is a superset of [`wait()`][SetExtLinux::wait] and will overwrite any state + /// that was previously set using it. + fn wait_until(self, deadline: Instant) -> Self; + /// Sets the clipboard the operation will store its data to. /// /// If wayland support is enabled and available, attempting to use the Secondary clipboard will @@ -256,21 +254,11 @@ pub trait SetExtLinux: private::Sealed { /// # } /// ``` fn clipboard(self, selection: LinuxClipboardKind) -> Self; - - /// Whether or not to wait for the clipboard's content to be replaced after setting it. This waits until the - /// `deadline` has exceeded. - /// - /// This is useful for short-lived programs so it won't block until new contents on the clipboard - /// were added. - /// - /// Note: this is a superset of [`wait()`][SetExtLinux::wait] and will overwrite any state - /// that was previously set using it. - fn wait_until(self, deadline: Instant) -> Self; } impl SetExtLinux for crate::Set<'_> { fn wait(mut self) -> Self { - self.platform.wait = true; + self.platform.wait = WaitConfig::Forever; self } @@ -280,11 +268,7 @@ impl SetExtLinux for crate::Set<'_> { } fn wait_until(mut self, deadline: Instant) -> Self { - self.platform.wait_until = Some(deadline); - if !self.platform.wait { - self.platform.wait = true; - } - + self.platform.wait = WaitConfig::Until(deadline); self } } diff --git a/src/platform/linux/wayland.rs b/src/platform/linux/wayland.rs index c2b316d..c874274 100644 --- a/src/platform/linux/wayland.rs +++ b/src/platform/linux/wayland.rs @@ -10,7 +10,7 @@ use wl_clipboard_rs::{ #[cfg(feature = "image-data")] use super::encode_as_png; -use super::{into_unknown, LinuxClipboardKind}; +use super::{into_unknown, LinuxClipboardKind, WaitConfig}; use crate::common::Error; #[cfg(feature = "image-data")] use crate::common::ImageData; @@ -79,10 +79,14 @@ impl Clipboard { &self, text: Cow<'_, str>, selection: LinuxClipboardKind, - wait: bool, + wait: WaitConfig, ) -> Result<(), Error> { let mut opts = Options::new(); - opts.foreground(wait); + opts.foreground(match wait { + WaitConfig::Forever => true, + _ => false, + }); + opts.clipboard(selection.try_into()?); let source = Source::Bytes(text.into_owned().into_bytes().into_boxed_slice()); opts.copy(source, MimeType::Text).map_err(|e| match e { @@ -97,11 +101,15 @@ impl Clipboard { html: Cow<'_, str>, alt: Option>, selection: LinuxClipboardKind, - wait: bool, + wait: WaitConfig, ) -> Result<(), Error> { let html_mime = MimeType::Specific(String::from("text/html")); let mut opts = Options::new(); - opts.foreground(wait); + opts.foreground(match wait { + WaitConfig::Forever => true, + _ => false, + }); + opts.clipboard(selection.try_into()?); let html_source = Source::Bytes(html.into_owned().into_bytes().into_boxed_slice()); match alt { @@ -163,11 +171,15 @@ impl Clipboard { &mut self, image: ImageData, selection: LinuxClipboardKind, - wait: bool, + wait: WaitConfig, ) -> Result<(), Error> { let image = encode_as_png(&image)?; let mut opts = Options::new(); - opts.foreground(wait); + opts.foreground(match wait { + WaitConfig::Forever => true, + _ => false, + }); + opts.clipboard(selection.try_into()?); let source = Source::Bytes(image.into()); opts.copy(source, MimeType::Specific(MIME_PNG.into())).map_err(into_unknown)?; diff --git a/src/platform/linux/x11.rs b/src/platform/linux/x11.rs index 68545bc..a8d659d 100644 --- a/src/platform/linux/x11.rs +++ b/src/platform/linux/x11.rs @@ -45,7 +45,7 @@ use x11rb::{ #[cfg(feature = "image-data")] use super::encode_as_png; -use super::{into_unknown, LinuxClipboardKind}; +use super::{into_unknown, LinuxClipboardKind, WaitConfig}; #[cfg(feature = "image-data")] use crate::ImageData; use crate::{common::ScopeGuard, Error}; @@ -205,20 +205,6 @@ enum ReadSelNotifyResult { EventNotRecognized, } -/// Configuration on how long to wait for a new X11 copy event is emitted. -#[derive(Default)] -pub enum WaitConfig { - /// Waits until the given [`Instant`] has reached. - Until(Instant), - - /// Waits forever until a new event is reached. - Forever, - - /// It shouldn't wait. - #[default] - None, -} - impl Inner { fn new() -> Result { let server = XContext::new()?; From e2846f928808ecc6e403215ed642ac9656053e97 Mon Sep 17 00:00:00 2001 From: Noel Date: Thu, 11 Apr 2024 13:29:59 -0700 Subject: [PATCH 31/82] Fix clippy errors --- src/platform/linux/wayland.rs | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/src/platform/linux/wayland.rs b/src/platform/linux/wayland.rs index c874274..b3916a6 100644 --- a/src/platform/linux/wayland.rs +++ b/src/platform/linux/wayland.rs @@ -1,5 +1,4 @@ use std::borrow::Cow; -use std::convert::TryInto; use std::io::Read; use wl_clipboard_rs::{ @@ -82,11 +81,7 @@ impl Clipboard { wait: WaitConfig, ) -> Result<(), Error> { let mut opts = Options::new(); - opts.foreground(match wait { - WaitConfig::Forever => true, - _ => false, - }); - + opts.foreground(matches!(wait, WaitConfig::Forever)); opts.clipboard(selection.try_into()?); let source = Source::Bytes(text.into_owned().into_bytes().into_boxed_slice()); opts.copy(source, MimeType::Text).map_err(|e| match e { @@ -105,11 +100,7 @@ impl Clipboard { ) -> Result<(), Error> { let html_mime = MimeType::Specific(String::from("text/html")); let mut opts = Options::new(); - opts.foreground(match wait { - WaitConfig::Forever => true, - _ => false, - }); - + opts.foreground(matches!(wait, WaitConfig::Forever)); opts.clipboard(selection.try_into()?); let html_source = Source::Bytes(html.into_owned().into_bytes().into_boxed_slice()); match alt { @@ -175,11 +166,7 @@ impl Clipboard { ) -> Result<(), Error> { let image = encode_as_png(&image)?; let mut opts = Options::new(); - opts.foreground(match wait { - WaitConfig::Forever => true, - _ => false, - }); - + opts.foreground(matches!(wait, WaitConfig::Forever)); opts.clipboard(selection.try_into()?); let source = Source::Bytes(image.into()); opts.copy(source, MimeType::Specific(MIME_PNG.into())).map_err(into_unknown)?; From b4646f6c5f6aa91029a33efaa67886852356a9b9 Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Mon, 22 Apr 2024 14:00:38 -0500 Subject: [PATCH 32/82] Increase version of clipboard-win used by default --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5417e66..eb49fd9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -103,9 +103,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clipboard-win" -version = "5.0.0" +version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c57002a5d9be777c1ef967e33674dac9ebd310d8893e4e3437b14d5f0f6372cc" +checksum = "79f4473f5144e20d9aceaf2972478f06ddf687831eafeeb434fbaf0acc4144ad" dependencies = [ "error-code", ] diff --git a/Cargo.toml b/Cargo.toml index b9febae..7443610 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ windows-sys = { version = "0.48.0", optional = true, features = [ "Win32_System_Memory", "Win32_System_Ole", ]} -clipboard-win = "5.0.0" +clipboard-win = "5.3.1" log = "0.4" [target.'cfg(target_os = "macos")'.dependencies] From 1cca83d7e59613b016cc7558e73f6b873ea4175e Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Mon, 22 Apr 2024 14:03:17 -0500 Subject: [PATCH 33/82] Revert "add timeout to RustConnection::connect to X11 server" This reverts commit efedfb9e2058eaa97073bda49f1b7deae762c2fb. --- src/platform/linux/x11.rs | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/platform/linux/x11.rs b/src/platform/linux/x11.rs index a8d659d..0dd7c50 100644 --- a/src/platform/linux/x11.rs +++ b/src/platform/linux/x11.rs @@ -18,9 +18,9 @@ use std::{ collections::{hash_map::Entry, HashMap}, sync::{ atomic::{AtomicBool, Ordering}, - mpsc, Arc, + Arc, }, - thread::{self, JoinHandle}, + thread::JoinHandle, thread_local, time::{Duration, Instant}, usize, @@ -134,16 +134,12 @@ struct Inner { impl XContext { fn new() -> Result { // create a new connection to an X11 server - // with a timeout on connecting to the socket in case of hangage - let (tx, rx) = mpsc::channel(); - thread::spawn(move || { - tx.send(RustConnection::connect(None)).ok(); // disregard error sending on channel as main thread has timed out. - }); - let patient_conn = rx.recv_timeout(SHORT_TIMEOUT_DUR).map_err(|_| Error::Unknown { - description: String::from("X11 server connection timed out because it was unreachable"), - })?; - let (conn, screen_num): (RustConnection, _) = patient_conn.map_err(into_unknown)?; - + let (conn, screen_num): (RustConnection, _) = + RustConnection::connect(None).map_err(|_| Error::Unknown { + description: String::from( + "X11 server connection timed out because it was unreachable", + ), + })?; let screen = conn .setup() .roots From 0bff1e07eac7c895265fefefac02b39a4dda892b Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Tue, 23 Apr 2024 15:42:50 +0200 Subject: [PATCH 34/82] Use objc2 and its framework crates `objc2` is a replacement for `objc`/`objc_id` that contains a bunch of safety improvements, including `msg_send_id!` which automatically upholds memory management rules (`Id::from_ptr`/`Id::from_retained_ptr` is no longer necessary). Additionally, we use the framework crates `objc2-foundation` and `objc2-app-kit`, which provide for example the `NSPasteboard` type, which has the methods that arboard needs already defined, and with the correct types, ensuring that passing e.g. `Id` and thus accidentally giving away ownership over the array won't happen again. These crates are automatically generated, ensuring that if you need some obscure API in the future, it's very likely to be there already. --- Cargo.lock | 78 ++++++++++++-------- Cargo.toml | 7 +- src/platform/osx.rs | 170 +++++++++++++++++++------------------------- 3 files changed, 128 insertions(+), 127 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eb49fd9..94d3091 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,9 +26,9 @@ dependencies = [ "env_logger", "image", "log", - "objc", - "objc-foundation", - "objc_id", + "objc2", + "objc2-app-kit", + "objc2-foundation", "parking_lot", "thiserror", "windows-sys", @@ -66,10 +66,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" [[package]] -name = "block" -version = "0.1.6" +name = "block2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" +checksum = "43ff7d91d3c1d568065b06c899777d1e48dcf76103a672a0adbc238a7f247f1e" +dependencies = [ + "objc2", +] [[package]] name = "bytecount" @@ -384,15 +387,6 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] - [[package]] name = "memchr" version = "2.5.0" @@ -456,32 +450,58 @@ dependencies = [ ] [[package]] -name = "objc" -version = "0.2.7" +name = "objc-sys" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +checksum = "da284c198fb9b7b0603f8635185e85fbd5b64ee154b1ed406d489077de2d6d60" + +[[package]] +name = "objc2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4b25e1034d0e636cd84707ccdaa9f81243d399196b8a773946dcffec0401659" dependencies = [ - "malloc_buf", + "objc-sys", + "objc2-encode", ] [[package]] -name = "objc-foundation" -version = "0.1.1" +name = "objc2-app-kit" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +checksum = "fb79768a710a9a1798848179edb186d1af7e8a8679f369e4b8d201dd2a034047" dependencies = [ - "block", - "objc", - "objc_id", + "block2", + "objc2", + "objc2-core-data", + "objc2-foundation", ] [[package]] -name = "objc_id" -version = "0.1.1" +name = "objc2-core-data" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +checksum = "6e092bc42eaf30a08844e6a076938c60751225ec81431ab89f5d1ccd9f958d6c" dependencies = [ - "objc", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-encode" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88658da63e4cc2c8adb1262902cd6af51094df0488b760d6fd27194269c0950a" + +[[package]] +name = "objc2-foundation" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfaefe14254871ea16c7d88968c0ff14ba554712a20d76421eec52f0a7fb8904" +dependencies = [ + "block2", + "objc2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 7443610..6ab3516 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,9 +33,10 @@ clipboard-win = "5.3.1" log = "0.4" [target.'cfg(target_os = "macos")'.dependencies] -objc = "0.2" -objc_id = "0.1" -objc-foundation = "0.1" +# Use `relax-void-encoding`, as that allows us to pass `c_void` instead of implementing `Encode` correctly for `&CGImageRef` +objc2 = { version = "0.5.1", features = ["relax-void-encoding"] } +objc2-foundation = { version = "0.2.0", features = ["NSArray", "NSString", "NSEnumerator", "NSGeometry"] } +objc2-app-kit = { version = "0.2.0", features = ["NSPasteboard", "NSPasteboardItem", "NSImage"] } core-graphics = { version = "0.23", optional = true } image = { version = "0.25", optional = true, default-features = false, features = ["tiff"] } diff --git a/src/platform/osx.rs b/src/platform/osx.rs index c48df73..ac4dc79 100644 --- a/src/platform/osx.rs +++ b/src/platform/osx.rs @@ -11,31 +11,18 @@ and conditions of the chosen license apply to this file. use crate::common::Error; #[cfg(feature = "image-data")] use crate::common::ImageData; -#[cfg(feature = "image-data")] -use core_graphics::{ - base::{kCGBitmapByteOrderDefault, kCGImageAlphaLast, kCGRenderingIntentDefault, CGFloat}, - color_space::CGColorSpace, - data_provider::{CGDataProvider, CustomData}, - image::CGImage, +use objc2::{ + msg_send_id, + rc::{autoreleasepool, Id}, + runtime::ProtocolObject, + ClassType, }; -use objc::{ - msg_send, - rc::autoreleasepool, - runtime::{Class, Object}, - sel, sel_impl, +use objc2_app_kit::{NSPasteboard, NSPasteboardTypeHTML, NSPasteboardTypeString}; +use objc2_foundation::{NSArray, NSString}; +use std::{ + borrow::Cow, + panic::{RefUnwindSafe, UnwindSafe}, }; -use objc_foundation::{INSArray, INSFastEnumeration, INSString, NSArray, NSObject, NSString}; -use objc_id::{Id, Owned}; -use std::{borrow::Cow, ptr::NonNull}; - -// Required to bring NSPasteboard into the path of the class-resolver -#[link(name = "AppKit", kind = "framework")] -extern "C" { - static NSPasteboardTypeHTML: *const Object; - static NSPasteboardTypeString: *const Object; - #[cfg(feature = "image-data")] - static NSPasteboardTypeTIFF: *const Object; -} /// Returns an NSImage object on success. #[cfg(feature = "image-data")] @@ -43,13 +30,16 @@ fn image_from_pixels( pixels: Vec, width: usize, height: usize, -) -> Result, Box> { - #[repr(C)] - #[derive(Copy, Clone)] - struct NSSize { - width: CGFloat, - height: CGFloat, - } +) -> Result, Box> { + use core_graphics::{ + base::{kCGBitmapByteOrderDefault, kCGImageAlphaLast, kCGRenderingIntentDefault, CGFloat}, + color_space::CGColorSpace, + data_provider::{CGDataProvider, CustomData}, + image::{CGImage, CGImageRef}, + }; + use objc2_app_kit::NSImage; + use objc2_foundation::NSSize; + use std::ffi::c_void; #[derive(Debug)] struct PixelArray { @@ -81,42 +71,54 @@ fn image_from_pixels( false, kCGRenderingIntentDefault, ); + + // Convert the owned `CGImage` into a reference `&CGImageRef`, and pass + // that as `*const c_void`, since `CGImageRef` does not implement + // `RefEncode`. + let cg_image: *const CGImageRef = &*cg_image; + let cg_image: *const c_void = cg_image.cast(); + let size = NSSize { width: width as CGFloat, height: height as CGFloat }; - let nsimage_class = objc::class!(NSImage); - // Take ownership of the newly allocated object, which has an existing retain count. - let image: Id = unsafe { Id::from_retained_ptr(msg_send![nsimage_class, alloc]) }; - #[allow(clippy::let_unit_value)] - { - // Note: `initWithCGImage` expects a reference (`CGImageRef`), not an actual object. - let _: () = unsafe { msg_send![image, initWithCGImage: &*cg_image size:size] }; - } + // XXX: Use `NSImage::initWithCGImage_size` once `objc2-app-kit` supports + // CoreGraphics. + let image: Id = + unsafe { msg_send_id![NSImage::alloc(), initWithCGImage: cg_image, size:size] }; Ok(image) } pub(crate) struct Clipboard { - pasteboard: Id, + pasteboard: Id, } +unsafe impl Send for Clipboard {} +unsafe impl Sync for Clipboard {} +impl UnwindSafe for Clipboard {} +impl RefUnwindSafe for Clipboard {} + impl Clipboard { pub(crate) fn new() -> Result { - let cls = Class::get("NSPasteboard").expect("NSPasteboard not registered"); - let pasteboard: *mut Object = unsafe { msg_send![cls, generalPasteboard] }; + // Rust only supports 10.7+, while `generalPasteboard` first appeared + // in 10.0, so this should always be available. + // + // However, in some edge cases, like running under launchd (in some + // modes) as a daemon, the clipboard object may be unavailable, and + // then `generalPasteboard` will return NULL even though it's + // documented not to. + // + // Otherwise we'd just use `NSPasteboard::generalPasteboard()` here. + let pasteboard: Option> = + unsafe { msg_send_id![NSPasteboard::class(), generalPasteboard] }; - if !pasteboard.is_null() { - // SAFETY: `generalPasteboard` is not null and a valid object pointer. - let pasteboard: Id = unsafe { Id::from_ptr(pasteboard) }; + if let Some(pasteboard) = pasteboard { Ok(Clipboard { pasteboard }) } else { - // Rust only supports 10.7+, while `generalPasteboard` first appeared in 10.0, so this - // is unreachable in "normal apps". However in some edge cases, like running under - // launchd (in some modes) as a daemon, the clipboard object may be unavailable. Err(Error::ClipboardNotSupported) } } fn clear(&mut self) { - let _: usize = unsafe { msg_send![self.pasteboard, clearContents] }; + unsafe { self.pasteboard.clearContents() }; } // fn get_binary_contents(&mut self) -> Result, Box> { @@ -171,38 +173,31 @@ impl Clipboard { } pub(crate) struct Get<'clipboard> { - pasteboard: &'clipboard Object, + clipboard: &'clipboard Clipboard, } impl<'clipboard> Get<'clipboard> { pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self { - Self { pasteboard: &*clipboard.pasteboard } + Self { clipboard } } pub(crate) fn text(self) -> Result { // XXX: There does not appear to be an alternative for obtaining text without the need for // autorelease behavior. - autoreleasepool(|| { + autoreleasepool(|_| { // XXX: We explicitly use `pasteboardItems` and not `stringForType` since the latter will concat // multiple strings, if present, into one and return it instead of reading just the first which is `arboard`'s // historical behavior. - let contents: Option>> = - unsafe { msg_send![self.pasteboard, pasteboardItems] }; - - let contents = contents.map(|c| unsafe { c.as_ref() }).ok_or_else(|| { - Error::Unknown { description: String::from("NSPasteboard#pasteboardItems errored") } - })?; - - for item in contents.enumerator() { - let maybe_str: Option> = - unsafe { msg_send![item, stringForType:NSPasteboardTypeString] }; - - match maybe_str { - Some(string) => { - let string: Id = unsafe { Id::from_ptr(string.as_ptr()) }; - return Ok(string.as_str().to_owned()); + let contents = + unsafe { self.clipboard.pasteboard.pasteboardItems() }.ok_or_else(|| { + Error::Unknown { + description: String::from("NSPasteboard#pasteboardItems errored"), } - None => continue, + })?; + + for item in contents { + if let Some(string) = unsafe { item.stringForType(NSPasteboardTypeString) } { + return Ok(string.to_string()); } } @@ -212,27 +207,16 @@ impl<'clipboard> Get<'clipboard> { #[cfg(feature = "image-data")] pub(crate) fn image(self) -> Result, Error> { - use objc_foundation::NSData; + use objc2_app_kit::NSPasteboardTypeTIFF; use std::io::Cursor; // XXX: There does not appear to be an alternative for obtaining images without the need for // autorelease behavior. - let image = autoreleasepool(|| { - let obj: Option> = - unsafe { msg_send![self.pasteboard, dataForType: NSPasteboardTypeTIFF] }; + let image = autoreleasepool(|_| { + let image_data = unsafe { self.clipboard.pasteboard.dataForType(NSPasteboardTypeTIFF) } + .ok_or(Error::ContentNotAvailable)?; - let image_data: Id = if let Some(obj) = obj { - unsafe { Id::from_ptr(obj.as_ptr()) } - } else { - return Err(Error::ContentNotAvailable); - }; - - let data = unsafe { - let len: usize = msg_send![&*image_data, length]; - let bytes: *const u8 = msg_send![&*image_data, bytes]; - - Cursor::new(std::slice::from_raw_parts(bytes, len)) - }; + let data = Cursor::new(image_data.bytes()); let reader = image::io::Reader::with_format(data, image::ImageFormat::Tiff); reader.decode().map_err(|_| Error::ConversionFailure) @@ -261,11 +245,9 @@ impl<'clipboard> Set<'clipboard> { pub(crate) fn text(self, data: Cow<'_, str>) -> Result<(), Error> { self.clipboard.clear(); - let string_array = NSArray::from_vec(vec![NSString::from_str(&data)]); - // Make sure that we pass a pointer to the system and not the array object itself. Otherwise, - // the system won't free it because the API doesn't give it ownership of the data. This results in - // a memory leak because Rust can never run its destructor. - let success = unsafe { msg_send![self.clipboard.pasteboard, writeObjects: &*string_array] }; + let string_array = + NSArray::from_vec(vec![ProtocolObject::from_id(NSString::from_str(&data))]); + let success = unsafe { self.clipboard.pasteboard.writeObjects(&string_array) }; if success { Ok(()) } else { @@ -286,15 +268,14 @@ impl<'clipboard> Set<'clipboard> { ); let html_nss = NSString::from_str(&html); // Make sure that we pass a pointer to the string and not the object itself. - let mut success: bool = unsafe { - msg_send![self.clipboard.pasteboard, setString: &*html_nss forType:NSPasteboardTypeHTML] - }; + let mut success = + unsafe { self.clipboard.pasteboard.setString_forType(&html_nss, NSPasteboardTypeHTML) }; if success { if let Some(alt_text) = alt { let alt_nss = NSString::from_str(&alt_text); // Similar to the primary string, we only want a pointer here too. success = unsafe { - msg_send![self.clipboard.pasteboard, setString: &*alt_nss forType:NSPasteboardTypeString] + self.clipboard.pasteboard.setString_forType(&alt_nss, NSPasteboardTypeString) }; } } @@ -313,9 +294,8 @@ impl<'clipboard> Set<'clipboard> { self.clipboard.clear(); - let image_array: Id> = NSArray::from_vec(vec![image]); - // Make sure that we pass a pointer to the system and not the array object itself. - let success = unsafe { msg_send![self.clipboard.pasteboard, writeObjects: &*image_array] }; + let image_array = NSArray::from_vec(vec![ProtocolObject::from_id(image)]); + let success = unsafe { self.clipboard.pasteboard.writeObjects(&image_array) }; if success { Ok(()) } else { From 83740b7ab098768e92b4a5f72eaa8fe7c0662905 Mon Sep 17 00:00:00 2001 From: Roman Vlasenko <56698047+Klavionik@users.noreply.github.com> Date: Sun, 28 Apr 2024 09:57:04 +0300 Subject: [PATCH 35/82] Copy image as PNG file on Windows (#141) * Add image to Windows dependencies * Set image data as PNG file on Windows --- Cargo.toml | 1 + src/platform/windows.rs | 70 ++++++++++++++++++++++++++++++++++------- 2 files changed, 59 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6ab3516..8074266 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ windows-sys = { version = "0.48.0", optional = true, features = [ ]} clipboard-win = "5.3.1" log = "0.4" +image = { version = "0.25", optional = true, default-features = false, features = ["png"] } [target.'cfg(target_os = "macos")'.dependencies] # Use `relax-void-encoding`, as that allows us to pass `c_void` instead of implementing `Encode` correctly for `&CGImageRef` diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 0e0e0e6..6544b92 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -17,6 +17,9 @@ use std::{borrow::Cow, marker::PhantomData, thread, time::Duration}; mod image_data { use super::*; use crate::common::ScopeGuard; + use image::codecs::png::PngEncoder; + use image::ExtendedColorType; + use image::ImageEncoder; use std::{convert::TryInto, ffi::c_void, io, mem::size_of, ptr::copy_nonoverlapping}; use windows_sys::Win32::{ Foundation::HGLOBAL, @@ -37,6 +40,18 @@ mod image_data { Error::unknown(format!("{}: {}", message, os_error)) } + unsafe fn global_unlock_checked(hdata: isize) { + // If the memory object is unlocked after decrementing the lock count, the function + // returns zero and GetLastError returns NO_ERROR. If it fails, the return value is + // zero and GetLastError returns a value other than NO_ERROR. + if GlobalUnlock(hdata) == 0 { + let err = io::Error::last_os_error(); + if err.raw_os_error() != Some(0) { + log::error!("Failed calling GlobalUnlock when writing data: {}", err); + } + } + } + pub(super) fn add_cf_dibv5( _open_clipboard: OpenClipboard, image: ImageData, @@ -85,17 +100,7 @@ mod image_data { let hdata = unsafe { global_alloc(data_size)? }; unsafe { let data_ptr = global_lock(hdata)?; - let _unlock = ScopeGuard::new(|| { - // If the memory object is unlocked after decrementing the lock count, the function - // returns zero and GetLastError returns NO_ERROR. If it fails, the return value is - // zero and GetLastError returns a value other than NO_ERROR. - if GlobalUnlock(hdata) == 0 { - let err = io::Error::last_os_error(); - if err.raw_os_error() != Some(0) { - log::error!("Failed calling GlobalUnlock when writing dibv5 data: {}", err); - } - } - }); + let _unlock = ScopeGuard::new(|| global_unlock_checked(hdata)); copy_nonoverlapping::((&header) as *const _ as *const u8, data_ptr, header_size); @@ -122,6 +127,43 @@ mod image_data { } } + pub(super) fn add_png_file(image: &ImageData) -> Result<(), Error> { + // Try encoding the image as PNG. + let mut buf = Vec::new(); + let encoder = PngEncoder::new(&mut buf); + + encoder + .write_image( + &image.bytes, + image.width as u32, + image.height as u32, + ExtendedColorType::Rgba8, + ) + .map_err(|_| Error::ConversionFailure)?; + + // Register PNG format. + let format_id = match clipboard_win::register_format("PNG") { + Some(format_id) => format_id.into(), + None => return Err(last_error("Cannot register PNG clipboard format.")), + }; + + let data_size = buf.len(); + let hdata = unsafe { global_alloc(data_size)? }; + + unsafe { + let pixels_dst = global_lock(hdata)?; + copy_nonoverlapping::(buf.as_ptr(), pixels_dst, data_size); + global_unlock_checked(hdata); + } + + if unsafe { SetClipboardData(format_id, hdata as _) } == 0 { + unsafe { DeleteObject(hdata as _) }; + Err(last_error("SetClipboardData failed with error")) + } else { + Ok(()) + } + } + unsafe fn global_alloc(bytes: usize) -> Result { let hdata = GlobalAlloc(GHND, bytes); if hdata == 0 { @@ -609,7 +651,11 @@ impl<'clipboard> Set<'clipboard> { ))); }; - image_data::add_cf_dibv5(open_clipboard, image) + // XXX: The ordering of these functions is important, as some programs will grab the + // first format available. PNGs tend to have better compatibility on Windows, so it is set first. + image_data::add_png_file(&image)?; + image_data::add_cf_dibv5(open_clipboard, image)?; + Ok(()) } } From 610e29ba8156b7a86922de7a786cbb6991e4e55c Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Sun, 28 Apr 2024 03:59:32 -0500 Subject: [PATCH 36/82] Remove direct thiserror dependency --- Cargo.lock | 1 - Cargo.toml | 1 - src/common.rs | 21 ++++++++++++++------- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 94d3091..9d3c382 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,7 +30,6 @@ dependencies = [ "objc2-app-kit", "objc2-foundation", "parking_lot", - "thiserror", "windows-sys", "wl-clipboard-rs", "x11rb", diff --git a/Cargo.toml b/Cargo.toml index 8074266..74fa993 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,6 @@ image-data = ["core-graphics", "image", "windows-sys"] wayland-data-control = ["wl-clipboard-rs"] [dependencies] -thiserror = "1.0" [dev-dependencies] env_logger = "0.9.0" diff --git a/src/common.rs b/src/common.rs index 808d842..221a883 100644 --- a/src/common.rs +++ b/src/common.rs @@ -10,20 +10,17 @@ and conditions of the chosen license apply to this file. #[cfg(feature = "image-data")] use std::borrow::Cow; -use thiserror::Error; /// An error that might happen during a clipboard operation. /// /// Note that both the `Display` and the `Debug` trait is implemented for this type in such a way /// that they give a short human-readable description of the error; however the documentation /// gives a more detailed explanation for each error kind. -#[derive(Error)] #[non_exhaustive] pub enum Error { /// The clipboard contents were not available in the requested format. /// This could either be due to the clipboard being empty or the clipboard contents having /// an incompatible format to the requested one (eg when calling `get_image` on text) - #[error("The clipboard contents were not available in the requested format or the clipboard is empty.")] ContentNotAvailable, /// The selected clipboard is not supported by the current configuration (system and/or environment). @@ -31,7 +28,6 @@ pub enum Error { /// This can be caused by a few conditions: /// - Using the Primary clipboard with an older Wayland compositor (that doesn't support version 2) /// - Using the Secondary clipboard on Wayland - #[error("The selected clipboard is not supported with the current system configuration.")] ClipboardNotSupported, /// The native clipboard is not accessible due to being held by an other party. @@ -43,22 +39,33 @@ pub enum Error { /// Note that it's OK to have multiple `Clipboard` instances. The underlying /// implementation will make sure that the native clipboard is only /// opened for transferring data and then closed as soon as possible. - #[error("The native clipboard is not accessible due to being held by an other party.")] ClipboardOccupied, /// The image or the text that was about the be transferred to/from the clipboard could not be /// converted to the appropriate format. - #[error("The image or the text that was about the be transferred to/from the clipboard could not be converted to the appropriate format.")] ConversionFailure, /// Any error that doesn't fit the other error types. /// /// The `description` field is only meant to help the developer and should not be relied on as a /// means to identify an error case during runtime. - #[error("Unknown error while interacting with the clipboard: {description}")] Unknown { description: String }, } +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::ContentNotAvailable => f.write_str("The clipboard contents were not available in the requested format or the clipboard is empty."), + Error::ClipboardNotSupported => f.write_str("The selected clipboard is not supported with the current system configuration."), + Error::ClipboardOccupied => f.write_str("The native clipboard is not accessible due to being held by an other party."), + Error::ConversionFailure => f.write_str("The image or the text that was about the be transferred to/from the clipboard could not be converted to the appropriate format."), + Error::Unknown { description } => f.write_fmt(format_args!("Unknown error while interacting with the clipboard: {description}")), + } + } +} + +impl std::error::Error for Error {} + impl std::fmt::Debug for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { use Error::*; From 151e679ee5c208403b06ba02d28f92c5891f7867 Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Sun, 28 Apr 2024 03:52:39 -0500 Subject: [PATCH 37/82] Release 3.4.0 --- CHANGELOG.md | 19 +++++++++++++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 339dd6f..e61007e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## 3.4.0 on 2024-29-04 + +### Added +- Added a `wait_until` method for Linux, as a superset of the existing `wait` functionality. + This is a helper for letting an application wait without manual timeout handling. + +### Fixed +- Transparency in copied images now behaves better in certain Windows apps. + +### Changed +- Updated `image` to `0.25`. +- Removed direct `thiserror` dependency. +- Fixed Linux documentation links +- Raised MSRV to 1.67.1 +- Reverted timeout behavior of `Clipboard::new()` on platforms using X11. Applications are + encouraged to wrap constructor calls in their own thread/channel timeout mechanisms instead + to make sure the behavior matches each usecase. +- Migrated away from `objc` to the `objc2` ecosystem for the Apple clipboard implementation. + ## 3.3.2 on 2024-12-02 ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 9d3c382..2680970 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,7 +19,7 @@ dependencies = [ [[package]] name = "arboard" -version = "3.3.2" +version = "3.4.0" dependencies = [ "clipboard-win", "core-graphics", diff --git a/Cargo.toml b/Cargo.toml index 74fa993..5bbceaa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "arboard" -version = "3.3.2" +version = "3.4.0" authors = ["Artur Kovacs ", "Avi Weinstock ", "Arboard contributors"] description = "Image and text handling for the OS clipboard." repository = "https://github.com/1Password/arboard" From ee39c47896cded1ebf86857cdc31bf6c98a445a7 Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Sat, 24 Aug 2024 16:28:08 -0500 Subject: [PATCH 38/82] Fix new lints for CI --- src/common.rs | 2 +- src/platform/linux/x11.rs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/common.rs b/src/common.rs index 221a883..adda67c 100644 --- a/src/common.rs +++ b/src/common.rs @@ -176,7 +176,7 @@ impl Drop for ScopeGuard { pub(crate) mod private { // This is currently unused on macOS, so silence the warning which appears // since there's no extension traits making use of this trait sealing structure. - #[cfg_attr(target_vendor = "apple", allow(unreachable_pub))] + #[cfg_attr(target_vendor = "apple", allow(unreachable_pub, dead_code))] pub trait Sealed {} impl Sealed for crate::Get<'_> {} diff --git a/src/platform/linux/x11.rs b/src/platform/linux/x11.rs index 0dd7c50..406b43f 100644 --- a/src/platform/linux/x11.rs +++ b/src/platform/linux/x11.rs @@ -23,7 +23,6 @@ use std::{ thread::JoinHandle, thread_local, time::{Duration, Instant}, - usize, }; use log::{error, trace, warn}; From dd43f44aceb1c70fd915a87082ea76ddd16cdb50 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Tue, 27 Aug 2024 20:10:21 +0200 Subject: [PATCH 39/82] Add support for excluding macos clipboard items from history (#159) * Adds support for excluding macos clipboard items from history --- src/common.rs | 3 --- src/lib.rs | 3 +++ src/platform/mod.rs | 2 +- src/platform/osx.rs | 46 ++++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/common.rs b/src/common.rs index adda67c..2e10fea 100644 --- a/src/common.rs +++ b/src/common.rs @@ -174,9 +174,6 @@ impl Drop for ScopeGuard { /// Common trait for sealing platform extension traits. pub(crate) mod private { - // This is currently unused on macOS, so silence the warning which appears - // since there's no extension traits making use of this trait sealing structure. - #[cfg_attr(target_vendor = "apple", allow(unreachable_pub, dead_code))] pub trait Sealed {} impl Sealed for crate::Get<'_> {} diff --git a/src/lib.rs b/src/lib.rs index 57256d5..ee752f6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,6 +27,9 @@ pub use platform::{ClearExtLinux, GetExtLinux, LinuxClipboardKind, SetExtLinux}; #[cfg(windows)] pub use platform::SetExtWindows; +#[cfg(target_os = "macos")] +pub use platform::SetExtApple; + /// The OS independent struct for accessing the clipboard. /// /// Any number of `Clipboard` instances are allowed to exist at a single point in time. Note however diff --git a/src/platform/mod.rs b/src/platform/mod.rs index b336463..268eb47 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -14,4 +14,4 @@ pub use windows::*; #[cfg(target_os = "macos")] mod osx; #[cfg(target_os = "macos")] -pub(crate) use osx::*; +pub use osx::*; diff --git a/src/platform/osx.rs b/src/platform/osx.rs index ac4dc79..ec5f881 100644 --- a/src/platform/osx.rs +++ b/src/platform/osx.rs @@ -8,9 +8,9 @@ the Apache 2.0 or the MIT license at the licensee's choice. The terms and conditions of the chosen license apply to this file. */ -use crate::common::Error; #[cfg(feature = "image-data")] use crate::common::ImageData; +use crate::common::{private, Error}; use objc2::{ msg_send_id, rc::{autoreleasepool, Id}, @@ -18,7 +18,7 @@ use objc2::{ ClassType, }; use objc2_app_kit::{NSPasteboard, NSPasteboardTypeHTML, NSPasteboardTypeString}; -use objc2_foundation::{NSArray, NSString}; +use objc2_foundation::{ns_string, NSArray, NSString}; use std::{ borrow::Cow, panic::{RefUnwindSafe, UnwindSafe}, @@ -235,11 +235,12 @@ impl<'clipboard> Get<'clipboard> { pub(crate) struct Set<'clipboard> { clipboard: &'clipboard mut Clipboard, + exclude_from_history: bool, } impl<'clipboard> Set<'clipboard> { pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self { - Self { clipboard } + Self { clipboard, exclude_from_history: false } } pub(crate) fn text(self, data: Cow<'_, str>) -> Result<(), Error> { @@ -248,6 +249,9 @@ impl<'clipboard> Set<'clipboard> { let string_array = NSArray::from_vec(vec![ProtocolObject::from_id(NSString::from_str(&data))]); let success = unsafe { self.clipboard.pasteboard.writeObjects(&string_array) }; + + add_clipboard_exclusions(self.clipboard, self.exclude_from_history); + if success { Ok(()) } else { @@ -279,6 +283,9 @@ impl<'clipboard> Set<'clipboard> { }; } } + + add_clipboard_exclusions(self.clipboard, self.exclude_from_history); + if success { Ok(()) } else { @@ -296,6 +303,9 @@ impl<'clipboard> Set<'clipboard> { let image_array = NSArray::from_vec(vec![ProtocolObject::from_id(image)]); let success = unsafe { self.clipboard.pasteboard.writeObjects(&image_array) }; + + add_clipboard_exclusions(self.clipboard, self.exclude_from_history); + if success { Ok(()) } else { @@ -322,3 +332,33 @@ impl<'clipboard> Clear<'clipboard> { Ok(()) } } + +fn add_clipboard_exclusions(clipboard: &mut Clipboard, exclude_from_history: bool) { + // On Mac there isn't an official standard for excluding data from clipboard, however + // there is an unofficial standard which is to set `org.nspasteboard.ConcealedType`. + // + // See http://nspasteboard.org/ for details about the community standard. + if exclude_from_history { + unsafe { + clipboard + .pasteboard + .setString_forType(ns_string!(""), ns_string!("org.nspasteboard.ConcealedType")); + } + } +} + +/// Apple-specific extensions to the [`Set`](crate::Set) builder. +pub trait SetExtApple: private::Sealed { + /// Excludes the data which will be set on the clipboard from being added to + /// third party clipboard history software. + /// + /// See http://nspasteboard.org/ for details about the community standard. + fn exclude_from_history(self) -> Self; +} + +impl SetExtApple for crate::Set<'_> { + fn exclude_from_history(mut self) -> Self { + self.platform.exclude_from_history = true; + self + } +} From 6b45272702b1caeff0fa6eb96e1b9fd2f0f557d9 Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Thu, 12 Sep 2024 11:22:40 -0400 Subject: [PATCH 40/82] Release 3.4.1 --- CHANGELOG.md | 6 ++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e61007e..f8d852d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 3.4.1 on 2024-12-09 + +### Added +- Added support for excluding macOS clipboard items from history. + - Note that macOS has no official history, so arboard's implementation uses a community standard instead. + ## 3.4.0 on 2024-29-04 ### Added diff --git a/Cargo.lock b/Cargo.lock index 2680970..ae52788 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,7 +19,7 @@ dependencies = [ [[package]] name = "arboard" -version = "3.4.0" +version = "3.4.1" dependencies = [ "clipboard-win", "core-graphics", diff --git a/Cargo.toml b/Cargo.toml index 5bbceaa..51ca568 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "arboard" -version = "3.4.0" +version = "3.4.1" authors = ["Artur Kovacs ", "Avi Weinstock ", "Arboard contributors"] description = "Image and text handling for the OS clipboard." repository = "https://github.com/1Password/arboard" From 5350a8fb91f1c14bd3ef9ccce3db3a1698566d4d Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Sat, 19 Oct 2024 10:32:10 -0500 Subject: [PATCH 41/82] Bump env_logger This removes `atty` and `winapi` from CI's test builds. --- Cargo.lock | 75 ++++++++++++++++++------------------------------------ Cargo.toml | 2 +- 2 files changed, 26 insertions(+), 51 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ae52788..97bdca1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,17 +35,6 @@ dependencies = [ "x11rb", ] -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi", - "libc", - "winapi", -] - [[package]] name = "autocfg" version = "1.1.0" @@ -189,12 +178,12 @@ checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" [[package]] name = "env_logger" -version = "0.9.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c90bf5f19754d10198ccb95b70664fc925bd1fc090a0fd9a6ebc54acc8cd6272" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" dependencies = [ - "atty", "humantime", + "is-terminal", "log", "regex", "termcolor", @@ -300,12 +289,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hermit-abi" -version = "0.1.19" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "humantime" @@ -336,6 +322,17 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "is-terminal" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24fddda5af7e54bf7da53067d6e802dbcc381d0a8eef629df528e3ebf68755cb" +dependencies = [ + "hermit-abi", + "rustix", + "windows-sys", +] + [[package]] name = "jpeg-decoder" version = "0.3.1" @@ -382,9 +379,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "memchr" @@ -631,9 +628,9 @@ checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" [[package]] name = "rustix" -version = "0.38.17" +version = "0.38.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f25469e9ae0f3d0047ca8b93fc56843f38e6774f0914a107ff8b41be8be8e0b7" +checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" dependencies = [ "bitflags 2.4.0", "errno", @@ -697,9 +694,9 @@ dependencies = [ [[package]] name = "termcolor" -version = "1.1.3" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" dependencies = [ "winapi-util", ] @@ -834,37 +831,15 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "winapi", + "windows-sys", ] -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index 51ca568..bbce191 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ wayland-data-control = ["wl-clipboard-rs"] [dependencies] [dev-dependencies] -env_logger = "0.9.0" +env_logger = "0.10.2" [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.48.0", optional = true, features = [ From 782b98c1e3cca063594178f2dc39b55f83fb12aa Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Thu, 26 Dec 2024 13:21:11 -0600 Subject: [PATCH 42/82] Cleanup unneeded lifetimes --- src/common.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common.rs b/src/common.rs index 2e10fea..291c528 100644 --- a/src/common.rs +++ b/src/common.rs @@ -132,7 +132,7 @@ pub struct ImageData<'a> { } #[cfg(feature = "image-data")] -impl<'a> ImageData<'a> { +impl ImageData<'_> { /// Returns a the bytes field in a way that it's guaranteed to be owned. /// It moves the bytes if they are already owned and clones them if they are borrowed. pub fn into_owned_bytes(self) -> Cow<'static, [u8]> { From e458e1a26c177c9bc2c551ac35542f6a425ee701 Mon Sep 17 00:00:00 2001 From: Hamir Mahal Date: Thu, 26 Dec 2024 11:39:38 -0800 Subject: [PATCH 43/82] style: simplify some statements for readability --- examples/hello_world.rs | 2 +- src/common.rs | 2 +- src/lib.rs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/hello_world.rs b/examples/hello_world.rs index efc4ebf..6c8376c 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -7,5 +7,5 @@ fn main() { let the_string = "Hello, world!"; clipboard.set_text(the_string).unwrap(); - println!("But now the clipboard text should be: \"{}\"", the_string); + println!("But now the clipboard text should be: \"{the_string}\""); } diff --git a/src/common.rs b/src/common.rs index 291c528..ab00b9c 100644 --- a/src/common.rs +++ b/src/common.rs @@ -85,7 +85,7 @@ impl std::fmt::Debug for Error { ConversionFailure, Unknown { .. } ); - f.write_fmt(format_args!("{} - \"{}\"", name, self)) + f.write_fmt(format_args!("{name} - \"{self}\"")) } } diff --git a/src/lib.rs b/src/lib.rs index ee752f6..01eac45 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -295,7 +295,7 @@ mod tests { match ctx.get_text() { Ok(text) => assert!(text.is_empty()), Err(Error::ContentNotAvailable) => {} - Err(e) => panic!("unexpected error: {}", e), + Err(e) => panic!("unexpected error: {e}"), }; // confirm it is OK to clear when already empty. @@ -310,7 +310,7 @@ mod tests { match ctx.get_text() { Ok(text) => assert!(text.is_empty()), Err(Error::ContentNotAvailable) => {} - Err(e) => panic!("unexpected error: {}", e), + Err(e) => panic!("unexpected error: {e}"), }; } { From 4b91bfe93e534cbb179794e8d340953355dcf161 Mon Sep 17 00:00:00 2001 From: Gae24 <96017547+Gae24@users.noreply.github.com> Date: Thu, 13 Feb 2025 06:01:02 +0100 Subject: [PATCH 44/82] Implement Get::html() for all platforms (#163) * implement get html operation Signed-off-by: Gae24 <96017547+Gae24@users.noreply.github.com> --- examples/{set_html.rs => set_get_html.rs} | 3 ++ src/lib.rs | 23 ++++++++++++ src/platform/linux/mod.rs | 8 ++++ src/platform/linux/wayland.rs | 18 +++++++-- src/platform/linux/x11.rs | 6 +++ src/platform/osx.rs | 45 +++++++++++++---------- src/platform/windows.rs | 13 +++++++ 7 files changed, 92 insertions(+), 24 deletions(-) rename examples/{set_html.rs => set_get_html.rs} (78%) diff --git a/examples/set_html.rs b/examples/set_get_html.rs similarity index 78% rename from examples/set_html.rs rename to examples/set_get_html.rs index 125f6b1..96ab58a 100644 --- a/examples/set_html.rs +++ b/examples/set_get_html.rs @@ -15,4 +15,7 @@ consectetur adipiscing elit."#; ctx.set_html(html, Some(alt_text)).unwrap(); thread::sleep(Duration::from_secs(5)); + + let success = ctx.get().html().unwrap() == html; + println!("Set and Get html operations were successful: {success}"); } diff --git a/src/lib.rs b/src/lib.rs index 01eac45..c002bdc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -192,6 +192,11 @@ impl Get<'_> { pub fn image(self) -> Result, Error> { self.platform.image() } + + /// Completes the "get" operation by fetching HTML from the clipboard. + pub fn html(self) -> Result { + self.platform.html() + } } /// A builder for an operation that sets a value to the clipboard. @@ -322,6 +327,24 @@ mod tests { ctx.set_html(html, Some(alt_text)).unwrap(); assert_eq!(ctx.get_text().unwrap(), alt_text); } + { + let mut ctx = Clipboard::new().unwrap(); + + let html = "hello world!"; + + ctx.set().html(html, None).unwrap(); + + if cfg!(target_os = "macos") { + // Copying HTML on macOS adds wrapper content to work around + // historical platform bugs. We control this wrapper, so we are + // able to check that the full user data still appears and at what + // position in the final copy contents. + let content = ctx.get().html().unwrap(); + assert!(content.ends_with(&format!("{html}"))); + } else { + assert_eq!(ctx.get().html().unwrap(), html); + } + } #[cfg(feature = "image-data")] { let mut ctx = Clipboard::new().unwrap(); diff --git a/src/platform/linux/mod.rs b/src/platform/linux/mod.rs index d8207e3..4ce13c1 100644 --- a/src/platform/linux/mod.rs +++ b/src/platform/linux/mod.rs @@ -122,6 +122,14 @@ impl<'clipboard> Get<'clipboard> { Clipboard::WlDataControl(clipboard) => clipboard.get_image(self.selection), } } + + pub(crate) fn html(self) -> Result { + match self.clipboard { + Clipboard::X11(clipboard) => clipboard.get_html(self.selection), + #[cfg(feature = "wayland-data-control")] + Clipboard::WlDataControl(clipboard) => clipboard.get_html(self.selection), + } + } } /// Linux-specific extensions to the [`Get`](super::Get) builder. diff --git a/src/platform/linux/wayland.rs b/src/platform/linux/wayland.rs index b3916a6..9cd12bf 100644 --- a/src/platform/linux/wayland.rs +++ b/src/platform/linux/wayland.rs @@ -53,10 +53,12 @@ impl Clipboard { Ok(Self {}) } - pub(crate) fn get_text(&mut self, selection: LinuxClipboardKind) -> Result { - use wl_clipboard_rs::paste::MimeType; - - let result = get_contents(selection.try_into()?, Seat::Unspecified, MimeType::Text); + fn string_for_mime( + &mut self, + selection: LinuxClipboardKind, + mime: paste::MimeType, + ) -> Result { + let result = get_contents(selection.try_into()?, Seat::Unspecified, mime); match result { Ok((mut pipe, _)) => { let mut contents = vec![]; @@ -74,6 +76,10 @@ impl Clipboard { } } + pub(crate) fn get_text(&mut self, selection: LinuxClipboardKind) -> Result { + self.string_for_mime(selection, paste::MimeType::Text) + } + pub(crate) fn set_text( &self, text: Cow<'_, str>, @@ -91,6 +97,10 @@ impl Clipboard { Ok(()) } + pub(crate) fn get_html(&mut self, selection: LinuxClipboardKind) -> Result { + self.string_for_mime(selection, paste::MimeType::Specific("text/html")) + } + pub(crate) fn set_html( &self, html: Cow<'_, str>, diff --git a/src/platform/linux/x11.rs b/src/platform/linux/x11.rs index 406b43f..c221c2d 100644 --- a/src/platform/linux/x11.rs +++ b/src/platform/linux/x11.rs @@ -885,6 +885,12 @@ impl Clipboard { self.inner.write(data, selection, wait) } + pub(crate) fn get_html(&self, selection: LinuxClipboardKind) -> Result { + let formats = [self.inner.atoms.HTML]; + let result = self.inner.read(&formats, selection)?; + String::from_utf8(result.bytes).map_err(|_| Error::ConversionFailure) + } + pub(crate) fn set_html( &self, html: Cow<'_, str>, diff --git a/src/platform/osx.rs b/src/platform/osx.rs index ec5f881..1df7222 100644 --- a/src/platform/osx.rs +++ b/src/platform/osx.rs @@ -121,6 +121,27 @@ impl Clipboard { unsafe { self.pasteboard.clearContents() }; } + fn string_from_type(&self, type_: &'static NSString) -> Result { + // XXX: There does not appear to be an alternative for obtaining text without the need for + // autorelease behavior. + autoreleasepool(|_| { + // XXX: We explicitly use `pasteboardItems` and not `stringForType` since the latter will concat + // multiple strings, if present, into one and return it instead of reading just the first which is `arboard`'s + // historical behavior. + let contents = unsafe { self.pasteboard.pasteboardItems() }.ok_or_else(|| { + Error::Unknown { description: String::from("NSPasteboard#pasteboardItems errored") } + })?; + + for item in contents { + if let Some(string) = unsafe { item.stringForType(type_) } { + return Ok(string.to_string()); + } + } + + Err(Error::ContentNotAvailable) + }) + } + // fn get_binary_contents(&mut self) -> Result, Box> { // let string_class: Id = { // let cls: Id = unsafe { Id::from_ptr(class("NSString")) }; @@ -182,27 +203,11 @@ impl<'clipboard> Get<'clipboard> { } pub(crate) fn text(self) -> Result { - // XXX: There does not appear to be an alternative for obtaining text without the need for - // autorelease behavior. - autoreleasepool(|_| { - // XXX: We explicitly use `pasteboardItems` and not `stringForType` since the latter will concat - // multiple strings, if present, into one and return it instead of reading just the first which is `arboard`'s - // historical behavior. - let contents = - unsafe { self.clipboard.pasteboard.pasteboardItems() }.ok_or_else(|| { - Error::Unknown { - description: String::from("NSPasteboard#pasteboardItems errored"), - } - })?; + unsafe { self.clipboard.string_from_type(NSPasteboardTypeString) } + } - for item in contents { - if let Some(string) = unsafe { item.stringForType(NSPasteboardTypeString) } { - return Ok(string.to_string()); - } - } - - Err(Error::ContentNotAvailable) - }) + pub(crate) fn html(self) -> Result { + unsafe { self.clipboard.string_from_type(NSPasteboardTypeHTML) } } #[cfg(feature = "image-data")] diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 6544b92..79dca54 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -567,6 +567,19 @@ impl<'clipboard> Get<'clipboard> { String::from_utf16(&out[..bytes_read]).map_err(|_| Error::ConversionFailure) } + pub(crate) fn html(self) -> Result { + let _clipboard_assertion = self.clipboard?; + + let format = clipboard_win::register_format("HTML Format") + .ok_or_else(|| Error::unknown("unable to register HTML format"))?; + + let mut out: Vec = Vec::new(); + clipboard_win::raw::get_html(format.get(), &mut out) + .map_err(|_| Error::unknown("failed to read clipboard string"))?; + + String::from_utf8(out).map_err(|_| Error::ConversionFailure) + } + #[cfg(feature = "image-data")] pub(crate) fn image(self) -> Result, Error> { const FORMAT: u32 = clipboard_win::formats::CF_DIBV5; From 431702b65721995a7457dfd6cdc0d207fcb8d879 Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Wed, 12 Feb 2025 23:16:21 -0600 Subject: [PATCH 45/82] Bump Ubuntu runner version for cargo-semver-checks (#174) --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 27adbf9..7e08cc2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -93,7 +93,7 @@ jobs: run: cargo miri test windows --features image-data semver: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Check semver From 108cc382698c582ede2a3fbc8637d8072ed7e80b Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Thu, 13 Feb 2025 20:44:37 +0100 Subject: [PATCH 46/82] Update to objc2 v0.6 (#173) This includes using the new crate `objc2-core-graphics`, which notably does not have the `CustomData` helper that `core-graphics`. This is probably for the better, as it allows us to avoid a double-boxing of the data. --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 8 +- Cargo.lock | 168 +++++++++++-------------------------- Cargo.toml | 66 ++++++++++++--- README.md | 2 +- src/platform/osx.rs | 108 ++++++++++++------------ 6 files changed, 167 insertions(+), 187 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7e08cc2..46ad856 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,7 +26,7 @@ jobs: os: [macos-latest, windows-latest, ubuntu-latest] # Latest stable and MSRV. We only run checks with all features enabled # for the MSRV build to keep CI fast, since other configurations should also work. - rust_version: [stable, "1.67.1"] + rust_version: [stable, "1.71.0"] steps: - uses: actions-rust-lang/setup-rust-toolchain@v1 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index f8d852d..95f0d66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### Changed +- Updated `objc2` to `v0.6`. +- Raised MSRV to 1.71.0. + ## 3.4.1 on 2024-12-09 ### Added @@ -97,11 +101,11 @@ from a `write` call to a X11 and Wayland or clipboard - Updated `wl-clipboard-rs` to the version `0.6`. - Updated `x11rb` to the version `0.10`. - Cleaned up spelling in documentation -- (Breaking) Functions that used to accept `String` now take `Into, str>` instead. +- (Breaking) Functions that used to accept `String` now take `Into, str>` instead. This avoids cloning the string more times then necessary on platforms that can. - (Breaking) `Error` is now marked as `#[non_exhaustive]`. - (Breaking) Removed all platform specific modules and clipboard structures from the public API. -If you were using these directly, the recommended replacement is using `arboard::Clipboard` and +If you were using these directly, the recommended replacement is using `arboard::Clipboard` and the new platform-specific extension traits instead. - (Breaking) On Windows, the clipboard is now opened once per call to `Clipboard::new()` instead of on each operation. This means that instances of `Clipboard` should be dropped once you're performed the diff --git a/Cargo.lock b/Cargo.lock index 97bdca1..7b04703 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -22,12 +22,13 @@ name = "arboard" version = "3.4.1" dependencies = [ "clipboard-win", - "core-graphics", "env_logger", "image", "log", "objc2", "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", "objc2-foundation", "parking_lot", "windows-sys", @@ -49,18 +50,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" - -[[package]] -name = "block2" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43ff7d91d3c1d568065b06c899777d1e48dcf76103a672a0adbc238a7f247f1e" -dependencies = [ - "objc2", -] +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" [[package]] name = "bytecount" @@ -101,46 +93,6 @@ dependencies = [ "error-code", ] -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" - -[[package]] -name = "core-graphics" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "970a29baf4110c26fedbc7f82107d42c23f7e88e404c4577ed73fe99ff85a212" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "core-graphics-types", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "libc", -] - [[package]] name = "crc32fast" version = "1.3.2" @@ -158,7 +110,7 @@ checksum = "3418329ca0ad70234b9735dc4ceed10af4df60eff9c8e7b06cb5e520d92c3535" dependencies = [ "proc-macro2", "quote", - "syn 1.0.100", + "syn", ] [[package]] @@ -244,33 +196,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foreign-types" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" -dependencies = [ - "foreign-types-macros", - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-macros" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.48", -] - -[[package]] -name = "foreign-types-shared" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" - [[package]] name = "gethostname" version = "0.4.3" @@ -445,59 +370,75 @@ dependencies = [ "autocfg", ] -[[package]] -name = "objc-sys" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da284c198fb9b7b0603f8635185e85fbd5b64ee154b1ed406d489077de2d6d60" - [[package]] name = "objc2" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4b25e1034d0e636cd84707ccdaa9f81243d399196b8a773946dcffec0401659" +checksum = "3531f65190d9cff863b77a99857e74c314dd16bf56c538c4b57c7cbc3f3a6e59" dependencies = [ - "objc-sys", "objc2-encode", ] [[package]] name = "objc2-app-kit" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb79768a710a9a1798848179edb186d1af7e8a8679f369e4b8d201dd2a034047" +checksum = "5906f93257178e2f7ae069efb89fbd6ee94f0592740b5f8a1512ca498814d0fb" dependencies = [ - "block2", + "bitflags 2.8.0", "objc2", - "objc2-core-data", + "objc2-core-graphics", "objc2-foundation", ] [[package]] -name = "objc2-core-data" -version = "0.2.0" +name = "objc2-core-foundation" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e092bc42eaf30a08844e6a076938c60751225ec81431ab89f5d1ccd9f958d6c" +checksum = "daeaf60f25471d26948a1c2f840e3f7d86f4109e3af4e8e4b5cd70c39690d925" dependencies = [ - "block2", + "bitflags 2.8.0", "objc2", - "objc2-foundation", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dca602628b65356b6513290a21a6405b4d4027b8b250f0b98dddbb28b7de02" +dependencies = [ + "bitflags 2.8.0", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", ] [[package]] name = "objc2-encode" -version = "4.0.1" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88658da63e4cc2c8adb1262902cd6af51094df0488b760d6fd27194269c0950a" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" [[package]] name = "objc2-foundation" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfaefe14254871ea16c7d88968c0ff14ba554712a20d76421eec52f0a7fb8904" +checksum = "3a21c6c9014b82c39515db5b396f91645182611c97d24637cf56ac01e5f8d998" dependencies = [ - "block2", + "bitflags 2.8.0", "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "161a8b87e32610086e1a7a9e9ec39f84459db7b3a0881c1f16ca5a2605581c19" +dependencies = [ + "bitflags 2.8.0", + "objc2", + "objc2-core-foundation", ] [[package]] @@ -632,7 +573,7 @@ version = "0.38.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.8.0", "errno", "libc", "linux-raw-sys", @@ -668,17 +609,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "syn" -version = "2.0.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - [[package]] name = "tempfile" version = "3.8.0" @@ -718,7 +648,7 @@ checksum = "f8b463991b4eab2d801e724172285ec4195c650e8ec79b149e6c2a8e6dd3f783" dependencies = [ "proc-macro2", "quote", - "syn 1.0.100", + "syn", ] [[package]] @@ -772,7 +702,7 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ca7d52347346f5473bf2f56705f360e8440873052e575e55890c4fa57843ed3" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.8.0", "nix", "wayland-backend", "wayland-scanner", @@ -784,7 +714,7 @@ version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e253d7107ba913923dc253967f35e8561a3c65f914543e46843c88ddd729e21c" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.8.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -796,7 +726,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.8.0", "wayland-backend", "wayland-client", "wayland-protocols", diff --git a/Cargo.toml b/Cargo.toml index bbce191..b70745b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,20 +1,36 @@ [package] name = "arboard" version = "3.4.1" -authors = ["Artur Kovacs ", "Avi Weinstock ", "Arboard contributors"] +authors = [ + "Artur Kovacs ", + "Avi Weinstock ", + "Arboard contributors", +] description = "Image and text handling for the OS clipboard." repository = "https://github.com/1Password/arboard" license = "MIT OR Apache-2.0" readme = "README.md" keywords = ["clipboard", "image"] edition = "2021" -rust-version = "1.67.1" +rust-version = "1.71.0" [features] default = ["image-data"] -image-data = ["core-graphics", "image", "windows-sys"] +image-data = [ + "dep:objc2-core-graphics", + "dep:objc2-core-foundation", + "image", + "windows-sys", + "core-graphics", +] wayland-data-control = ["wl-clipboard-rs"] +# For backwards compat +core-graphics = ["dep:objc2-core-graphics"] +windows-sys = ["dep:windows-sys"] +image = ["dep:image"] +wl-clipboard-rs = ["dep:wl-clipboard-rs"] + [dependencies] [dev-dependencies] @@ -27,24 +43,50 @@ windows-sys = { version = "0.48.0", optional = true, features = [ "Win32_System_DataExchange", "Win32_System_Memory", "Win32_System_Ole", -]} +] } clipboard-win = "5.3.1" log = "0.4" -image = { version = "0.25", optional = true, default-features = false, features = ["png"] } +image = { version = "0.25", optional = true, default-features = false, features = [ + "png", +] } [target.'cfg(target_os = "macos")'.dependencies] -# Use `relax-void-encoding`, as that allows us to pass `c_void` instead of implementing `Encode` correctly for `&CGImageRef` -objc2 = { version = "0.5.1", features = ["relax-void-encoding"] } -objc2-foundation = { version = "0.2.0", features = ["NSArray", "NSString", "NSEnumerator", "NSGeometry"] } -objc2-app-kit = { version = "0.2.0", features = ["NSPasteboard", "NSPasteboardItem", "NSImage"] } -core-graphics = { version = "0.23", optional = true } -image = { version = "0.25", optional = true, default-features = false, features = ["tiff"] } +objc2 = "0.6.0" +objc2-foundation = { version = "0.3.0", default-features = false, features = [ + "std", + "NSArray", + "NSString", + "NSEnumerator", + "NSGeometry", +] } +objc2-app-kit = { version = "0.3.0", default-features = false, features = [ + "std", + "objc2-core-graphics", + "NSPasteboard", + "NSPasteboardItem", + "NSImage", +] } +objc2-core-foundation = { version = "0.3.0", default-features = false, optional = true, features = [ + "std", + "CFCGTypes", +] } +objc2-core-graphics = { version = "0.3.0", default-features = false, optional = true, features = [ + "std", + "CGImage", + "CGColorSpace", + "CGDataProvider", +] } +image = { version = "0.25", optional = true, default-features = false, features = [ + "tiff", +] } [target.'cfg(all(unix, not(any(target_os="macos", target_os="android", target_os="emscripten"))))'.dependencies] log = "0.4" x11rb = { version = "0.13" } wl-clipboard-rs = { version = "0.8", optional = true } -image = { version = "0.25", optional = true, default-features = false, features = ["png"] } +image = { version = "0.25", optional = true, default-features = false, features = [ + "png", +] } parking_lot = "0.12" [[example]] diff --git a/README.md b/README.md index b64ef8e..c4139bb 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Latest version](https://img.shields.io/crates/v/arboard?color=mediumvioletred)](https://crates.io/crates/arboard) [![Documentation](https://docs.rs/arboard/badge.svg)](https://docs.rs/arboard) -![MSRV](https://img.shields.io/badge/rustc-1.67.1+-blue.svg) +![MSRV](https://img.shields.io/badge/rustc-1.71.0+-blue.svg) ## General diff --git a/src/platform/osx.rs b/src/platform/osx.rs index 1df7222..c4c7c0d 100644 --- a/src/platform/osx.rs +++ b/src/platform/osx.rs @@ -12,8 +12,8 @@ and conditions of the chosen license apply to this file. use crate::common::ImageData; use crate::common::{private, Error}; use objc2::{ - msg_send_id, - rc::{autoreleasepool, Id}, + msg_send, + rc::{autoreleasepool, Retained}, runtime::ProtocolObject, ClassType, }; @@ -30,65 +30,67 @@ fn image_from_pixels( pixels: Vec, width: usize, height: usize, -) -> Result, Box> { - use core_graphics::{ - base::{kCGBitmapByteOrderDefault, kCGImageAlphaLast, kCGRenderingIntentDefault, CGFloat}, - color_space::CGColorSpace, - data_provider::{CGDataProvider, CustomData}, - image::{CGImage, CGImageRef}, - }; +) -> Result, Box> { + use objc2::AllocAnyThread; use objc2_app_kit::NSImage; + use objc2_core_foundation::CGFloat; + use objc2_core_graphics::{ + CGBitmapInfo, CGColorRenderingIntent, CGColorSpaceCreateDeviceRGB, + CGDataProviderCreateWithData, CGImageAlphaInfo, CGImageCreate, + }; use objc2_foundation::NSSize; - use std::ffi::c_void; + use std::{ + ffi::c_void, + ptr::{self, NonNull}, + }; - #[derive(Debug)] - struct PixelArray { - data: Vec, + unsafe extern "C-unwind" fn release(_info: *mut c_void, data: NonNull, size: usize) { + let data = data.cast::(); + let slice = NonNull::slice_from_raw_parts(data, size); + // SAFETY: This is the same slice that we got from `Box::into_raw`. + drop(unsafe { Box::from_raw(slice.as_ptr()) }) } - impl CustomData for PixelArray { - unsafe fn ptr(&self) -> *const u8 { - self.data.as_ptr() - } - unsafe fn len(&self) -> usize { - self.data.len() - } + let provider = { + let pixels = pixels.into_boxed_slice(); + let len = pixels.len(); + let pixels: *mut [u8] = Box::into_raw(pixels); + // Convert slice pointer to thin pointer. + let data_ptr = pixels.cast::(); + + // SAFETY: The data pointer and length are valid. + // The info pointer can safely be NULL, we don't use it in the `release` callback. + unsafe { CGDataProviderCreateWithData(ptr::null_mut(), data_ptr, len, Some(release)) } } + .unwrap(); - let colorspace = CGColorSpace::create_device_rgb(); - let pixel_data: Box> = Box::new(Box::new(PixelArray { data: pixels })); - let provider = unsafe { CGDataProvider::from_custom_data(pixel_data) }; + let colorspace = unsafe { CGColorSpaceCreateDeviceRGB() }.unwrap(); - let cg_image = CGImage::new( - width, - height, - 8, - 32, - 4 * width, - &colorspace, - kCGBitmapByteOrderDefault | kCGImageAlphaLast, - &provider, - false, - kCGRenderingIntentDefault, - ); - - // Convert the owned `CGImage` into a reference `&CGImageRef`, and pass - // that as `*const c_void`, since `CGImageRef` does not implement - // `RefEncode`. - let cg_image: *const CGImageRef = &*cg_image; - let cg_image: *const c_void = cg_image.cast(); + let cg_image = unsafe { + CGImageCreate( + width, + height, + 8, + 32, + 4 * width, + Some(&colorspace), + CGBitmapInfo::ByteOrderDefault | CGBitmapInfo(CGImageAlphaInfo::Last.0), + Some(&provider), + ptr::null_mut(), + false, + CGColorRenderingIntent::RenderingIntentDefault, + ) + } + .unwrap(); let size = NSSize { width: width as CGFloat, height: height as CGFloat }; - // XXX: Use `NSImage::initWithCGImage_size` once `objc2-app-kit` supports - // CoreGraphics. - let image: Id = - unsafe { msg_send_id![NSImage::alloc(), initWithCGImage: cg_image, size:size] }; + let image = unsafe { NSImage::initWithCGImage_size(NSImage::alloc(), &cg_image, size) }; Ok(image) } pub(crate) struct Clipboard { - pasteboard: Id, + pasteboard: Retained, } unsafe impl Send for Clipboard {} @@ -107,8 +109,8 @@ impl Clipboard { // documented not to. // // Otherwise we'd just use `NSPasteboard::generalPasteboard()` here. - let pasteboard: Option> = - unsafe { msg_send_id![NSPasteboard::class(), generalPasteboard] }; + let pasteboard: Option> = + unsafe { msg_send![NSPasteboard::class(), generalPasteboard] }; if let Some(pasteboard) = pasteboard { Ok(Clipboard { pasteboard }) @@ -221,7 +223,8 @@ impl<'clipboard> Get<'clipboard> { let image_data = unsafe { self.clipboard.pasteboard.dataForType(NSPasteboardTypeTIFF) } .ok_or(Error::ContentNotAvailable)?; - let data = Cursor::new(image_data.bytes()); + // SAFETY: The data is not modified while in use here. + let data = Cursor::new(unsafe { image_data.as_bytes_unchecked() }); let reader = image::io::Reader::with_format(data, image::ImageFormat::Tiff); reader.decode().map_err(|_| Error::ConversionFailure) @@ -251,8 +254,9 @@ impl<'clipboard> Set<'clipboard> { pub(crate) fn text(self, data: Cow<'_, str>) -> Result<(), Error> { self.clipboard.clear(); - let string_array = - NSArray::from_vec(vec![ProtocolObject::from_id(NSString::from_str(&data))]); + let string_array = NSArray::from_retained_slice(&[ProtocolObject::from_retained( + NSString::from_str(&data), + )]); let success = unsafe { self.clipboard.pasteboard.writeObjects(&string_array) }; add_clipboard_exclusions(self.clipboard, self.exclude_from_history); @@ -306,7 +310,7 @@ impl<'clipboard> Set<'clipboard> { self.clipboard.clear(); - let image_array = NSArray::from_vec(vec![ProtocolObject::from_id(image)]); + let image_array = NSArray::from_retained_slice(&[ProtocolObject::from_retained(image)]); let success = unsafe { self.clipboard.pasteboard.writeObjects(&image_array) }; add_clipboard_exclusions(self.clipboard, self.exclude_from_history); From c474298e4bae35665b009525234070f124f8562d Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Thu, 13 Feb 2025 14:04:33 -0600 Subject: [PATCH 47/82] Remove unneeded Result from image_from_pixels --- src/platform/osx.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/platform/osx.rs b/src/platform/osx.rs index c4c7c0d..93731fa 100644 --- a/src/platform/osx.rs +++ b/src/platform/osx.rs @@ -30,7 +30,7 @@ fn image_from_pixels( pixels: Vec, width: usize, height: usize, -) -> Result, Box> { +) -> Retained { use objc2::AllocAnyThread; use objc2_app_kit::NSImage; use objc2_core_foundation::CGFloat; @@ -84,9 +84,7 @@ fn image_from_pixels( .unwrap(); let size = NSSize { width: width as CGFloat, height: height as CGFloat }; - let image = unsafe { NSImage::initWithCGImage_size(NSImage::alloc(), &cg_image, size) }; - - Ok(image) + unsafe { NSImage::initWithCGImage_size(NSImage::alloc(), &cg_image, size) } } pub(crate) struct Clipboard { @@ -305,8 +303,7 @@ impl<'clipboard> Set<'clipboard> { #[cfg(feature = "image-data")] pub(crate) fn image(self, data: ImageData) -> Result<(), Error> { let pixels = data.bytes.into(); - let image = image_from_pixels(pixels, data.width, data.height) - .map_err(|_| Error::ConversionFailure)?; + let image = image_from_pixels(pixels, data.width, data.height); self.clipboard.clear(); From 9d6a0b9b426fb718bd4e828b8560c2ba2e5b5565 Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Thu, 13 Feb 2025 17:07:10 -0600 Subject: [PATCH 48/82] Update windows-sys version compatibility Co-authored-by: Exotik850 --- Cargo.lock | 150 +++++++++++++++++++++++++++------------- Cargo.toml | 2 +- src/platform/windows.rs | 42 ++++++++--- 3 files changed, 136 insertions(+), 58 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7b04703..32a5760 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -143,25 +143,14 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.1" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ - "errno-dragonfly", "libc", "windows-sys", ] -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - [[package]] name = "error-code" version = "3.0.0" @@ -203,7 +192,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" dependencies = [ "libc", - "windows-targets", + "windows-targets 0.48.0", ] [[package]] @@ -214,9 +203,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hermit-abi" -version = "0.3.9" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" [[package]] name = "humantime" @@ -249,12 +238,12 @@ dependencies = [ [[package]] name = "is-terminal" -version = "0.4.8" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24fddda5af7e54bf7da53067d6e802dbcc381d0a8eef629df528e3ebf68755cb" +checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37" dependencies = [ "hermit-abi", - "rustix", + "libc", "windows-sys", ] @@ -272,25 +261,25 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.148" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libloading" -version = "0.8.0" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d580318f95776505201b28cf98eb1fa5e4be3b689633ba6a3e6cd880ff22d8cb" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "windows-sys", + "windows-targets 0.48.0", ] [[package]] name = "linux-raw-sys" -version = "0.4.8" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3852614a3bd9ca9804678ba6be5e3b8ce76dfc902cae004e3e0c44051b6e88db" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "lock_api" @@ -449,9 +438,9 @@ checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" [[package]] name = "os_pipe" -version = "1.1.4" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ae859aa07428ca9a929b936690f8b12dc5f11dd8c6992a18ca93919f28bc177" +checksum = "29d73ba8daf8fac13b0501d1abeddcfe21ba7401ada61a819144b6c2a4f32209" dependencies = [ "libc", "windows-sys", @@ -469,15 +458,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.8" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -543,11 +532,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.3.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.8.0", ] [[package]] @@ -569,9 +558,9 @@ checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" [[package]] name = "rustix" -version = "0.38.21" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ "bitflags 2.8.0", "errno", @@ -611,13 +600,12 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.8.0" +version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand", - "redox_syscall", "rustix", "windows-sys", ] @@ -772,11 +760,11 @@ dependencies = [ [[package]] name = "windows-sys" -version = "0.48.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -785,13 +773,29 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -800,42 +804,90 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + [[package]] name = "windows_aarch64_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + [[package]] name = "windows_i686_gnu" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + [[package]] name = "windows_i686_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + [[package]] name = "windows_x86_64_gnu" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + [[package]] name = "windows_x86_64_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "wl-clipboard-rs" version = "0.8.0" diff --git a/Cargo.toml b/Cargo.toml index b70745b..42e7fcc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ wl-clipboard-rs = ["dep:wl-clipboard-rs"] env_logger = "0.10.2" [target.'cfg(windows)'.dependencies] -windows-sys = { version = "0.48.0", optional = true, features = [ +windows-sys = { version = ">=0.52.0, <0.60.0", optional = true, features = [ "Win32_Foundation", "Win32_Graphics_Gdi", "Win32_System_DataExchange", diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 79dca54..e736073 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -40,7 +40,7 @@ mod image_data { Error::unknown(format!("{}: {}", message, os_error)) } - unsafe fn global_unlock_checked(hdata: isize) { + unsafe fn global_unlock_checked(hdata: HGLOBAL) { // If the memory object is unlocked after decrementing the lock count, the function // returns zero and GetLastError returns NO_ERROR. If it fails, the return value is // zero and GetLastError returns a value other than NO_ERROR. @@ -119,7 +119,7 @@ mod image_data { } } - if unsafe { SetClipboardData(CF_DIBV5 as u32, hdata as _) } == 0 { + if unsafe { SetClipboardData(CF_DIBV5 as u32, hdata as _) }.failure() { unsafe { DeleteObject(hdata as _) }; Err(last_error("SetClipboardData failed with error")) } else { @@ -156,7 +156,7 @@ mod image_data { global_unlock_checked(hdata); } - if unsafe { SetClipboardData(format_id, hdata as _) } == 0 { + if unsafe { SetClipboardData(format_id, hdata as _) }.failure() { unsafe { DeleteObject(hdata as _) }; Err(last_error("SetClipboardData failed with error")) } else { @@ -166,7 +166,7 @@ mod image_data { unsafe fn global_alloc(bytes: usize) -> Result { let hdata = GlobalAlloc(GHND, bytes); - if hdata == 0 { + if hdata.is_null() { Err(last_error("Could not allocate global memory object")) } else { Ok(hdata) @@ -225,7 +225,7 @@ mod image_data { biHeight: -h, biBitCount: 32, biPlanes: 1, - biCompression: BI_RGB as u32, + biCompression: BI_RGB, biSizeImage: 0, biXPelsPerMeter: 0, biYPelsPerMeter: 0, @@ -261,8 +261,8 @@ mod image_data { fn get_screen_device_context() -> Result { // SAFETY: Calling `GetDC` with `NULL` is safe. - let hdc = unsafe { GetDC(0) }; - if hdc == 0 { + let hdc = unsafe { GetDC(ResultValue::NULL) }; + if hdc.failure() { Err(Error::unknown("Failed to get the device context. GetDC returned null")) } else { Ok(hdc) @@ -282,7 +282,7 @@ mod image_data { header as _, DIB_RGB_COLORS, ); - if hbitmap == 0 { + if hbitmap.failure() { Err(Error::unknown( "Failed to create the HBITMAP while reading DIBV5. CreateDIBitmap returned null", )) @@ -308,6 +308,32 @@ mod image_data { } } + /// An abstraction trait over the different ways a Win32 function may return + /// a value with a failure marker. + /// + /// This is primarily to abstract over changes in `windows-sys` versions and unify how + /// error handling is done in the above image code. + trait ResultValue: Sized { + const NULL: Self; + fn failure(self) -> bool; + } + + // windows-sys >= 0.59 + impl ResultValue for *mut T { + const NULL: Self = core::ptr::null_mut(); + fn failure(self) -> bool { + self == Self::NULL + } + } + + // `windows-sys` 0.52 + impl ResultValue for isize { + const NULL: Self = 0; + fn failure(self) -> bool { + self == Self::NULL + } + } + /// Converts the RGBA (u8) pixel data into the bitmap-native ARGB (u32) /// format in-place. /// From 543484f77c138950d6f8fb7ce18cbc98c9cb5c21 Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Thu, 13 Feb 2025 17:24:47 -0600 Subject: [PATCH 49/82] Check multiple windows-sys versions in CI This is to prevent accidental breakage due to the new version range --- .github/workflows/test.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 46ad856..9284f8e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -49,6 +49,12 @@ jobs: - name: Run `cargo clippy` with all features run: cargo clippy --verbose --all-features -- -D warnings -D clippy::dbg_macro + - name: Run `cargo clippy` with dependency version checks + if: ${{ matrix.rust_version == 'stable' }} + run: | + cargo update -p windows-sys + cargo clippy --verbose --all-features -- -D warnings -D clippy::dbg_macro + test: needs: clippy runs-on: ${{ matrix.os }} From e824bc8324670034ba8468650a7845914bc1912d Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Thu, 13 Feb 2025 18:01:04 -0600 Subject: [PATCH 50/82] Cleanup Windows pointer casting --- src/platform/windows.rs | 60 ++++++++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/src/platform/windows.rs b/src/platform/windows.rs index e736073..9b35c40 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -20,12 +20,12 @@ mod image_data { use image::codecs::png::PngEncoder; use image::ExtendedColorType; use image::ImageEncoder; - use std::{convert::TryInto, ffi::c_void, io, mem::size_of, ptr::copy_nonoverlapping}; + use std::{convert::TryInto, io, mem::size_of, ptr::copy_nonoverlapping}; use windows_sys::Win32::{ - Foundation::HGLOBAL, + Foundation::{HANDLE, HGLOBAL}, Graphics::Gdi::{ CreateDIBitmap, DeleteObject, GetDC, GetDIBits, BITMAPINFO, BITMAPINFOHEADER, - BITMAPV5HEADER, BI_BITFIELDS, BI_RGB, CBM_INIT, DIB_RGB_COLORS, HBITMAP, HDC, + BITMAPV5HEADER, BI_BITFIELDS, BI_RGB, CBM_INIT, DIB_RGB_COLORS, HBITMAP, HDC, HGDIOBJ, LCS_GM_IMAGES, RGBQUAD, }, System::{ @@ -102,10 +102,14 @@ mod image_data { let data_ptr = global_lock(hdata)?; let _unlock = ScopeGuard::new(|| global_unlock_checked(hdata)); - copy_nonoverlapping::((&header) as *const _ as *const u8, data_ptr, header_size); + copy_nonoverlapping::( + (&header as *const BITMAPV5HEADER).cast(), + data_ptr, + header_size, + ); // Not using the `add` function, because that has a restriction, that the result cannot overflow isize - let pixels_dst = (data_ptr as usize + header_size) as *mut u8; + let pixels_dst = data_ptr.add(header_size); copy_nonoverlapping::(image.bytes.as_ptr(), pixels_dst, image.bytes.len()); let dst_pixels_slice = std::slice::from_raw_parts_mut(pixels_dst, image.bytes.len()); @@ -119,8 +123,8 @@ mod image_data { } } - if unsafe { SetClipboardData(CF_DIBV5 as u32, hdata as _) }.failure() { - unsafe { DeleteObject(hdata as _) }; + if unsafe { SetClipboardData(CF_DIBV5 as u32, hdata as HANDLE) }.failure() { + unsafe { DeleteObject(hdata as HGDIOBJ) }; Err(last_error("SetClipboardData failed with error")) } else { Ok(()) @@ -156,8 +160,8 @@ mod image_data { global_unlock_checked(hdata); } - if unsafe { SetClipboardData(format_id, hdata as _) }.failure() { - unsafe { DeleteObject(hdata as _) }; + if unsafe { SetClipboardData(format_id, hdata as HANDLE) }.failure() { + unsafe { DeleteObject(hdata as HGDIOBJ) }; Err(last_error("SetClipboardData failed with error")) } else { Ok(()) @@ -174,7 +178,7 @@ mod image_data { } unsafe fn global_lock(hmem: HGLOBAL) -> Result<*mut u8, Error> { - let data_ptr = GlobalLock(hmem) as *mut u8; + let data_ptr = GlobalLock(hmem).cast::(); if data_ptr.is_null() { Err(last_error("Could not lock the global memory object")) } else { @@ -195,7 +199,7 @@ mod image_data { if dibv5.len() < header_size { return Err(Error::unknown("When reading the DIBV5 data, it contained fewer bytes than the BITMAPV5HEADER size. This is invalid.")); } - let header = unsafe { &*(dibv5.as_ptr() as *const BITMAPV5HEADER) }; + let header = unsafe { &*(dibv5.as_ptr().cast::()) }; let has_profile = header.bV5CSType == PROFILE_LINKED || header.bV5CSType == PROFILE_EMBEDDED; @@ -207,9 +211,9 @@ mod image_data { }; unsafe { - let image_bytes = dibv5.as_ptr().offset(pixel_data_start) as *const _; + let image_bytes = dibv5.as_ptr().offset(pixel_data_start); let hdc = get_screen_device_context()?; - let hbitmap = create_bitmap_from_dib(hdc, header as _, image_bytes)?; + let hbitmap = create_bitmap_from_dib(hdc, header, image_bytes)?; // Now extract the pixels in a desired format let w = header.bV5Width; let h = header.bV5Height.abs(); @@ -237,9 +241,9 @@ mod image_data { let lines = convert_bitmap_to_rgb( hdc, hbitmap, - h as _, - result_bytes.as_mut_ptr() as _, - &mut output_header as _, + h, + result_bytes.as_mut_slice(), + &mut output_header, )?; let read_len = lines as usize * w as usize * 4; assert!( @@ -272,14 +276,14 @@ mod image_data { unsafe fn create_bitmap_from_dib( hdc: HDC, header: *const BITMAPV5HEADER, - image_bytes: *const c_void, + image_bytes: *const u8, ) -> Result { let hbitmap = CreateDIBitmap( hdc, - header as _, + header.cast(), CBM_INIT as u32, - image_bytes, - header as _, + image_bytes.cast(), + header.cast(), DIB_RGB_COLORS, ); if hbitmap.failure() { @@ -296,11 +300,19 @@ mod image_data { unsafe fn convert_bitmap_to_rgb( hdc: HDC, hbitmap: HBITMAP, - lines: u32, - dst: *mut c_void, - header: *mut BITMAPINFO, + lines: i32, + dst: &mut [u8], + header: &mut BITMAPINFO, ) -> Result { - let lines = GetDIBits(hdc, hbitmap, 0, lines, dst, header, DIB_RGB_COLORS); + let lines = GetDIBits( + hdc, + hbitmap, + 0, + lines as u32, + dst.as_mut_ptr().cast(), + header, + DIB_RGB_COLORS, + ); if lines == 0 { Err(Error::unknown("Could not get the bitmap bits, GetDIBits returned 0")) } else { From a9c2d68c183c33d86abc0df24d7fc258f3273f08 Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Thu, 13 Feb 2025 18:41:34 -0600 Subject: [PATCH 51/82] Update wl-clipboard-rs --- Cargo.lock | 125 ++++++++++------------------------------------------- Cargo.toml | 2 +- 2 files changed, 23 insertions(+), 104 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 32a5760..9289934 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -54,12 +54,6 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" -[[package]] -name = "bytecount" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c" - [[package]] name = "bytemuck" version = "1.12.1" @@ -102,26 +96,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "derive-new" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3418329ca0ad70234b9735dc4ceed10af4df60eff9c8e7b06cb5e520d92c3535" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "dlib" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" -dependencies = [ - "libloading", -] - [[package]] name = "downcast-rs" version = "1.2.0" @@ -253,28 +227,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - [[package]] name = "libc" version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" -[[package]] -name = "libloading" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" -dependencies = [ - "cfg-if", - "windows-targets 0.48.0", -] - [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -303,15 +261,6 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" -[[package]] -name = "memoffset" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" -dependencies = [ - "autocfg", -] - [[package]] name = "minimal-lexical" version = "0.2.1" @@ -327,19 +276,6 @@ dependencies = [ "adler", ] -[[package]] -name = "nix" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" -dependencies = [ - "bitflags 1.3.2", - "cfg-if", - "libc", - "memoffset", - "pin-utils", -] - [[package]] name = "nom" version = "7.1.1" @@ -479,12 +415,6 @@ dependencies = [ "indexmap", ] -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "pkg-config" version = "0.3.25" @@ -514,9 +444,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.30.0" +version = "0.37.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" +checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003" dependencies = [ "memchr", ] @@ -569,12 +499,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - [[package]] name = "scopeguard" version = "1.1.0" @@ -652,13 +576,12 @@ dependencies = [ [[package]] name = "tree_magic_mini" -version = "3.0.3" +version = "3.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91adfd0607cacf6e4babdb870e9bec4037c1c4b151cfd279ccefc5e0c7feaa6d" +checksum = "aac5e8971f245c3389a5a76e648bfc80803ae066a1243a75db0064d7c1129d63" dependencies = [ - "bytecount", "fnv", - "lazy_static", + "memchr", "nom", "once_cell", "petgraph", @@ -672,35 +595,34 @@ checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" [[package]] name = "wayland-backend" -version = "0.3.2" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19152ddd73f45f024ed4534d9ca2594e0ef252c1847695255dae47f34df9fbe4" +checksum = "b7208998eaa3870dad37ec8836979581506e0c5c64c20c9e79e9d2a10d6f47bf" dependencies = [ "cc", "downcast-rs", - "nix", - "scoped-tls", + "rustix", "smallvec", "wayland-sys", ] [[package]] name = "wayland-client" -version = "0.31.1" +version = "0.31.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ca7d52347346f5473bf2f56705f360e8440873052e575e55890c4fa57843ed3" +checksum = "c2120de3d33638aaef5b9f4472bff75f07c56379cf76ea320bd3a3d65ecaf73f" dependencies = [ "bitflags 2.8.0", - "nix", + "rustix", "wayland-backend", "wayland-scanner", ] [[package]] name = "wayland-protocols" -version = "0.31.0" +version = "0.32.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e253d7107ba913923dc253967f35e8561a3c65f914543e46843c88ddd729e21c" +checksum = "0781cf46869b37e36928f7b432273c0995aa8aed9552c556fb18754420541efc" dependencies = [ "bitflags 2.8.0", "wayland-backend", @@ -710,9 +632,9 @@ dependencies = [ [[package]] name = "wayland-protocols-wlr" -version = "0.2.0" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" +checksum = "248a02e6f595aad796561fa82d25601bd2c8c3b145b1c7453fc8f94c1a58f8b2" dependencies = [ "bitflags 2.8.0", "wayland-backend", @@ -723,9 +645,9 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.0" +version = "0.31.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb8e28403665c9f9513202b7e1ed71ec56fde5c107816843fb14057910b2c09c" +checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484" dependencies = [ "proc-macro2", "quick-xml", @@ -734,12 +656,10 @@ dependencies = [ [[package]] name = "wayland-sys" -version = "0.31.1" +version = "0.31.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15a0c8eaff5216d07f226cb7a549159267f3467b289d9a2e52fd3ef5aae2b7af" +checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615" dependencies = [ - "dlib", - "log", "pkg-config", ] @@ -890,15 +810,14 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "wl-clipboard-rs" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57af79e973eadf08627115c73847392e6b766856ab8e3844a59245354b23d2fa" +checksum = "4de22eebb1d1e2bad2d970086e96da0e12cde0b411321e5b0f7b2a1f876aa26f" dependencies = [ - "derive-new", "libc", "log", - "nix", "os_pipe", + "rustix", "tempfile", "thiserror", "tree_magic_mini", diff --git a/Cargo.toml b/Cargo.toml index 42e7fcc..de3bf0d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -83,7 +83,7 @@ image = { version = "0.25", optional = true, default-features = false, features [target.'cfg(all(unix, not(any(target_os="macos", target_os="android", target_os="emscripten"))))'.dependencies] log = "0.4" x11rb = { version = "0.13" } -wl-clipboard-rs = { version = "0.8", optional = true } +wl-clipboard-rs = { version = "0.9.0", optional = true } image = { version = "0.25", optional = true, default-features = false, features = [ "png", ] } From bcb2954db5ac03cc6703443687018b2ed6a7b0b5 Mon Sep 17 00:00:00 2001 From: Gae24 <96017547+Gae24@users.noreply.github.com> Date: Sat, 8 Mar 2025 22:37:01 +0100 Subject: [PATCH 52/82] use Error::unknown on linux and osx too --- src/common.rs | 1 - src/platform/linux/wayland.rs | 4 ++-- src/platform/linux/x11.rs | 24 ++++++------------------ src/platform/osx.rs | 17 +++++++---------- 4 files changed, 15 insertions(+), 31 deletions(-) diff --git a/src/common.rs b/src/common.rs index ab00b9c..8815016 100644 --- a/src/common.rs +++ b/src/common.rs @@ -90,7 +90,6 @@ impl std::fmt::Debug for Error { } impl Error { - #[cfg(windows)] pub(crate) fn unknown>(message: M) -> Self { Error::Unknown { description: message.into() } } diff --git a/src/platform/linux/wayland.rs b/src/platform/linux/wayland.rs index 9cd12bf..3b87d57 100644 --- a/src/platform/linux/wayland.rs +++ b/src/platform/linux/wayland.rs @@ -72,7 +72,7 @@ impl Clipboard { Err(PasteError::PrimarySelectionUnsupported) => Err(Error::ClipboardNotSupported), - Err(err) => Err(Error::Unknown { description: err.to_string() }), + Err(err) => Err(into_unknown(err)), } } @@ -163,7 +163,7 @@ impl Clipboard { Err(Error::ContentNotAvailable) } - Err(err) => Err(Error::Unknown { description: err.to_string() }), + Err(err) => Err(into_unknown(err)), } } diff --git a/src/platform/linux/x11.rs b/src/platform/linux/x11.rs index c221c2d..8099c0d 100644 --- a/src/platform/linux/x11.rs +++ b/src/platform/linux/x11.rs @@ -134,16 +134,10 @@ impl XContext { fn new() -> Result { // create a new connection to an X11 server let (conn, screen_num): (RustConnection, _) = - RustConnection::connect(None).map_err(|_| Error::Unknown { - description: String::from( - "X11 server connection timed out because it was unreachable", - ), + RustConnection::connect(None).map_err(|_| { + Error::unknown("X11 server connection timed out because it was unreachable") })?; - let screen = conn - .setup() - .roots - .get(screen_num) - .ok_or(Error::Unknown { description: String::from("no screen found") })?; + let screen = conn.setup().roots.get(screen_num).ok_or(Error::unknown("no screen found"))?; let win_id = conn.generate_id().map_err(into_unknown)?; let event_mask = @@ -225,9 +219,7 @@ impl Inner { wait: WaitConfig, ) -> Result<()> { if self.serve_stopped.load(Ordering::Relaxed) { - return Err(Error::Unknown { - description: "The clipboard handler thread seems to have stopped. Logging messages may reveal the cause. (See the `log` crate.)".into() - }); + return Err(Error::unknown("The clipboard handler thread seems to have stopped. Logging messages may reveal the cause. (See the `log` crate.)")); } let server_win = self.server.win_id; @@ -527,9 +519,7 @@ impl Inner { Ok(ReadSelNotifyResult::IncrStarted) } else { // this should never happen, we have sent a request only for supported types - Err(Error::Unknown { - description: String::from("incorrect type received from clipboard"), - }) + Err(Error::unknown("incorrect type received from clipboard")) } } @@ -714,9 +704,7 @@ impl Inner { return Ok(()); } - Err(Error::Unknown { - description: "The handover was not finished and the condvar didn't time out, yet the condvar wait ended. This should be unreachable.".into() - }) + Err(Error::unknown("The handover was not finished and the condvar didn't time out, yet the condvar wait ended. This should be unreachable.")) } } diff --git a/src/platform/osx.rs b/src/platform/osx.rs index 93731fa..b5a4ce2 100644 --- a/src/platform/osx.rs +++ b/src/platform/osx.rs @@ -128,9 +128,8 @@ impl Clipboard { // XXX: We explicitly use `pasteboardItems` and not `stringForType` since the latter will concat // multiple strings, if present, into one and return it instead of reading just the first which is `arboard`'s // historical behavior. - let contents = unsafe { self.pasteboard.pasteboardItems() }.ok_or_else(|| { - Error::Unknown { description: String::from("NSPasteboard#pasteboardItems errored") } - })?; + let contents = unsafe { self.pasteboard.pasteboardItems() } + .ok_or_else(|| Error::unknown("NSPasteboard#pasteboardItems errored"))?; for item in contents { if let Some(string) = unsafe { item.stringForType(type_) } { @@ -262,7 +261,7 @@ impl<'clipboard> Set<'clipboard> { if success { Ok(()) } else { - Err(Error::Unknown { description: "NSPasteboard#writeObjects: returned false".into() }) + Err(Error::unknown("NSPasteboard#writeObjects: returned false")) } } @@ -296,7 +295,7 @@ impl<'clipboard> Set<'clipboard> { if success { Ok(()) } else { - Err(Error::Unknown { description: "NSPasteboard#writeObjects: returned false".into() }) + Err(Error::unknown("NSPasteboard#writeObjects: returned false")) } } @@ -315,11 +314,9 @@ impl<'clipboard> Set<'clipboard> { if success { Ok(()) } else { - Err(Error::Unknown { - description: - "Failed to write the image to the pasteboard (`writeObjects` returned NO)." - .into(), - }) + Err(Error::unknown( + "Failed to write the image to the pasteboard (`writeObjects` returned NO).", + )) } } } From 1dcc18b221a52d93342db7ba9b1340c10af6abd1 Mon Sep 17 00:00:00 2001 From: Gae24 <96017547+Gae24@users.noreply.github.com> Date: Sun, 23 Mar 2025 10:08:18 +0100 Subject: [PATCH 53/82] Add `file_list` to `Get` interface (#179) --- Cargo.lock | 7 ++++++ Cargo.toml | 4 +++- src/lib.rs | 7 +++++- src/platform/linux/mod.rs | 45 ++++++++++++++++++++++++++++++++++- src/platform/linux/wayland.rs | 13 +++++++--- src/platform/linux/x11.rs | 12 +++++++++- src/platform/osx.rs | 37 ++++++++++++++++++++++++++-- src/platform/windows.rs | 12 +++++++++- 8 files changed, 127 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9289934..540ed6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31,6 +31,7 @@ dependencies = [ "objc2-core-graphics", "objc2-foundation", "parking_lot", + "percent-encoding", "windows-sys", "wl-clipboard-rs", "x11rb", @@ -405,6 +406,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + [[package]] name = "petgraph" version = "0.6.2" diff --git a/Cargo.toml b/Cargo.toml index de3bf0d..dfe4399 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,7 @@ windows-sys = { version = ">=0.52.0, <0.60.0", optional = true, features = [ "Win32_System_Memory", "Win32_System_Ole", ] } -clipboard-win = "5.3.1" +clipboard-win = { version = "5.3.1", features = ["std"] } log = "0.4" image = { version = "0.25", optional = true, default-features = false, features = [ "png", @@ -58,6 +58,7 @@ objc2-foundation = { version = "0.3.0", default-features = false, features = [ "NSString", "NSEnumerator", "NSGeometry", + "NSValue", ] } objc2-app-kit = { version = "0.3.0", default-features = false, features = [ "std", @@ -88,6 +89,7 @@ image = { version = "0.25", optional = true, default-features = false, features "png", ] } parking_lot = "0.12" +percent-encoding = "2.3.1" [[example]] name = "get_image" diff --git a/src/lib.rs b/src/lib.rs index c002bdc..4702827 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,7 +10,7 @@ and conditions of the chosen license apply to this file. #![warn(unreachable_pub)] mod common; -use std::borrow::Cow; +use std::{borrow::Cow, path::PathBuf}; pub use common::Error; #[cfg(feature = "image-data")] @@ -197,6 +197,11 @@ impl Get<'_> { pub fn html(self) -> Result { self.platform.html() } + + /// Completes the "get" operation by fetching a list of file paths from the clipboard. + pub fn file_list(self) -> Result, Error> { + self.platform.file_list() + } } /// A builder for an operation that sets a value to the clipboard. diff --git a/src/platform/linux/mod.rs b/src/platform/linux/mod.rs index 4ce13c1..63b4f9f 100644 --- a/src/platform/linux/mod.rs +++ b/src/platform/linux/mod.rs @@ -1,7 +1,8 @@ -use std::{borrow::Cow, time::Instant}; +use std::{borrow::Cow, path::PathBuf, time::Instant}; #[cfg(feature = "wayland-data-control")] use log::{trace, warn}; +use percent_encoding::percent_decode_str; #[cfg(feature = "image-data")] use crate::ImageData; @@ -38,6 +39,15 @@ fn encode_as_png(image: &ImageData) -> Result, Error> { Ok(png_bytes) } +fn paths_from_uri_list(uri_list: String) -> Vec { + uri_list + .lines() + .filter_map(|s| s.strip_prefix("file://")) + .filter_map(|s| percent_decode_str(s).decode_utf8().ok()) + .map(|decoded| PathBuf::from(decoded.as_ref())) + .collect() +} + /// Clipboard selection /// /// Linux has a concept of clipboard "selections" which tend to be used in different contexts. This @@ -130,6 +140,14 @@ impl<'clipboard> Get<'clipboard> { Clipboard::WlDataControl(clipboard) => clipboard.get_html(self.selection), } } + + pub(crate) fn file_list(self) -> Result, Error> { + match self.clipboard { + Clipboard::X11(clipboard) => clipboard.get_file_list(self.selection), + #[cfg(feature = "wayland-data-control")] + Clipboard::WlDataControl(clipboard) => clipboard.get_file_list(self.selection), + } + } } /// Linux-specific extensions to the [`Get`](super::Get) builder. @@ -330,3 +348,28 @@ impl ClearExtLinux for crate::Clear<'_> { self.platform.clear_inner(selection) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_decoding_uri_list() { + // Test that paths_from_uri_list correctly decodes + // differents percent encoded characters + let file_list = vec![ + "file:///tmp/bar.log", + "file:///tmp/test%5C.txt", + "file:///tmp/foo%3F.png", + "file:///tmp/white%20space.txt", + ]; + + let paths = vec![ + PathBuf::from("/tmp/bar.log"), + PathBuf::from("/tmp/test\\.txt"), + PathBuf::from("/tmp/foo?.png"), + PathBuf::from("/tmp/white space.txt"), + ]; + assert_eq!(paths_from_uri_list(file_list.join("\n")), paths); + } +} diff --git a/src/platform/linux/wayland.rs b/src/platform/linux/wayland.rs index 3b87d57..b554c9c 100644 --- a/src/platform/linux/wayland.rs +++ b/src/platform/linux/wayland.rs @@ -1,5 +1,4 @@ -use std::borrow::Cow; -use std::io::Read; +use std::{borrow::Cow, io::Read, path::PathBuf}; use wl_clipboard_rs::{ copy::{self, Error as CopyError, MimeSource, MimeType, Options, Source}, @@ -9,7 +8,7 @@ use wl_clipboard_rs::{ #[cfg(feature = "image-data")] use super::encode_as_png; -use super::{into_unknown, LinuxClipboardKind, WaitConfig}; +use super::{into_unknown, paths_from_uri_list, LinuxClipboardKind, WaitConfig}; use crate::common::Error; #[cfg(feature = "image-data")] use crate::common::ImageData; @@ -182,4 +181,12 @@ impl Clipboard { opts.copy(source, MimeType::Specific(MIME_PNG.into())).map_err(into_unknown)?; Ok(()) } + + pub(crate) fn get_file_list( + &mut self, + selection: LinuxClipboardKind, + ) -> Result, Error> { + self.string_for_mime(selection, paste::MimeType::Specific("text/uri-list")) + .map(paths_from_uri_list) + } } diff --git a/src/platform/linux/x11.rs b/src/platform/linux/x11.rs index 8099c0d..4ac5977 100644 --- a/src/platform/linux/x11.rs +++ b/src/platform/linux/x11.rs @@ -16,6 +16,7 @@ use std::{ borrow::Cow, cell::RefCell, collections::{hash_map::Entry, HashMap}, + path::PathBuf, sync::{ atomic::{AtomicBool, Ordering}, Arc, @@ -44,7 +45,7 @@ use x11rb::{ #[cfg(feature = "image-data")] use super::encode_as_png; -use super::{into_unknown, LinuxClipboardKind, WaitConfig}; +use super::{into_unknown, paths_from_uri_list, LinuxClipboardKind, WaitConfig}; #[cfg(feature = "image-data")] use crate::ImageData; use crate::{common::ScopeGuard, Error}; @@ -77,6 +78,7 @@ x11rb::atom_manager! { TEXT_MIME_UNKNOWN: b"text/plain", HTML: b"text/html", + URI_LIST: b"text/uri-list", PNG_MIME: b"image/png", @@ -929,6 +931,14 @@ impl Clipboard { let data = vec![ClipboardData { bytes: encoded, format: self.inner.atoms.PNG_MIME }]; self.inner.write(data, selection, wait) } + + pub(crate) fn get_file_list(&self, selection: LinuxClipboardKind) -> Result> { + let result = self.inner.read(&[self.inner.atoms.URI_LIST], selection)?; + + String::from_utf8(result.bytes) + .map_err(|_| Error::ConversionFailure) + .map(paths_from_uri_list) + } } impl Drop for Clipboard { diff --git a/src/platform/osx.rs b/src/platform/osx.rs index b5a4ce2..8637e50 100644 --- a/src/platform/osx.rs +++ b/src/platform/osx.rs @@ -17,11 +17,15 @@ use objc2::{ runtime::ProtocolObject, ClassType, }; -use objc2_app_kit::{NSPasteboard, NSPasteboardTypeHTML, NSPasteboardTypeString}; -use objc2_foundation::{ns_string, NSArray, NSString}; +use objc2_app_kit::{ + NSPasteboard, NSPasteboardTypeHTML, NSPasteboardTypeString, + NSPasteboardURLReadingFileURLsOnlyKey, +}; +use objc2_foundation::{ns_string, NSArray, NSDictionary, NSNumber, NSString, NSURL}; use std::{ borrow::Cow, panic::{RefUnwindSafe, UnwindSafe}, + path::PathBuf, }; /// Returns an NSImage object on success. @@ -236,6 +240,35 @@ impl<'clipboard> Get<'clipboard> { bytes: rgba.into_raw().into(), }) } + + pub(crate) fn file_list(self) -> Result, Error> { + autoreleasepool(|_| { + let class_array = NSArray::from_slice(&[NSURL::class()]); + let options = NSDictionary::from_slices( + &[unsafe { NSPasteboardURLReadingFileURLsOnlyKey }], + &[NSNumber::new_bool(true).as_ref()], + ); + let objects = unsafe { + self.clipboard + .pasteboard + .readObjectsForClasses_options(&class_array, Some(&options)) + }; + + objects + .map(|array| { + array + .iter() + .filter_map(|obj| { + obj.downcast::().ok().and_then(|url| { + unsafe { url.path() }.map(|p| PathBuf::from(p.to_string())) + }) + }) + .collect::>() + }) + .filter(|file_list| !file_list.is_empty()) + .ok_or(Error::ContentNotAvailable) + }) + } } pub(crate) struct Set<'clipboard> { diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 9b35c40..9e4563e 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -11,7 +11,7 @@ and conditions of the chosen license apply to this file. #[cfg(feature = "image-data")] use crate::common::ImageData; use crate::common::{private, Error}; -use std::{borrow::Cow, marker::PhantomData, thread, time::Duration}; +use std::{borrow::Cow, marker::PhantomData, path::PathBuf, thread, time::Duration}; #[cfg(feature = "image-data")] mod image_data { @@ -635,6 +635,16 @@ impl<'clipboard> Get<'clipboard> { image_data::read_cf_dibv5(&data) } + + pub(crate) fn file_list(self) -> Result, Error> { + let _clipboard_assertion = self.clipboard?; + + let mut file_list = Vec::new(); + clipboard_win::raw::get_file_list_path(&mut file_list) + .map_err(|_| Error::ContentNotAvailable)?; + + Ok(file_list) + } } pub(crate) struct Set<'clipboard> { From 91c33159b019b636f0a0419557d5d736196f9681 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Art=C3=BAr=20Kov=C3=A1cs?= Date: Thu, 3 Apr 2025 04:17:08 +0200 Subject: [PATCH 54/82] Release v3.5 (#183) * Release v3.5 * Update CHANGELOG for 3.5 release --------- Co-authored-by: ComplexSpaces --- CHANGELOG.md | 8 ++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95f0d66..1a68696 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ # Changelog +## 3.5.0 on 2025-04-01 + +### Added +- Add `file_list` to the `Get` interface. +- Implement `Get::html()` for all platforms. + ### Changed +- Updated `wl-clipboard-rs` to `0.9`. +- Improved `windows-sys` version compatibility range to support `0.52` - `0.60`. - Updated `objc2` to `v0.6`. - Raised MSRV to 1.71.0. diff --git a/Cargo.lock b/Cargo.lock index 540ed6d..472a00d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,7 +19,7 @@ dependencies = [ [[package]] name = "arboard" -version = "3.4.1" +version = "3.5.0" dependencies = [ "clipboard-win", "env_logger", diff --git a/Cargo.toml b/Cargo.toml index dfe4399..c598799 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "arboard" -version = "3.4.1" +version = "3.5.0" authors = [ "Artur Kovacs ", "Avi Weinstock ", From b5e123032c10ac2c46c187468ec94b0663c4f0cd Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Sun, 15 Jun 2025 14:37:11 -0500 Subject: [PATCH 55/82] Add exclude_from_history on linux by setting x-kde-passwordManagerHint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses #129 Co-authored-by: MrSmör <66489839+MrSmoer@users.noreply.github.com> --- src/platform/linux/mod.rs | 50 ++++++++++--- src/platform/linux/wayland.rs | 83 +++++++++++++++------ src/platform/linux/x11.rs | 131 ++++++++++++++++++++++++++++++---- 3 files changed, 222 insertions(+), 42 deletions(-) diff --git a/src/platform/linux/mod.rs b/src/platform/linux/mod.rs index 63b4f9f..a10fb42 100644 --- a/src/platform/linux/mod.rs +++ b/src/platform/linux/mod.rs @@ -8,6 +8,10 @@ use percent_encoding::percent_decode_str; use crate::ImageData; use crate::{common::private, Error}; +// Magic strings used in `Set::exclude_from_history()` on linux +const KDE_EXCLUSION_MIME: &str = "x-kde-passwordManagerHint"; +const KDE_EXCLUSION_HINT: &[u8] = b"secret"; + mod x11; #[cfg(feature = "wayland-data-control")] @@ -184,38 +188,56 @@ pub(crate) struct Set<'clipboard> { clipboard: &'clipboard mut Clipboard, wait: WaitConfig, selection: LinuxClipboardKind, + exclude_from_history: bool, } impl<'clipboard> Set<'clipboard> { pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self { - Self { clipboard, wait: WaitConfig::default(), selection: LinuxClipboardKind::Clipboard } + Self { + clipboard, + wait: WaitConfig::default(), + selection: LinuxClipboardKind::Clipboard, + exclude_from_history: false, + } } pub(crate) fn text(self, text: Cow<'_, str>) -> Result<(), Error> { match self.clipboard { - Clipboard::X11(clipboard) => clipboard.set_text(text, self.selection, self.wait), + Clipboard::X11(clipboard) => { + clipboard.set_text(text, self.selection, self.wait, self.exclude_from_history) + } #[cfg(feature = "wayland-data-control")] - Clipboard::WlDataControl(clipboard) => clipboard.set_text(text, self.selection, self.wait), + Clipboard::WlDataControl(clipboard) => { + clipboard.set_text(text, self.selection, self.wait, self.exclude_from_history) + } } } pub(crate) fn html(self, html: Cow<'_, str>, alt: Option>) -> Result<(), Error> { match self.clipboard { - Clipboard::X11(clipboard) => clipboard.set_html(html, alt, self.selection, self.wait), + Clipboard::X11(clipboard) => { + clipboard.set_html(html, alt, self.selection, self.wait, self.exclude_from_history) + } #[cfg(feature = "wayland-data-control")] - Clipboard::WlDataControl(clipboard) => clipboard.set_html(html, alt, self.selection, self.wait), + Clipboard::WlDataControl(clipboard) => { + clipboard.set_html(html, alt, self.selection, self.wait, self.exclude_from_history) + } } } #[cfg(feature = "image-data")] pub(crate) fn image(self, image: ImageData<'_>) -> Result<(), Error> { match self.clipboard { - Clipboard::X11(clipboard) => clipboard.set_image(image, self.selection, self.wait), + Clipboard::X11(clipboard) => { + clipboard.set_image(image, self.selection, self.wait, self.exclude_from_history) + } #[cfg(feature = "wayland-data-control")] - Clipboard::WlDataControl(clipboard) => clipboard.set_image(image, self.selection, self.wait), + Clipboard::WlDataControl(clipboard) => { + clipboard.set_image(image, self.selection, self.wait, self.exclude_from_history) + } } } } @@ -280,6 +302,13 @@ pub trait SetExtLinux: private::Sealed { /// # } /// ``` fn clipboard(self, selection: LinuxClipboardKind) -> Self; + + /// Excludes the data which will be set on the clipboard from being added to + /// the desktop clipboard managers' histories by adding the MIME-Type `x-kde-passwordMangagerHint` + /// to the clipboard's selection data. + /// + /// This is the most widely adopted convention on Linux. + fn exclude_from_history(self) -> Self; } impl SetExtLinux for crate::Set<'_> { @@ -297,6 +326,11 @@ impl SetExtLinux for crate::Set<'_> { self.platform.wait = WaitConfig::Until(deadline); self } + + fn exclude_from_history(mut self) -> Self { + self.platform.exclude_from_history = true; + self + } } pub(crate) struct Clear<'clipboard> { @@ -357,7 +391,7 @@ mod tests { fn test_decoding_uri_list() { // Test that paths_from_uri_list correctly decodes // differents percent encoded characters - let file_list = vec![ + let file_list = [ "file:///tmp/bar.log", "file:///tmp/test%5C.txt", "file:///tmp/foo%3F.png", diff --git a/src/platform/linux/wayland.rs b/src/platform/linux/wayland.rs index b554c9c..3dd4ba5 100644 --- a/src/platform/linux/wayland.rs +++ b/src/platform/linux/wayland.rs @@ -8,7 +8,10 @@ use wl_clipboard_rs::{ #[cfg(feature = "image-data")] use super::encode_as_png; -use super::{into_unknown, paths_from_uri_list, LinuxClipboardKind, WaitConfig}; +use super::{ + into_unknown, paths_from_uri_list, LinuxClipboardKind, WaitConfig, KDE_EXCLUSION_HINT, + KDE_EXCLUSION_MIME, +}; use crate::common::Error; #[cfg(feature = "image-data")] use crate::common::ImageData; @@ -42,6 +45,15 @@ impl TryInto for LinuxClipboardKind { } } +fn add_clipboard_exclusions(exclude_from_history: bool, sources: &mut Vec) { + if exclude_from_history { + sources.push(MimeSource { + source: Source::Bytes(Box::from(KDE_EXCLUSION_HINT)), + mime_type: MimeType::Specific(String::from(KDE_EXCLUSION_MIME)), + }); + } +} + impl Clipboard { #[allow(clippy::unnecessary_wraps)] pub(crate) fn new() -> Result { @@ -84,12 +96,22 @@ impl Clipboard { text: Cow<'_, str>, selection: LinuxClipboardKind, wait: WaitConfig, + exclude_from_history: bool, ) -> Result<(), Error> { let mut opts = Options::new(); opts.foreground(matches!(wait, WaitConfig::Forever)); opts.clipboard(selection.try_into()?); - let source = Source::Bytes(text.into_owned().into_bytes().into_boxed_slice()); - opts.copy(source, MimeType::Text).map_err(|e| match e { + + let mut sources = Vec::with_capacity(if exclude_from_history { 2 } else { 1 }); + + sources.push(MimeSource { + source: Source::Bytes(text.into_owned().into_bytes().into_boxed_slice()), + mime_type: MimeType::Text, + }); + + add_clipboard_exclusions(exclude_from_history, &mut sources); + + opts.copy_multi(sources).map_err(|e| match e { CopyError::PrimarySelectionUnsupported => Error::ClipboardNotSupported, other => into_unknown(other), })?; @@ -106,24 +128,35 @@ impl Clipboard { alt: Option>, selection: LinuxClipboardKind, wait: WaitConfig, + exclude_from_history: bool, ) -> Result<(), Error> { - let html_mime = MimeType::Specific(String::from("text/html")); let mut opts = Options::new(); opts.foreground(matches!(wait, WaitConfig::Forever)); opts.clipboard(selection.try_into()?); - let html_source = Source::Bytes(html.into_owned().into_bytes().into_boxed_slice()); - match alt { - Some(alt_text) => { - let alt_source = - Source::Bytes(alt_text.into_owned().into_bytes().into_boxed_slice()); - opts.copy_multi(vec![ - MimeSource { source: alt_source, mime_type: MimeType::Text }, - MimeSource { source: html_source, mime_type: html_mime }, - ]) - } - None => opts.copy(html_source, html_mime), + + let mut sources = { + let cap = [true, alt.is_some(), exclude_from_history] + .map(|v| usize::from(v as u8)) + .iter() + .sum(); + Vec::with_capacity(cap) + }; + + if let Some(alt) = alt { + sources.push(MimeSource { + source: Source::Bytes(alt.into_owned().into_bytes().into_boxed_slice()), + mime_type: MimeType::Text, + }); } - .map_err(|e| match e { + + sources.push(MimeSource { + source: Source::Bytes(html.into_owned().into_bytes().into_boxed_slice()), + mime_type: MimeType::Specific(String::from("text/html")), + }); + + add_clipboard_exclusions(exclude_from_history, &mut sources); + + opts.copy_multi(sources).map_err(|e| match e { CopyError::PrimarySelectionUnsupported => Error::ClipboardNotSupported, other => into_unknown(other), })?; @@ -172,14 +205,24 @@ impl Clipboard { image: ImageData, selection: LinuxClipboardKind, wait: WaitConfig, + exclude_from_history: bool, ) -> Result<(), Error> { - let image = encode_as_png(&image)?; let mut opts = Options::new(); opts.foreground(matches!(wait, WaitConfig::Forever)); opts.clipboard(selection.try_into()?); - let source = Source::Bytes(image.into()); - opts.copy(source, MimeType::Specific(MIME_PNG.into())).map_err(into_unknown)?; - Ok(()) + + let image = encode_as_png(&image)?; + + let mut sources = Vec::with_capacity(if exclude_from_history { 2 } else { 1 }); + + sources.push(MimeSource { + source: Source::Bytes(image.into()), + mime_type: MimeType::Specific(String::from(MIME_PNG)), + }); + + add_clipboard_exclusions(exclude_from_history, &mut sources); + + opts.copy_multi(sources).map_err(into_unknown) } pub(crate) fn get_file_list( diff --git a/src/platform/linux/x11.rs b/src/platform/linux/x11.rs index 4ac5977..e28bf44 100644 --- a/src/platform/linux/x11.rs +++ b/src/platform/linux/x11.rs @@ -45,7 +45,10 @@ use x11rb::{ #[cfg(feature = "image-data")] use super::encode_as_png; -use super::{into_unknown, paths_from_uri_list, LinuxClipboardKind, WaitConfig}; +use super::{ + into_unknown, paths_from_uri_list, LinuxClipboardKind, WaitConfig, KDE_EXCLUSION_HINT, + KDE_EXCLUSION_MIME, +}; #[cfg(feature = "image-data")] use crate::ImageData; use crate::{common::ScopeGuard, Error}; @@ -81,6 +84,7 @@ x11rb::atom_manager! { URI_LIST: b"text/uri-list", PNG_MIME: b"image/png", + X_KDE_PASSWORDMANAGERHINT: KDE_EXCLUSION_MIME.as_bytes(), // This is just some random name for the property on our window, into which // the clipboard owner writes the data we requested. @@ -577,11 +581,13 @@ impl Inner { // we are asked for a list of supported conversion targets if event.target == self.atoms.TARGETS { trace!("Handling TARGETS, dst property is {}", self.atom_name_dbg(event.property)); - let mut targets = Vec::with_capacity(10); - targets.push(self.atoms.TARGETS); - targets.push(self.atoms.SAVE_TARGETS); + let data = self.selection_of(selection).data.read(); - if let Some(data_list) = &*data { + let (data_targets, excluded) = if let Some(data_list) = &*data { + // Estimation based on current data types, plus the other UTF-8 ones, plus `SAVE_TARGETS`. + let mut targets = Vec::with_capacity(data_list.len() + 3); + let mut excluded = false; + for data in data_list { targets.push(data.format); if data.format == self.atoms.UTF8_STRING { @@ -590,8 +596,32 @@ impl Inner { targets.push(self.atoms.UTF8_MIME_0); targets.push(self.atoms.UTF8_MIME_1); } + + if data.format == self.atoms.X_KDE_PASSWORDMANAGERHINT { + excluded = true; + } } + (targets, excluded) + } else { + // If there's no data, we advertise an empty list of targets. + (Vec::with_capacity(2), false) + }; + + let mut targets = data_targets; + targets.push(self.atoms.TARGETS); + + // NB: `SAVE_TARGETS` in this context is a marker atom which infomrs the clipboard manager + // we support this operation and _may_ use it in the future. To try and keep the manager's + // expectations/assumptions (if any) about when we will invoke this handoff, we go ahead and + // skip advertising support for the save operation entirely when the data was marked as + // sensitive. + // + // Note that even if we don't advertise it, some managers may respond to it anyways so this is + // only half of exclusion handling. See `ask_clipboard_manager_to_request_our_data` for more. + if !excluded { + targets.push(self.atoms.SAVE_TARGETS); } + self.server .conn .change_property32( @@ -664,13 +694,55 @@ impl Inner { return Ok(()); } - if !self.is_owner(LinuxClipboardKind::Clipboard)? { + // Per the `ClipboardManager` specification, only the `CLIPBOARD` target is + // to be saved from other X clients, so if the caller set the `Primary` (or `Secondary`) clipboard, + // we wouldn't expect any clipboard manager to save that anyway. + let selection = LinuxClipboardKind::Clipboard; + + if !self.is_owner(selection)? { // We are not owning the clipboard, nothing to do. return Ok(()); } - if self.selection_of(LinuxClipboardKind::Clipboard).data.read().is_none() { - // If we don't have any data, there's nothing to do. - return Ok(()); + + match &*self.selection_of(selection).data.read() { + Some(data) => { + // If the data we are serving intended to be excluded, then don't bother asking the clipboard + // manager to save it. This is for several reasons: + // 1. Its counter-intuitive because the caller asked for this data to be minimally retained. + // 2. Regardless of if `SAVE_TARGETS` was advertised, we have to assume the manager may be saving history + // in a more proactive way and that would also be entirely dependent on it seeing the exclusion MIME before this. + // 3. Due to varying behavior in clipboard managers (some save prior to `SAVE_TARGETS`), it may just + // generate unnessecary warning logs in our handoff path even when we know a well-behaving manager isn't + // trying to save our sensitive data and that is misleading to users. + if data.iter().any(|data| data.format == self.atoms.X_KDE_PASSWORDMANAGERHINT) { + // This step is the most important. Without it, some clipboard managers may think that our process + // crashed since the X window is destroyed without changing the selection owner first and try to save data. + // + // While this shouldn't need to happen based only on ICCCM 2.3.1 ("Voluntarily Giving Up Selection Ownership"), + // its documentation that destorying the owner window or terminating also reverts the owner to `None` doesn't + // reflect how desktop environment's X servers work in reality. + // + // By removing the owner, the manager doesn't think it needs to pick up our window's data serving once + // its destroyed and cleanly lets the data disappear based off the previously advertised exclusion hint. + if let Err(e) = self.server.conn.set_selection_owner( + NONE, + self.atoms.CLIPBOARD, + Time::CURRENT_TIME, + ) { + warn!("failed to release sensitive data's clipboard ownership: {e}; it may end up persisted!"); + // This is still not an error because we werent going to handoff anything to the manager. + return Ok(()); + } + + // It doesn't matter if this fails, the clipboard window will be destroyed regardless. + let _ = self.server.conn.flush(); + return Ok(()); + } + } + None => { + // If we don't have any data, there's nothing to do. + return Ok(()); + } } // It's important that we lock the state before sending the request @@ -706,7 +778,7 @@ impl Inner { return Ok(()); } - Err(Error::unknown("The handover was not finished and the condvar didn't time out, yet the condvar wait ended. This should be unreachable.")) + unreachable!("This is a bug! The handover was not finished and the condvar didn't time out, yet the condvar wait ended.") } } @@ -843,6 +915,15 @@ impl Clipboard { Ok(Self { inner: ctx }) } + fn add_clipboard_exclusions(&self, exclude_from_history: bool, data: &mut Vec) { + if exclude_from_history { + data.push(ClipboardData { + bytes: KDE_EXCLUSION_HINT.to_vec(), + format: self.inner.atoms.X_KDE_PASSWORDMANAGERHINT, + }) + } + } + pub(crate) fn get_text(&self, selection: LinuxClipboardKind) -> Result { let formats = [ self.inner.atoms.UTF8_STRING, @@ -867,11 +948,16 @@ impl Clipboard { message: Cow<'_, str>, selection: LinuxClipboardKind, wait: WaitConfig, + exclude_from_history: bool, ) -> Result<()> { - let data = vec![ClipboardData { + let mut data = Vec::with_capacity(if exclude_from_history { 2 } else { 1 }); + data.push(ClipboardData { bytes: message.into_owned().into_bytes(), format: self.inner.atoms.UTF8_STRING, - }]; + }); + + self.add_clipboard_exclusions(exclude_from_history, &mut data); + self.inner.write(data, selection, wait) } @@ -887,8 +973,16 @@ impl Clipboard { alt: Option>, selection: LinuxClipboardKind, wait: WaitConfig, + exclude_from_history: bool, ) -> Result<()> { - let mut data = vec![]; + let mut data = { + let cap = [true, alt.is_some(), exclude_from_history] + .map(|v| usize::from(v as u8)) + .iter() + .sum(); + Vec::with_capacity(cap) + }; + if let Some(alt_text) = alt { data.push(ClipboardData { bytes: alt_text.into_owned().into_bytes(), @@ -899,6 +993,9 @@ impl Clipboard { bytes: html.into_owned().into_bytes(), format: self.inner.atoms.HTML, }); + + self.add_clipboard_exclusions(exclude_from_history, &mut data); + self.inner.write(data, selection, wait) } @@ -926,9 +1023,15 @@ impl Clipboard { image: ImageData, selection: LinuxClipboardKind, wait: WaitConfig, + exclude_from_history: bool, ) -> Result<()> { let encoded = encode_as_png(&image)?; - let data = vec![ClipboardData { bytes: encoded, format: self.inner.atoms.PNG_MIME }]; + let mut data = Vec::with_capacity(if exclude_from_history { 2 } else { 1 }); + + data.push(ClipboardData { bytes: encoded, format: self.inner.atoms.PNG_MIME }); + + self.add_clipboard_exclusions(exclude_from_history, &mut data); + self.inner.write(data, selection, wait) } From 7ea1cf2caad715a7ebf054636f8510523f36506b Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Sun, 15 Jun 2025 12:30:45 -0500 Subject: [PATCH 56/82] Clarify ownership handling in selection data writing While there is no behavior difference, its more obviously correct if we only claim clipboard ownership _after_ we've written data and prepared to serve it. --- src/platform/linux/wayland.rs | 6 ++---- src/platform/linux/x11.rs | 14 +++++++------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/platform/linux/wayland.rs b/src/platform/linux/wayland.rs index 3dd4ba5..af49992 100644 --- a/src/platform/linux/wayland.rs +++ b/src/platform/linux/wayland.rs @@ -114,8 +114,7 @@ impl Clipboard { opts.copy_multi(sources).map_err(|e| match e { CopyError::PrimarySelectionUnsupported => Error::ClipboardNotSupported, other => into_unknown(other), - })?; - Ok(()) + }) } pub(crate) fn get_html(&mut self, selection: LinuxClipboardKind) -> Result { @@ -159,8 +158,7 @@ impl Clipboard { opts.copy_multi(sources).map_err(|e| match e { CopyError::PrimarySelectionUnsupported => Error::ClipboardNotSupported, other => into_unknown(other), - })?; - Ok(()) + }) } #[cfg(feature = "image-data")] diff --git a/src/platform/linux/x11.rs b/src/platform/linux/x11.rs index e28bf44..b31df48 100644 --- a/src/platform/linux/x11.rs +++ b/src/platform/linux/x11.rs @@ -221,7 +221,7 @@ impl Inner { fn write( &self, data: Vec, - selection: LinuxClipboardKind, + clipboard_selection: LinuxClipboardKind, wait: WaitConfig, ) -> Result<()> { if self.serve_stopped.load(Ordering::Relaxed) { @@ -230,20 +230,20 @@ impl Inner { let server_win = self.server.win_id; + // Just setting the data, and the `serve_requests` will take care of the rest. + let selection = self.selection_of(clipboard_selection); + let mut data_guard = selection.data.write(); + *data_guard = Some(data); + // ICCCM version 2, section 2.6.1.3 states that we should re-assert ownership whenever data // changes. self.server .conn - .set_selection_owner(server_win, self.atom_of(selection), Time::CURRENT_TIME) + .set_selection_owner(server_win, self.atom_of(clipboard_selection), Time::CURRENT_TIME) .map_err(|_| Error::ClipboardOccupied)?; self.server.conn.flush().map_err(into_unknown)?; - // Just setting the data, and the `serve_requests` will take care of the rest. - let selection = self.selection_of(selection); - let mut data_guard = selection.data.write(); - *data_guard = Some(data); - // Lock the mutex to both ensure that no wakers of `data_changed` can wake us between // dropping the `data_guard` and calling `wait[_for]` and that we don't we wake other // threads in that position. From 825026572ac943e5ea789b437e4c50c42508e9d6 Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Sun, 15 Jun 2025 17:17:54 -0500 Subject: [PATCH 57/82] Refactor Wayland error handling to better account for missing Primary clipboard --- src/platform/linux/wayland.rs | 44 ++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/src/platform/linux/wayland.rs b/src/platform/linux/wayland.rs index af49992..f78d404 100644 --- a/src/platform/linux/wayland.rs +++ b/src/platform/linux/wayland.rs @@ -54,14 +54,29 @@ fn add_clipboard_exclusions(exclude_from_history: bool, sources: &mut Vec Error { + match e { + CopyError::PrimarySelectionUnsupported => Error::ClipboardNotSupported, + other => into_unknown(other), + } +} + +fn handle_paste_error(e: paste::Error) -> Error { + match e { + PasteError::PrimarySelectionUnsupported => Error::ClipboardNotSupported, + other => into_unknown(other), + } +} + impl Clipboard { - #[allow(clippy::unnecessary_wraps)] pub(crate) fn new() -> Result { // Check if it's possible to communicate with the wayland compositor - if let Err(e) = is_primary_selection_supported() { - return Err(into_unknown(e)); + match is_primary_selection_supported() { + // We don't care if the primary clipboard is supported or not, `wl-clipboard-rs` will fail + // if not and we don't want to duplicate more of their logic. + Ok(_) => Ok(Self {}), + Err(e) => Err(into_unknown(e)), } - Ok(Self {}) } fn string_for_mime( @@ -76,14 +91,10 @@ impl Clipboard { pipe.read_to_end(&mut contents).map_err(into_unknown)?; String::from_utf8(contents).map_err(|_| Error::ConversionFailure) } - Err(PasteError::ClipboardEmpty) | Err(PasteError::NoMimeType) => { Err(Error::ContentNotAvailable) } - - Err(PasteError::PrimarySelectionUnsupported) => Err(Error::ClipboardNotSupported), - - Err(err) => Err(into_unknown(err)), + Err(err) => Err(handle_paste_error(err)), } } @@ -111,10 +122,7 @@ impl Clipboard { add_clipboard_exclusions(exclude_from_history, &mut sources); - opts.copy_multi(sources).map_err(|e| match e { - CopyError::PrimarySelectionUnsupported => Error::ClipboardNotSupported, - other => into_unknown(other), - }) + opts.copy_multi(sources).map_err(handle_copy_error) } pub(crate) fn get_html(&mut self, selection: LinuxClipboardKind) -> Result { @@ -155,10 +163,7 @@ impl Clipboard { add_clipboard_exclusions(exclude_from_history, &mut sources); - opts.copy_multi(sources).map_err(|e| match e { - CopyError::PrimarySelectionUnsupported => Error::ClipboardNotSupported, - other => into_unknown(other), - }) + opts.copy_multi(sources).map_err(handle_copy_error) } #[cfg(feature = "image-data")] @@ -171,6 +176,7 @@ impl Clipboard { let result = get_contents(selection.try_into()?, Seat::Unspecified, MimeType::Specific(MIME_PNG)); + match result { Ok((mut pipe, _mime_type)) => { let mut buffer = vec![]; @@ -193,7 +199,7 @@ impl Clipboard { Err(Error::ContentNotAvailable) } - Err(err) => Err(into_unknown(err)), + Err(err) => Err(handle_paste_error(err)), } } @@ -220,7 +226,7 @@ impl Clipboard { add_clipboard_exclusions(exclude_from_history, &mut sources); - opts.copy_multi(sources).map_err(into_unknown) + opts.copy_multi(sources).map_err(handle_copy_error) } pub(crate) fn get_file_list( From 6b0e47ac8aaab56fd8e621a4395b71360f0bf2b9 Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Sun, 15 Jun 2025 17:36:06 -0500 Subject: [PATCH 58/82] Reimplement Linux clipboard clearing with correct primitives --- src/platform/linux/mod.rs | 9 +++++---- src/platform/linux/wayland.rs | 5 +++++ src/platform/linux/x11.rs | 31 +++++++++++++++++++++++-------- 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/src/platform/linux/mod.rs b/src/platform/linux/mod.rs index a10fb42..2bef80a 100644 --- a/src/platform/linux/mod.rs +++ b/src/platform/linux/mod.rs @@ -347,10 +347,11 @@ impl<'clipboard> Clear<'clipboard> { } fn clear_inner(self, selection: LinuxClipboardKind) -> Result<(), Error> { - let mut set = Set::new(self.clipboard); - set.selection = selection; - - set.text(Cow::Borrowed("")) + match self.clipboard { + Clipboard::X11(clipboard) => clipboard.clear(selection), + #[cfg(feature = "wayland-data-control")] + Clipboard::WlDataControl(clipboard) => clipboard.clear(selection), + } } } diff --git a/src/platform/linux/wayland.rs b/src/platform/linux/wayland.rs index f78d404..70633bc 100644 --- a/src/platform/linux/wayland.rs +++ b/src/platform/linux/wayland.rs @@ -98,6 +98,11 @@ impl Clipboard { } } + pub(crate) fn clear(&mut self, selection: LinuxClipboardKind) -> Result<(), Error> { + let selection = selection.try_into()?; + copy::clear(selection, copy::Seat::All).map_err(handle_copy_error) + } + pub(crate) fn get_text(&mut self, selection: LinuxClipboardKind) -> Result { self.string_for_mime(selection, paste::MimeType::Text) } diff --git a/src/platform/linux/x11.rs b/src/platform/linux/x11.rs index b31df48..1e4c0ca 100644 --- a/src/platform/linux/x11.rs +++ b/src/platform/linux/x11.rs @@ -218,6 +218,24 @@ impl Inner { }) } + /// Performs a "clear" operation on the clipboard, which is implemented by + /// relinquishing the selection to revert its owner to `None`. This gracefully + /// and comformly informs the X server and any clipboard managers that the + /// data was no longer valid and won't be offered from our window anymore. + /// + /// See `ask_clipboard_manager_to_request_our_data` for more details on why + /// this is important and specification references. + fn clear(&self, selection: LinuxClipboardKind) -> Result<()> { + let selection = self.atom_of(selection); + + self.server + .conn + .set_selection_owner(NONE, selection, Time::CURRENT_TIME) + .map_err(into_unknown)?; + + self.server.conn.flush().map_err(into_unknown) + } + fn write( &self, data: Vec, @@ -724,18 +742,11 @@ impl Inner { // // By removing the owner, the manager doesn't think it needs to pick up our window's data serving once // its destroyed and cleanly lets the data disappear based off the previously advertised exclusion hint. - if let Err(e) = self.server.conn.set_selection_owner( - NONE, - self.atoms.CLIPBOARD, - Time::CURRENT_TIME, - ) { + if let Err(e) = self.clear(selection) { warn!("failed to release sensitive data's clipboard ownership: {e}; it may end up persisted!"); // This is still not an error because we werent going to handoff anything to the manager. - return Ok(()); } - // It doesn't matter if this fails, the clipboard window will be destroyed regardless. - let _ = self.server.conn.flush(); return Ok(()); } } @@ -924,6 +935,10 @@ impl Clipboard { } } + pub(crate) fn clear(&self, selection: LinuxClipboardKind) -> Result<()> { + self.inner.clear(selection) + } + pub(crate) fn get_text(&self, selection: LinuxClipboardKind) -> Result { let formats = [ self.inner.atoms.UTF8_STRING, From b1e6720c3e495c705a99937b57be5db058315b99 Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Sun, 15 Jun 2025 21:51:55 -0500 Subject: [PATCH 59/82] Fix getting text on Windows when locale identifiers differ --- src/platform/windows.rs | 44 +++++++++++++---------------------------- 1 file changed, 14 insertions(+), 30 deletions(-) diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 9e4563e..fea3403 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -573,36 +573,20 @@ impl<'clipboard> Get<'clipboard> { return Err(Error::ContentNotAvailable); } - let text_size = clipboard_win::raw::size(FORMAT) - .ok_or_else(|| Error::unknown("failed to read clipboard text size"))?; - - // Allocate the specific number of WTF-16 characters we need to receive. - // This division is always accurate because Windows uses 16-bit characters. - let mut out: Vec = vec![0u16; text_size.get() / 2]; - - let bytes_read = { - // SAFETY: The source slice has a greater alignment than the resulting one. - let out: &mut [u8] = - unsafe { std::slice::from_raw_parts_mut(out.as_mut_ptr().cast(), out.len() * 2) }; - - let mut bytes_read = clipboard_win::raw::get(FORMAT, out) - .map_err(|_| Error::unknown("failed to read clipboard string"))?; - - // Convert the number of bytes read to the number of `u16`s - bytes_read /= 2; - - // Remove the NUL terminator, if it existed. - if let Some(last) = out.last().copied() { - if last == 0 { - bytes_read -= 1; - } - } - - bytes_read - }; - - // Create a UTF-8 string from WTF-16 data, if it was valid. - String::from_utf16(&out[..bytes_read]).map_err(|_| Error::ConversionFailure) + // NB: Its important that whatever functionality decodes the text buffer from the clipboard + // uses `WideCharToMultiByte` with `CP_UTF8` (or an equivalent) in order to handle when both "text" + // and a locale identifier were placed on the clipboard. It is probable this occurs when an application + // is running with a codepage that isn't the current system's, such as under a locale emulator. + // + // In these cases, Windows decodes the text buffer with whatever codepage that identifier is for + // when creating the `CF_UNICODETEXT` buffer. Therefore, the buffer could then be in any format, + // not nessecarily wide UTF-16. We need to then undo that, taking the wide data and mapping it into + // the UTF-8 space as best as possible. + // + // (locale-specific text data, locale id) -> app -> system -> arboard (locale-specific text data) -> UTF-8 + let mut out = Vec::new(); + clipboard_win::raw::get_string(&mut out).map_err(|_| Error::ContentNotAvailable)?; + String::from_utf8(out).map_err(|_| Error::ConversionFailure) } pub(crate) fn html(self) -> Result { From 5f80bc1ddf8551fa4bf02531b9c0abbacf70749a Mon Sep 17 00:00:00 2001 From: crumblingstatue Date: Tue, 24 Jun 2025 21:11:45 +0200 Subject: [PATCH 60/82] linux/x11: Don't stop worker thread if handling selection request fails (#186) * linux/x11: Don't stop worker thread if handling selection request fails Just because one request fails, that doesn't mean we can't handle subsequent requests. Stopping the worker thread on failure means that every subsequent request will fail, which is usually undesirable. --- src/platform/linux/x11.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/platform/linux/x11.rs b/src/platform/linux/x11.rs index 1e4c0ca..b1a1e25 100644 --- a/src/platform/linux/x11.rs +++ b/src/platform/linux/x11.rs @@ -847,7 +847,10 @@ fn serve_requests(context: Arc) -> Result<(), Box> context.atom_name_dbg(event.target), ); // Someone is requesting the clipboard content from us. - context.handle_selection_request(event).map_err(into_unknown)?; + if let Err(e) = context.handle_selection_request(event) { + error!("Failed to handle selection request: {e}"); + continue; + } // if we are in the progress of saving to the clipboard manager // make sure we save that we have finished writing From 1040043ca426321e88d5277077c91147347e8316 Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Fri, 27 Jun 2025 16:43:19 -0500 Subject: [PATCH 61/82] Reword README sections and elaborate on Linux support --- README.md | 71 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 50 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index c4139bb..100f36d 100644 --- a/README.md +++ b/README.md @@ -10,22 +10,32 @@ This is a cross-platform library for interacting with the clipboard. It allows to copy and paste both text and image data in a platform independent way on Linux, Mac, and Windows. +Please note that this is not an official 1Password product. Feature requests will be considered like any other volunteer-based crate. + ## GNU/Linux -The GNU/Linux implementation uses the X protocol by default for managing the -clipboard but *fear not* because Wayland works with the X11 protocol just as -well. Furthermore this implementation uses the Clipboard selection (as opposed -to the primary selection) and it sends the data to the clipboard manager when -the application exits so that the data placed onto the clipboard with your -application remains to be available after exiting. +### Backend Support -There's also an optional wayland data control backend through the -`wl-clipboard-rs` crate. This can be enabled using the `wayland-data-control` -feature. When enabled this will be prioritized over the X11 backend, but if the -initialization fails, the implementation falls back to using the X11 protocol -automatically. Note that in my tests the wayland backend did not keep the -clipboard contents after the process exited. (Although neither did the X11 -backend on my Wayland setup). +By default, `arboard`'s backend on Linux supports X11 (or XWayland implementations) and uses +that for managing the various Linux clipboard variants. This supports the majority of desktop +environments that exist in the wild today. `arboard` will use the `Clipboard` selection by default, +but the [LinuxClipboardKind](https://docs.rs/arboard/latest/arboard/enum.LinuxClipboardKind.html) +selector lets you operate on the `Primary` or `Secondary` clipboard selections (if supported). + +However, Wayland is becoming the majority default as of 2025 (with some distributions) +even considering the removal of X by default. To support Wayland correctly, `arboard` users +should enable the `wayland-data-control` feature. If enabled, it will be prioritized over the X clipboard. + +Wayland support is not enabled by default because it may be counterintuitive +to some users: it relies on the data-control extension protocols, +which _are not_ support all Wayland compositors. You can check compositor support on `wayland.app`: +- [ext-data-control-v1](https://wayland.app/protocols/ext-data-control-v1) +- [wlr-data-control-unstable-v1](https://wayland.app/protocols/wlr-data-control-unstable-v1) + +If you or a user's desktop doesn't support these protocols, `arboard` won't function in a pure +Wayland environment. It is recommended to enable `XWayland` for these cases. If your app runs inside +an isolated sandbox, such as Flatpak or Snap, you'll need to expose the X11 socket to the application +_in addition_ to the Wayland communication interface. ## Example @@ -42,12 +52,31 @@ fn main() { } ``` -## Yet another clipboard crate +## Credits -This is a fork of `rust-clipboard`. The reason for forking instead of making a -PR is that `rust-clipboard` is not being maintained any more. Furthermore note -that the API of this crate is considerably different from that of -`rust-clipboard`. There are already a ton of clipboard crates out there which -is a bit unfortunate; I don't know why this is happening but while it is, we -might as well just start naming the clipboard crates after ourselves. This one -is arboard which stands for Artur's clipboard. +This crate is a combined effort by 1Password staff and `@ArturKovacs`, the crate's past +maintainer. + +#### License + + +Licensed under either of Apache License, Version +2.0 or MIT license at your option. + + +
+ + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in this crate by you, as defined in the Apache-2.0 license, shall +be dual licensed as above, without any additional terms or conditions. + + +#### History: Yet another clipboard crate + +This crate started out as a fork of `rust-clipboard`. The reason for forking is due to the former +crate not being maintained any longer. At this point, `arboard`'s backends and public APIs have diverged +a lot. + +`arboard`'s original maintainer noted that "I don't know why this is happening but while it is, we might +as well just start naming the clipboard crates after ourselves. This one is arboard which stands for Artur's clipboard.". From b704da3ceaea391659d292bcf1275f34c594a4e7 Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Fri, 27 Jun 2025 16:56:50 -0500 Subject: [PATCH 62/82] Add debug helper for too-early Linux clipboard dropping --- src/platform/linux/x11.rs | 55 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/src/platform/linux/x11.rs b/src/platform/linux/x11.rs index b1a1e25..8b8883b 100644 --- a/src/platform/linux/x11.rs +++ b/src/platform/linux/x11.rs @@ -178,8 +178,9 @@ impl XContext { #[derive(Default)] struct Selection { data: RwLock>>, - /// Mutex around nothing to use with the below condvar. - mutex: Mutex<()>, + /// Mutex around when this selection was last changed by us + /// for both use with the below condvar and logging. + mutex: Mutex>, /// A condvar that is notified when the contents of this clipboard are changed. /// /// This is associated with `Self::mutex`. @@ -266,6 +267,8 @@ impl Inner { // dropping the `data_guard` and calling `wait[_for]` and that we don't we wake other // threads in that position. let mut guard = selection.mutex.lock(); + // Record the time we modify the selection. + *guard = Some(Instant::now()); // Notify any existing waiting threads that we have changed the data in the selection. // It is important that the mutex is locked to prevent this notification getting lost. @@ -1089,7 +1092,10 @@ impl Drop for Clipboard { return; } if let Some(global_cb) = global_cb { - if let Err(e) = global_cb.server_handle.join() { + let GlobalClipboard { inner, server_handle } = global_cb; + drop(inner); + + if let Err(e) = server_handle.join() { // Let's try extracting the error message let message; if let Some(msg) = e.downcast_ref::<&'static str>() { @@ -1108,6 +1114,49 @@ impl Drop for Clipboard { error!("The clipboard server thread panicked."); } } + + // By this point we've dropped the Global's reference to `Inner` and the background + // thread has exited which means it also dropped its reference. Therefore `self.inner` should + // be the last strong count. + // + // Note: The following is all best effort and is only for logging. Nothing is guaranteed to execute + // or log. + #[cfg(debug_assertions)] + if let Some(inner) = Arc::get_mut(&mut self.inner) { + use std::io::IsTerminal; + + let mut change_timestamps = Vec::with_capacity(2); + let mut collect_changed = |sel: &mut Mutex>| { + if let Some(changed) = sel.get_mut() { + change_timestamps.push(*changed); + } + }; + + collect_changed(&mut inner.clipboard.mutex); + collect_changed(&mut inner.primary.mutex); + collect_changed(&mut inner.secondary.mutex); + + change_timestamps.sort(); + if let Some(last) = change_timestamps.last() { + let elapsed = last.elapsed().as_millis(); + // This number has no meaning, its just a guess for how long + // might be reasonable to give a clipboard manager a chance to + // save contents based ~roughly on the handoff timeout. + if elapsed > 100 { + return; + } + + // If the app isn't running in a terminal don't print, use log instead. + // Printing has a higher chance of being seen though, so its our default. + // Its also close enough to a `debug_assert!` that it shouldn't come across strange. + let msg = format!("Clipboard was dropped very quickly after writing ({elapsed}ms); clipboard managers may not have seen the contents\nConsider keeping `Clipboard` in more persistent state somewhere or keeping the contents alive longer using `SetLinuxExt` and/or threads."); + if std::io::stderr().is_terminal() { + eprintln!("{msg}"); + } else { + log::warn!("{msg}"); + } + } + } } } } From 8f6bab7d487fb22a3532a72d4b9ce1a88b953650 Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Fri, 27 Jun 2025 17:17:48 -0500 Subject: [PATCH 63/82] Add README section about Linux clipboard ownership --- README.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/README.md b/README.md index 100f36d..4a9c4dc 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,41 @@ Wayland environment. It is recommended to enable `XWayland` for these cases. If an isolated sandbox, such as Flatpak or Snap, you'll need to expose the X11 socket to the application _in addition_ to the Wayland communication interface. +### Clipboard Ownership + +Some apps and users may notice that sometimes values copied to a Linux clipboard with this crate vanish +before anyone gets the chance to paste, or just aren't available when you expect them to be. The root behind +these problems is _selection ownership_. + +X11 and Wayland put the responsibility for answering paste requests and serving data on the application +which originally copied it onto the clipboard. This usually means the app using `arboard`. Nothing you copy +to the clipboard is sent anywhere to start. It stays inside `arboard` until something else on the system requests it, +which is very different to how the clipboard works on other platforms like macOS or Windows. + +Note that `arboard` may attempt to warn you about these conditions when compiled in debug mode, to improve the debugging +experience. Even if you don't see these warnings, you should double check the lifetime of the `Clipboard` in your code. + +In some cases, an environment may have a clipboard manager installed. These services monitor the clipboard contents and +do their best to retain a copy when needed to smooth over clipboard ownership changes. A clipboar manager can make contents +available even after a process previously owning it exits. + +In order to keep the contents around longer, make sure that you don't `Drop` your `Clipboard` object right away or +terminate the copying process too fast. This is why, at times, adding a call to `sleep()` near the set operation +makes it behave more reliabily: the background thread `arboard` uses for serving clipboard contents has more time to run +and let other apps (including clipboard managers) make requests for the contents. However `sleep` isn't the recommended approach. + +If your application is exiting, you must make sure there is a clipboard manager running on the system. If nothing is listening for +the clipboard ownership transfer, or made a copy previously, the data will be lost. Note that this isn't a complete +guarantee as races are possible if your program's main thread is exiting. If you would like to fully synchronize the clipboard "paste" +before exiting, you can use the [wait](https://docs.rs/arboard/latest/arboard/trait.SetExtLinux.html#tymethod.wait) method when setting +contents on the clipboard. This will block the calling thread until another app has requested, and then received, the data. + +If your application is longer-running (ie a GUI, TUI, etc), it is highly recommended that you either store the `Clipboard` object in some +long-lived data structure (like app context, etc) or utilize `wait` method mentioned above, and/or threading to make sure another +app can request the clipboard data later. + +We welcome suggestions to improve on the above issues in ways that don't degrade other use cases. + ## Example ```rust From 68ea2074ac33e3c8262b0df7438c2575eb304199 Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Fri, 27 Jun 2025 17:41:50 -0500 Subject: [PATCH 64/82] Resolve new Clippy lints --- src/platform/windows.rs | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/platform/windows.rs b/src/platform/windows.rs index fea3403..c8058d9 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -37,7 +37,7 @@ mod image_data { fn last_error(message: &str) -> Error { let os_error = io::Error::last_os_error(); - Error::unknown(format!("{}: {}", message, os_error)) + Error::unknown(format!("{message}: {os_error}")) } unsafe fn global_unlock_checked(hdata: HGLOBAL) { @@ -823,18 +823,6 @@ fn wrap_html(ctn: &str) -> String { let n_end_frag = n_start_frag + ctn.len(); let n_end_html = n_end_frag + c_end_frag.len(); format!( - "{}{}{:010}{}{:010}{}{:010}{}{:010}{}{}{}", - h_version, - h_start_html, - n_start_html, - h_end_html, - n_end_html, - h_start_frag, - n_start_frag, - h_end_frag, - n_end_frag, - c_start_frag, - ctn, - c_end_frag, + "{h_version}{h_start_html}{n_start_html:010}{h_end_html}{n_end_html:010}{h_start_frag}{n_start_frag:010}{h_end_frag}{n_end_frag:010}{c_start_frag}{ctn}{c_end_frag}" ) } From 380d2a691b2c2bc11b508c7d73660333e1e1a19c Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Fri, 27 Jun 2025 18:35:59 -0500 Subject: [PATCH 65/82] Remove deprecated authors Cargo manifest field --- Cargo.toml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c598799..96fbbc3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,6 @@ [package] name = "arboard" version = "3.5.0" -authors = [ - "Artur Kovacs ", - "Avi Weinstock ", - "Arboard contributors", -] description = "Image and text handling for the OS clipboard." repository = "https://github.com/1Password/arboard" license = "MIT OR Apache-2.0" From 4f9bff86dc184dd635344a1ab9a62782631857f8 Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Fri, 27 Jun 2025 18:36:21 -0500 Subject: [PATCH 66/82] Release 3.6.0 --- CHANGELOG.md | 15 +++++++++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a68696..3b1fc35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## 3.6.0 on 2025-06-27 + +### Added +- Add support for excluding data from clipboard history on Linux. +- `arboard`, in debug builds, now attempts to call out clipboard lifetime mishandling. + - This is a debugging feature, and as such has no absolute or promised behavior. + +### Changed +- The background thread in the X11 backend no longer exits on every selection request failure. + +### Fixed +- Handled cases where using an unsupported Primary clipboard on Wayland would return the wrong error. +- Clearing the clipboard on Linux now behaves correctly when interacting with other apps on the system. +- Pasting text with an explicit locale ID on Windows now works as intended. + ## 3.5.0 on 2025-04-01 ### Added diff --git a/Cargo.lock b/Cargo.lock index 472a00d..df18e6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,7 +19,7 @@ dependencies = [ [[package]] name = "arboard" -version = "3.5.0" +version = "3.6.0" dependencies = [ "clipboard-win", "env_logger", diff --git a/Cargo.toml b/Cargo.toml index 96fbbc3..c01ebb6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "arboard" -version = "3.5.0" +version = "3.6.0" description = "Image and text handling for the OS clipboard." repository = "https://github.com/1Password/arboard" license = "MIT OR Apache-2.0" From 90f8f526f4d3dd113d69771d468ddea3026ca813 Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Sat, 28 Jun 2025 10:47:52 -0500 Subject: [PATCH 67/82] Fix grammar and typos in README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4a9c4dc..5c5111c 100644 --- a/README.md +++ b/README.md @@ -22,13 +22,13 @@ environments that exist in the wild today. `arboard` will use the `Clipboard` se but the [LinuxClipboardKind](https://docs.rs/arboard/latest/arboard/enum.LinuxClipboardKind.html) selector lets you operate on the `Primary` or `Secondary` clipboard selections (if supported). -However, Wayland is becoming the majority default as of 2025 (with some distributions) +However, Wayland is becoming the majority default as of 2025. Some distributions are even considering the removal of X by default. To support Wayland correctly, `arboard` users should enable the `wayland-data-control` feature. If enabled, it will be prioritized over the X clipboard. Wayland support is not enabled by default because it may be counterintuitive -to some users: it relies on the data-control extension protocols, -which _are not_ support all Wayland compositors. You can check compositor support on `wayland.app`: +to some users: it relies on the data-control protocol extension(s), which _are not_ +supported by all Wayland compositors. You can check compositor support on `wayland.app`: - [ext-data-control-v1](https://wayland.app/protocols/ext-data-control-v1) - [wlr-data-control-unstable-v1](https://wayland.app/protocols/wlr-data-control-unstable-v1) From d48f5ff0c7b08bf8edd05a972361cedb83b2a4cb Mon Sep 17 00:00:00 2001 From: Agathe Date: Fri, 11 Jul 2025 17:37:48 +0200 Subject: [PATCH 68/82] src/common.rs: fix typo "an other" -> "another" (#196) Found by lintian (spelling-error-in-binary). --- src/common.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common.rs b/src/common.rs index 8815016..b5e877b 100644 --- a/src/common.rs +++ b/src/common.rs @@ -30,7 +30,7 @@ pub enum Error { /// - Using the Secondary clipboard on Wayland ClipboardNotSupported, - /// The native clipboard is not accessible due to being held by an other party. + /// The native clipboard is not accessible due to being held by another party. /// /// This "other party" could be a different process or it could be within /// the same program. So for example you may get this error when trying @@ -57,7 +57,7 @@ impl std::fmt::Display for Error { match self { Error::ContentNotAvailable => f.write_str("The clipboard contents were not available in the requested format or the clipboard is empty."), Error::ClipboardNotSupported => f.write_str("The selected clipboard is not supported with the current system configuration."), - Error::ClipboardOccupied => f.write_str("The native clipboard is not accessible due to being held by an other party."), + Error::ClipboardOccupied => f.write_str("The native clipboard is not accessible due to being held by another party."), Error::ConversionFailure => f.write_str("The image or the text that was about the be transferred to/from the clipboard could not be converted to the appropriate format."), Error::Unknown { description } => f.write_fmt(format_args!("Unknown error while interacting with the clipboard: {description}")), } From a31adf444ddad6acb7ee0fd251a76c5829e5235c Mon Sep 17 00:00:00 2001 From: Gae24 <96017547+Gae24@users.noreply.github.com> Date: Fri, 1 Aug 2025 16:11:42 +0200 Subject: [PATCH 69/82] fix: avoid checking if data is utf8 compliant inside file list getter --- src/platform/linux/mod.rs | 12 ++++++------ src/platform/linux/wayland.rs | 18 ++++++++++++++++-- src/platform/linux/x11.rs | 4 +--- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/platform/linux/mod.rs b/src/platform/linux/mod.rs index 2bef80a..cf47099 100644 --- a/src/platform/linux/mod.rs +++ b/src/platform/linux/mod.rs @@ -2,7 +2,7 @@ use std::{borrow::Cow, path::PathBuf, time::Instant}; #[cfg(feature = "wayland-data-control")] use log::{trace, warn}; -use percent_encoding::percent_decode_str; +use percent_encoding::percent_decode; #[cfg(feature = "image-data")] use crate::ImageData; @@ -43,11 +43,11 @@ fn encode_as_png(image: &ImageData) -> Result, Error> { Ok(png_bytes) } -fn paths_from_uri_list(uri_list: String) -> Vec { +fn paths_from_uri_list(uri_list: Vec) -> Vec { uri_list - .lines() - .filter_map(|s| s.strip_prefix("file://")) - .filter_map(|s| percent_decode_str(s).decode_utf8().ok()) + .split(|char| *char == b'\n') + .filter_map(|line| line.strip_prefix(b"file://")) + .filter_map(|s| percent_decode(s).decode_utf8().ok()) .map(|decoded| PathBuf::from(decoded.as_ref())) .collect() } @@ -405,6 +405,6 @@ mod tests { PathBuf::from("/tmp/foo?.png"), PathBuf::from("/tmp/white space.txt"), ]; - assert_eq!(paths_from_uri_list(file_list.join("\n")), paths); + assert_eq!(paths_from_uri_list(file_list.join("\n").into()), paths); } } diff --git a/src/platform/linux/wayland.rs b/src/platform/linux/wayland.rs index 70633bc..2cbd4ca 100644 --- a/src/platform/linux/wayland.rs +++ b/src/platform/linux/wayland.rs @@ -238,7 +238,21 @@ impl Clipboard { &mut self, selection: LinuxClipboardKind, ) -> Result, Error> { - self.string_for_mime(selection, paste::MimeType::Specific("text/uri-list")) - .map(paths_from_uri_list) + let result = get_contents( + selection.try_into()?, + Seat::Unspecified, + paste::MimeType::Specific("text/uri-list"), + ); + match result { + Ok((mut pipe, _)) => { + let mut contents = vec![]; + pipe.read_to_end(&mut contents).map_err(into_unknown)?; + Ok(paths_from_uri_list(contents)) + } + Err(PasteError::ClipboardEmpty) | Err(PasteError::NoMimeType) => { + Err(Error::ContentNotAvailable) + } + Err(err) => Err(handle_paste_error(err)), + } } } diff --git a/src/platform/linux/x11.rs b/src/platform/linux/x11.rs index 8b8883b..c920fb9 100644 --- a/src/platform/linux/x11.rs +++ b/src/platform/linux/x11.rs @@ -1059,9 +1059,7 @@ impl Clipboard { pub(crate) fn get_file_list(&self, selection: LinuxClipboardKind) -> Result> { let result = self.inner.read(&[self.inner.atoms.URI_LIST], selection)?; - String::from_utf8(result.bytes) - .map_err(|_| Error::ConversionFailure) - .map(paths_from_uri_list) + Ok(paths_from_uri_list(result.bytes)) } } From 6eed118532cadb72f6e240227171f759efafa3d9 Mon Sep 17 00:00:00 2001 From: Gae24 <96017547+Gae24@users.noreply.github.com> Date: Fri, 1 Aug 2025 17:47:08 +0200 Subject: [PATCH 70/82] wayland: extract common code in helper function --- src/platform/linux/wayland.rs | 105 +++++++++++++--------------------- 1 file changed, 41 insertions(+), 64 deletions(-) diff --git a/src/platform/linux/wayland.rs b/src/platform/linux/wayland.rs index 2cbd4ca..860eaee 100644 --- a/src/platform/linux/wayland.rs +++ b/src/platform/linux/wayland.rs @@ -68,6 +68,25 @@ fn handle_paste_error(e: paste::Error) -> Error { } } +fn handle_clipboard_read) -> Result>( + selection: LinuxClipboardKind, + mime: paste::MimeType, + into_requested_data: F, +) -> Result { + let result = get_contents(selection.try_into()?, Seat::Unspecified, mime); + match result { + Ok((mut pipe, _)) => { + let mut buffer = vec![]; + pipe.read_to_end(&mut buffer).map_err(into_unknown)?; + into_requested_data(buffer) + } + Err(PasteError::ClipboardEmpty) | Err(PasteError::NoMimeType) => { + Err(Error::ContentNotAvailable) + } + Err(err) => Err(handle_paste_error(err)), + } +} + impl Clipboard { pub(crate) fn new() -> Result { // Check if it's possible to communicate with the wayland compositor @@ -79,32 +98,15 @@ impl Clipboard { } } - fn string_for_mime( - &mut self, - selection: LinuxClipboardKind, - mime: paste::MimeType, - ) -> Result { - let result = get_contents(selection.try_into()?, Seat::Unspecified, mime); - match result { - Ok((mut pipe, _)) => { - let mut contents = vec![]; - pipe.read_to_end(&mut contents).map_err(into_unknown)?; - String::from_utf8(contents).map_err(|_| Error::ConversionFailure) - } - Err(PasteError::ClipboardEmpty) | Err(PasteError::NoMimeType) => { - Err(Error::ContentNotAvailable) - } - Err(err) => Err(handle_paste_error(err)), - } - } - pub(crate) fn clear(&mut self, selection: LinuxClipboardKind) -> Result<(), Error> { let selection = selection.try_into()?; copy::clear(selection, copy::Seat::All).map_err(handle_copy_error) } pub(crate) fn get_text(&mut self, selection: LinuxClipboardKind) -> Result { - self.string_for_mime(selection, paste::MimeType::Text) + handle_clipboard_read(selection, paste::MimeType::Text, |contents| { + String::from_utf8(contents).map_err(|_| Error::ConversionFailure) + }) } pub(crate) fn set_text( @@ -131,7 +133,9 @@ impl Clipboard { } pub(crate) fn get_html(&mut self, selection: LinuxClipboardKind) -> Result { - self.string_for_mime(selection, paste::MimeType::Specific("text/html")) + handle_clipboard_read(selection, paste::MimeType::Specific("text/html"), |contents| { + String::from_utf8(contents).map_err(|_| Error::ConversionFailure) + }) } pub(crate) fn set_html( @@ -177,35 +181,21 @@ impl Clipboard { selection: LinuxClipboardKind, ) -> Result, Error> { use std::io::Cursor; - use wl_clipboard_rs::paste::MimeType; - let result = - get_contents(selection.try_into()?, Seat::Unspecified, MimeType::Specific(MIME_PNG)); + handle_clipboard_read(selection, paste::MimeType::Specific(MIME_PNG), |buffer| { + let image = image::io::Reader::new(Cursor::new(buffer)) + .with_guessed_format() + .map_err(|_| Error::ConversionFailure)? + .decode() + .map_err(|_| Error::ConversionFailure)?; + let image = image.into_rgba8(); - match result { - Ok((mut pipe, _mime_type)) => { - let mut buffer = vec![]; - pipe.read_to_end(&mut buffer).map_err(into_unknown)?; - let image = image::io::Reader::new(Cursor::new(buffer)) - .with_guessed_format() - .map_err(|_| Error::ConversionFailure)? - .decode() - .map_err(|_| Error::ConversionFailure)?; - let image = image.into_rgba8(); - - Ok(ImageData { - width: image.width() as usize, - height: image.height() as usize, - bytes: image.into_raw().into(), - }) - } - - Err(PasteError::ClipboardEmpty) | Err(PasteError::NoMimeType) => { - Err(Error::ContentNotAvailable) - } - - Err(err) => Err(handle_paste_error(err)), - } + Ok(ImageData { + width: image.width() as usize, + height: image.height() as usize, + bytes: image.into_raw().into(), + }) + }) } #[cfg(feature = "image-data")] @@ -238,21 +228,8 @@ impl Clipboard { &mut self, selection: LinuxClipboardKind, ) -> Result, Error> { - let result = get_contents( - selection.try_into()?, - Seat::Unspecified, - paste::MimeType::Specific("text/uri-list"), - ); - match result { - Ok((mut pipe, _)) => { - let mut contents = vec![]; - pipe.read_to_end(&mut contents).map_err(into_unknown)?; - Ok(paths_from_uri_list(contents)) - } - Err(PasteError::ClipboardEmpty) | Err(PasteError::NoMimeType) => { - Err(Error::ContentNotAvailable) - } - Err(err) => Err(handle_paste_error(err)), - } + handle_clipboard_read(selection, paste::MimeType::Specific("text/uri-list"), |contents| { + Ok(paths_from_uri_list(contents)) + }) } } From ca2e80c409131f36143fa2ebbfa9cea6f4035394 Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Wed, 13 Aug 2025 09:20:51 -0500 Subject: [PATCH 71/82] Update Clippy lints for Rust 1.89 --- src/platform/windows.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/windows.rs b/src/platform/windows.rs index c8058d9..29ab850 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -517,7 +517,7 @@ impl Clipboard { Ok(Self(())) } - fn open(&mut self) -> Result { + fn open(&mut self) -> Result, Error> { // Attempt to open the clipboard multiple times. On Windows, its common for something else to temporarily // be using it during attempts. // From 17ef05ce130557823750c26e482b25b2cee1e115 Mon Sep 17 00:00:00 2001 From: Gae24 <96017547+Gae24@users.noreply.github.com> Date: Wed, 13 Aug 2025 16:30:34 +0200 Subject: [PATCH 72/82] add `file_list` to `Set` interface (#181) --- Cargo.toml | 7 +- src/lib.rs | 23 ++- src/platform/linux/mod.rs | 59 ++++++- src/platform/linux/wayland.rs | 38 +++- src/platform/linux/x11.rs | 22 ++- src/platform/osx.rs | 33 +++- src/platform/windows.rs | 318 ++++++++++++++++++++++++++-------- 7 files changed, 417 insertions(+), 83 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c01ebb6..5bc7c67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ wayland-data-control = ["wl-clipboard-rs"] # For backwards compat core-graphics = ["dep:objc2-core-graphics"] -windows-sys = ["dep:windows-sys"] +windows-sys = ["windows-sys/Win32_Graphics_Gdi"] image = ["dep:image"] wl-clipboard-rs = ["dep:wl-clipboard-rs"] @@ -32,12 +32,13 @@ wl-clipboard-rs = ["dep:wl-clipboard-rs"] env_logger = "0.10.2" [target.'cfg(windows)'.dependencies] -windows-sys = { version = ">=0.52.0, <0.60.0", optional = true, features = [ +windows-sys = { version = ">=0.52.0, <0.60.0", features = [ "Win32_Foundation", - "Win32_Graphics_Gdi", + "Win32_Storage_FileSystem", "Win32_System_DataExchange", "Win32_System_Memory", "Win32_System_Ole", + "Win32_UI_Shell", ] } clipboard-win = { version = "5.3.1", features = ["std"] } log = "0.4" diff --git a/src/lib.rs b/src/lib.rs index 4702827..13eb5ff 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,7 +10,10 @@ and conditions of the chosen license apply to this file. #![warn(unreachable_pub)] mod common; -use std::{borrow::Cow, path::PathBuf}; +use std::{ + borrow::Cow, + path::{Path, PathBuf}, +}; pub use common::Error; #[cfg(feature = "image-data")] @@ -243,6 +246,11 @@ impl Set<'_> { pub fn image(self, image: ImageData) -> Result<(), Error> { self.platform.image(image) } + + /// Completes the "set" operation by placing a list of file paths onto the clipboard. + pub fn file_list(self, file_list: &[impl AsRef]) -> Result<(), Error> { + self.platform.file_list(file_list) + } } /// A builder for an operation that clears the data from the clipboard. @@ -350,6 +358,19 @@ mod tests { assert_eq!(ctx.get().html().unwrap(), html); } } + { + let mut ctx = Clipboard::new().unwrap(); + + let this_dir = env!("CARGO_MANIFEST_DIR"); + + let paths = &[ + PathBuf::from(this_dir).join("README.md"), + PathBuf::from(this_dir).join("Cargo.toml"), + ]; + + ctx.set().file_list(paths).unwrap(); + assert_eq!(ctx.get().file_list().unwrap().as_slice(), paths); + } #[cfg(feature = "image-data")] { let mut ctx = Clipboard::new().unwrap(); diff --git a/src/platform/linux/mod.rs b/src/platform/linux/mod.rs index cf47099..b9e4095 100644 --- a/src/platform/linux/mod.rs +++ b/src/platform/linux/mod.rs @@ -1,8 +1,13 @@ -use std::{borrow::Cow, path::PathBuf, time::Instant}; +use std::{ + borrow::Cow, + os::unix::ffi::OsStrExt, + path::{Path, PathBuf}, + time::Instant, +}; #[cfg(feature = "wayland-data-control")] use log::{trace, warn}; -use percent_encoding::percent_decode; +use percent_encoding::{percent_decode, percent_encode, AsciiSet, CONTROLS}; #[cfg(feature = "image-data")] use crate::ImageData; @@ -52,6 +57,37 @@ fn paths_from_uri_list(uri_list: Vec) -> Vec { .collect() } +fn paths_to_uri_list(file_list: &[impl AsRef]) -> Result { + // The characters that require encoding, which includes £ and € but they can't be added to the set. + const ASCII_SET: &AsciiSet = &CONTROLS + .add(b'#') + .add(b';') + .add(b'?') + .add(b'[') + .add(b']') + .add(b' ') + .add(b'\"') + .add(b'%') + .add(b'<') + .add(b'>') + .add(b'\\') + .add(b'^') + .add(b'`') + .add(b'{') + .add(b'|') + .add(b'}'); + + file_list + .iter() + .filter_map(|path| { + path.as_ref().canonicalize().ok().map(|path| { + format!("file://{}", percent_encode(path.as_os_str().as_bytes(), ASCII_SET)) + }) + }) + .reduce(|uri_list, uri| uri_list + "\n" + &uri) + .ok_or(Error::ConversionFailure) +} + /// Clipboard selection /// /// Linux has a concept of clipboard "selections" which tend to be used in different contexts. This @@ -240,6 +276,25 @@ impl<'clipboard> Set<'clipboard> { } } } + + pub(crate) fn file_list(self, file_list: &[impl AsRef]) -> Result<(), Error> { + match self.clipboard { + Clipboard::X11(clipboard) => clipboard.set_file_list( + file_list, + self.selection, + self.wait, + self.exclude_from_history, + ), + + #[cfg(feature = "wayland-data-control")] + Clipboard::WlDataControl(clipboard) => clipboard.set_file_list( + file_list, + self.selection, + self.wait, + self.exclude_from_history, + ), + } + } } /// Linux specific extensions to the [`Set`](super::Set) builder. diff --git a/src/platform/linux/wayland.rs b/src/platform/linux/wayland.rs index 860eaee..eb4dfe5 100644 --- a/src/platform/linux/wayland.rs +++ b/src/platform/linux/wayland.rs @@ -1,4 +1,8 @@ -use std::{borrow::Cow, io::Read, path::PathBuf}; +use std::{ + borrow::Cow, + io::Read, + path::{Path, PathBuf}, +}; use wl_clipboard_rs::{ copy::{self, Error as CopyError, MimeSource, MimeType, Options, Source}, @@ -9,8 +13,8 @@ use wl_clipboard_rs::{ #[cfg(feature = "image-data")] use super::encode_as_png; use super::{ - into_unknown, paths_from_uri_list, LinuxClipboardKind, WaitConfig, KDE_EXCLUSION_HINT, - KDE_EXCLUSION_MIME, + into_unknown, paths_from_uri_list, paths_to_uri_list, LinuxClipboardKind, WaitConfig, + KDE_EXCLUSION_HINT, KDE_EXCLUSION_MIME, }; use crate::common::Error; #[cfg(feature = "image-data")] @@ -19,6 +23,8 @@ use crate::common::ImageData; #[cfg(feature = "image-data")] const MIME_PNG: &str = "image/png"; +const MIME_URI: &str = "text/uri-list"; + pub(crate) struct Clipboard {} impl TryInto for LinuxClipboardKind { @@ -228,8 +234,32 @@ impl Clipboard { &mut self, selection: LinuxClipboardKind, ) -> Result, Error> { - handle_clipboard_read(selection, paste::MimeType::Specific("text/uri-list"), |contents| { + handle_clipboard_read(selection, paste::MimeType::Specific(MIME_URI), |contents| { Ok(paths_from_uri_list(contents)) }) } + + pub(crate) fn set_file_list( + &self, + file_list: &[impl AsRef], + selection: LinuxClipboardKind, + wait: WaitConfig, + exclude_from_history: bool, + ) -> Result<(), Error> { + let files = paths_to_uri_list(file_list)?; + + let mut opts = Options::new(); + opts.foreground(matches!(wait, WaitConfig::Forever)); + opts.clipboard(selection.try_into()?); + + let mut sources = Vec::with_capacity(if exclude_from_history { 2 } else { 1 }); + sources.push(MimeSource { + source: Source::Bytes(files.into_bytes().into_boxed_slice()), + mime_type: MimeType::Specific(String::from(MIME_URI)), + }); + + add_clipboard_exclusions(exclude_from_history, &mut sources); + + opts.copy_multi(sources).map_err(handle_copy_error) + } } diff --git a/src/platform/linux/x11.rs b/src/platform/linux/x11.rs index c920fb9..17f107b 100644 --- a/src/platform/linux/x11.rs +++ b/src/platform/linux/x11.rs @@ -16,7 +16,7 @@ use std::{ borrow::Cow, cell::RefCell, collections::{hash_map::Entry, HashMap}, - path::PathBuf, + path::{Path, PathBuf}, sync::{ atomic::{AtomicBool, Ordering}, Arc, @@ -46,8 +46,8 @@ use x11rb::{ #[cfg(feature = "image-data")] use super::encode_as_png; use super::{ - into_unknown, paths_from_uri_list, LinuxClipboardKind, WaitConfig, KDE_EXCLUSION_HINT, - KDE_EXCLUSION_MIME, + into_unknown, paths_from_uri_list, paths_to_uri_list, LinuxClipboardKind, WaitConfig, + KDE_EXCLUSION_HINT, KDE_EXCLUSION_MIME, }; #[cfg(feature = "image-data")] use crate::ImageData; @@ -1061,6 +1061,22 @@ impl Clipboard { Ok(paths_from_uri_list(result.bytes)) } + + pub(crate) fn set_file_list( + &self, + file_list: &[impl AsRef], + selection: LinuxClipboardKind, + wait: WaitConfig, + exclude_from_history: bool, + ) -> Result<()> { + let files = paths_to_uri_list(file_list)?; + let mut data = Vec::with_capacity(if exclude_from_history { 2 } else { 1 }); + + data.push(ClipboardData { bytes: files.into_bytes(), format: self.inner.atoms.URI_LIST }); + self.add_clipboard_exclusions(exclude_from_history, &mut data); + + self.inner.write(data, selection, wait) + } } impl Drop for Clipboard { diff --git a/src/platform/osx.rs b/src/platform/osx.rs index 8637e50..a2591eb 100644 --- a/src/platform/osx.rs +++ b/src/platform/osx.rs @@ -25,7 +25,7 @@ use objc2_foundation::{ns_string, NSArray, NSDictionary, NSNumber, NSString, NSU use std::{ borrow::Cow, panic::{RefUnwindSafe, UnwindSafe}, - path::PathBuf, + path::{Path, PathBuf}, }; /// Returns an NSImage object on success. @@ -352,6 +352,37 @@ impl<'clipboard> Set<'clipboard> { )) } } + + pub(crate) fn file_list(self, file_list: &[impl AsRef]) -> Result<(), Error> { + self.clipboard.clear(); + + let uri_list = file_list + .iter() + .filter_map(|path| { + path.as_ref().canonicalize().ok().and_then(|abs_path| { + abs_path.to_str().map(|str| { + let url = unsafe { NSURL::fileURLWithPath(&NSString::from_str(str)) }; + ProtocolObject::from_retained(url) + }) + }) + }) + .collect::>(); + + if uri_list.is_empty() { + return Err(Error::ConversionFailure); + } + + let objects = NSArray::from_retained_slice(&uri_list); + let success = unsafe { self.clipboard.pasteboard.writeObjects(&objects) }; + + add_clipboard_exclusions(self.clipboard, self.exclude_from_history); + + if success { + Ok(()) + } else { + Err(Error::unknown("NSPasteboard#writeObjects: returned false")) + } + } } pub(crate) struct Clear<'clipboard> { diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 29ab850..9880e88 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -11,7 +11,25 @@ and conditions of the chosen license apply to this file. #[cfg(feature = "image-data")] use crate::common::ImageData; use crate::common::{private, Error}; -use std::{borrow::Cow, marker::PhantomData, path::PathBuf, thread, time::Duration}; +use std::{ + borrow::Cow, + io, + marker::PhantomData, + os::windows::{fs::OpenOptionsExt, io::AsRawHandle}, + path::{Path, PathBuf}, + thread, + time::Duration, +}; +use windows_sys::Win32::{ + Foundation::{GetLastError, GlobalFree, HANDLE, HGLOBAL, POINT, S_OK}, + Storage::FileSystem::{GetFinalPathNameByHandleW, FILE_FLAG_BACKUP_SEMANTICS, VOLUME_NAME_DOS}, + System::{ + DataExchange::SetClipboardData, + Memory::{GlobalAlloc, GlobalLock, GlobalUnlock, GHND}, + Ole::CF_HDROP, + }, + UI::Shell::{PathCchStripPrefix, DROPFILES}, +}; #[cfg(feature = "image-data")] mod image_data { @@ -20,38 +38,16 @@ mod image_data { use image::codecs::png::PngEncoder; use image::ExtendedColorType; use image::ImageEncoder; - use std::{convert::TryInto, io, mem::size_of, ptr::copy_nonoverlapping}; + use std::{convert::TryInto, mem::size_of, ptr::copy_nonoverlapping}; use windows_sys::Win32::{ - Foundation::{HANDLE, HGLOBAL}, Graphics::Gdi::{ CreateDIBitmap, DeleteObject, GetDC, GetDIBits, BITMAPINFO, BITMAPINFOHEADER, BITMAPV5HEADER, BI_BITFIELDS, BI_RGB, CBM_INIT, DIB_RGB_COLORS, HBITMAP, HDC, HGDIOBJ, LCS_GM_IMAGES, RGBQUAD, }, - System::{ - DataExchange::SetClipboardData, - Memory::{GlobalAlloc, GlobalLock, GlobalUnlock, GHND}, - Ole::CF_DIBV5, - }, + System::Ole::CF_DIBV5, }; - fn last_error(message: &str) -> Error { - let os_error = io::Error::last_os_error(); - Error::unknown(format!("{message}: {os_error}")) - } - - unsafe fn global_unlock_checked(hdata: HGLOBAL) { - // If the memory object is unlocked after decrementing the lock count, the function - // returns zero and GetLastError returns NO_ERROR. If it fails, the return value is - // zero and GetLastError returns a value other than NO_ERROR. - if GlobalUnlock(hdata) == 0 { - let err = io::Error::last_os_error(); - if err.raw_os_error() != Some(0) { - log::error!("Failed calling GlobalUnlock when writing data: {}", err); - } - } - } - pub(super) fn add_cf_dibv5( _open_clipboard: OpenClipboard, image: ImageData, @@ -168,24 +164,6 @@ mod image_data { } } - unsafe fn global_alloc(bytes: usize) -> Result { - let hdata = GlobalAlloc(GHND, bytes); - if hdata.is_null() { - Err(last_error("Could not allocate global memory object")) - } else { - Ok(hdata) - } - } - - unsafe fn global_lock(hmem: HGLOBAL) -> Result<*mut u8, Error> { - let data_ptr = GlobalLock(hmem).cast::(); - if data_ptr.is_null() { - Err(last_error("Could not lock the global memory object")) - } else { - Ok(data_ptr) - } - } - pub(super) fn read_cf_dibv5(dibv5: &[u8]) -> Result, Error> { // The DIBV5 format is a BITMAPV5HEADER followed by the pixel data according to // https://docs.microsoft.com/en-us/windows/win32/dataxchg/standard-clipboard-formats @@ -320,32 +298,6 @@ mod image_data { } } - /// An abstraction trait over the different ways a Win32 function may return - /// a value with a failure marker. - /// - /// This is primarily to abstract over changes in `windows-sys` versions and unify how - /// error handling is done in the above image code. - trait ResultValue: Sized { - const NULL: Self; - fn failure(self) -> bool; - } - - // windows-sys >= 0.59 - impl ResultValue for *mut T { - const NULL: Self = core::ptr::null_mut(); - fn failure(self) -> bool { - self == Self::NULL - } - } - - // `windows-sys` 0.52 - impl ResultValue for isize { - const NULL: Self = 0; - fn failure(self) -> bool { - self == Self::NULL - } - } - /// Converts the RGBA (u8) pixel data into the bitmap-native ARGB (u32) /// format in-place. /// @@ -487,6 +439,67 @@ mod image_data { } } +unsafe fn global_alloc(bytes: usize) -> Result { + let hdata = GlobalAlloc(GHND, bytes); + if hdata.is_null() { + Err(last_error("Could not allocate global memory object")) + } else { + Ok(hdata) + } +} + +unsafe fn global_lock(hmem: HGLOBAL) -> Result<*mut u8, Error> { + let data_ptr = GlobalLock(hmem).cast::(); + if data_ptr.is_null() { + Err(last_error("Could not lock the global memory object")) + } else { + Ok(data_ptr) + } +} + +unsafe fn global_unlock_checked(hdata: HGLOBAL) { + // If the memory object is unlocked after decrementing the lock count, the function + // returns zero and GetLastError returns NO_ERROR. If it fails, the return value is + // zero and GetLastError returns a value other than NO_ERROR. + if GlobalUnlock(hdata) == 0 { + let err = io::Error::last_os_error(); + if err.raw_os_error() != Some(0) { + log::error!("Failed calling GlobalUnlock when writing data: {}", err); + } + } +} + +fn last_error(message: &str) -> Error { + let os_error = io::Error::last_os_error(); + Error::unknown(format!("{message}: {os_error}")) +} + +/// An abstraction trait over the different ways a Win32 function may return +/// a value with a failure marker. +/// +/// This trait helps unify error handling across varying `windows-sys` versions, +/// providing a consistent interface for representing NULL values. +trait ResultValue: Sized { + const NULL: Self; + fn failure(self) -> bool; +} + +// windows-sys >= 0.59 +impl ResultValue for *mut T { + const NULL: Self = core::ptr::null_mut(); + fn failure(self) -> bool { + self == Self::NULL + } +} + +// `windows-sys` 0.52 +impl ResultValue for isize { + const NULL: Self = 0; + fn failure(self) -> bool { + self == Self::NULL + } +} + /// A shim clipboard type that can have operations performed with it, but /// does not represent an open clipboard itself. /// @@ -702,6 +715,74 @@ impl<'clipboard> Set<'clipboard> { image_data::add_cf_dibv5(open_clipboard, image)?; Ok(()) } + + pub(crate) fn file_list(self, file_list: &[impl AsRef]) -> Result<(), Error> { + const DROPFILES_HEADER_SIZE: usize = std::mem::size_of::(); + + let clipboard_assertion = self.clipboard?; + + // https://learn.microsoft.com/en-us/windows/win32/shell/clipboard#cf_hdrop + // CF_HDROP consists of an STGMEDIUM structure that contains a global memory object. + // The structure's hGlobal member points to the resulting data: + // | DROPFILES | FILENAME | NULL | ... | nth FILENAME | NULL | NULL | + let dropfiles = DROPFILES { + pFiles: DROPFILES_HEADER_SIZE as u32, + pt: POINT { x: 0, y: 0 }, + fNC: 0, + fWide: 1, + }; + + let mut data_len = DROPFILES_HEADER_SIZE; + + let paths: Vec<_> = file_list + .iter() + .filter_map(|path| { + to_final_path_wide(path.as_ref()).map(|wide| { + // Windows uses wchar_t which is 16 bit + data_len += wide.len() * std::mem::size_of::(); + wide + }) + }) + .collect(); + + if paths.is_empty() { + return Err(Error::ConversionFailure); + } + + // Add space for the final null character + data_len += std::mem::size_of::(); + + unsafe { + let h_global = global_alloc(data_len)?; + let data_ptr = global_lock(h_global)?; + + (data_ptr as *mut DROPFILES).write(dropfiles); + + let mut ptr = data_ptr.add(DROPFILES_HEADER_SIZE) as *mut u16; + + for wide_path in paths { + std::ptr::copy_nonoverlapping::(wide_path.as_ptr(), ptr, wide_path.len()); + ptr = ptr.add(wide_path.len()); + } + + // Write final null character + ptr.write(0); + + global_unlock_checked(h_global); + + if SetClipboardData(CF_HDROP.into(), h_global as HANDLE).failure() { + GlobalFree(h_global); + return Err(last_error("SetClipboardData failed with error")); + } + } + + add_clipboard_exclusions( + clipboard_assertion, + self.exclude_from_monitoring, + self.exclude_from_cloud, + self.exclude_from_history, + ) + } } fn add_clipboard_exclusions( @@ -826,3 +907,102 @@ fn wrap_html(ctn: &str) -> String { "{h_version}{h_start_html}{n_start_html:010}{h_end_html}{n_end_html:010}{h_start_frag}{n_start_frag:010}{h_end_frag}{n_end_frag:010}{c_start_frag}{ctn}{c_end_frag}" ) } + +/// Given a file path attempt to open it and call GetFinalPathNameByHandleW, +/// on success return the final path as a NULL terminated u16 Vec +fn to_final_path_wide(p: &Path) -> Option> { + let file = std::fs::OpenOptions::new() + // No read or write permissions are necessary + .access_mode(0) + // This flag is so we can open directories too + .custom_flags(FILE_FLAG_BACKUP_SEMANTICS) + .open(p) + .ok()?; + + fill_utf16_buf( + |buf, sz| unsafe { + GetFinalPathNameByHandleW(file.as_raw_handle() as HANDLE, buf, sz, VOLUME_NAME_DOS) + }, + |buf| { + let mut wide = Vec::with_capacity(buf.len() + 1); + wide.extend_from_slice(buf); + wide.push(0); + + let hr = unsafe { PathCchStripPrefix(wide.as_mut_ptr(), wide.len()) }; + // On success truncate invalid data + if hr == S_OK { + if let Some(end) = wide.iter().position(|c| *c == 0) { + // Retain NULL character + wide.truncate(end + 1) + } + } + wide + }, + ) +} + +/// +fn fill_utf16_buf(mut f1: F1, f2: F2) -> Option +where + F1: FnMut(*mut u16, u32) -> u32, + F2: FnOnce(&[u16]) -> T, +{ + // Start off with a stack buf but then spill over to the heap if we end up + // needing more space. + // + // This initial size also works around `GetFullPathNameW` returning + // incorrect size hints for some short paths: + // https://github.com/dylni/normpath/issues/5 + let mut stack_buf: [std::mem::MaybeUninit; 512] = [std::mem::MaybeUninit::uninit(); 512]; + let mut heap_buf: Vec> = Vec::new(); + unsafe { + let mut n = stack_buf.len(); + loop { + let buf = if n <= stack_buf.len() { + &mut stack_buf[..] + } else { + let extra = n - heap_buf.len(); + heap_buf.reserve(extra); + // We used `reserve` and not `reserve_exact`, so in theory we + // may have gotten more than requested. If so, we'd like to use + // it... so long as we won't cause overflow. + n = heap_buf.capacity().min(u32::MAX as usize); + // Safety: MaybeUninit does not need initialization + heap_buf.set_len(n); + &mut heap_buf[..] + }; + + // This function is typically called on windows API functions which + // will return the correct length of the string, but these functions + // also return the `0` on error. In some cases, however, the + // returned "correct length" may actually be 0! + // + // To handle this case we call `SetLastError` to reset it to 0 and + // then check it again if we get the "0 error value". If the "last + // error" is still 0 then we interpret it as a 0 length buffer and + // not an actual error. + windows_sys::Win32::Foundation::SetLastError(0); + let k = match f1(buf.as_mut_ptr().cast::(), n as u32) { + 0 if GetLastError() == 0 => 0, + 0 => return None, + n => n, + } as usize; + if k == n && GetLastError() == windows_sys::Win32::Foundation::ERROR_INSUFFICIENT_BUFFER + { + n = n.saturating_mul(2).min(u32::MAX as usize); + } else if k > n { + n = k; + } else if k == n { + // It is impossible to reach this point. + // On success, k is the returned string length excluding the null. + // On failure, k is the required buffer length including the null. + // Therefore k never equals n. + unreachable!(); + } else { + // Safety: First `k` values are initialized. + let slice = std::slice::from_raw_parts(buf.as_ptr() as *const u16, k); + return Some(f2(slice)); + } + } + } +} From e6008eaa9164fc2d202fbfa1ed6ea3419169f2c9 Mon Sep 17 00:00:00 2001 From: wcassels Date: Sat, 9 Aug 2025 16:31:49 +0100 Subject: [PATCH 73/82] Use image for reading DIB and try to make it do the right thing for 32-bit BI_RGBs --- Cargo.toml | 2 +- src/platform/windows.rs | 186 +++++++++++++--------------------------- 2 files changed, 60 insertions(+), 128 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5bc7c67..075a52a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,7 @@ windows-sys = { version = ">=0.52.0, <0.60.0", features = [ clipboard-win = { version = "5.3.1", features = ["std"] } log = "0.4" image = { version = "0.25", optional = true, default-features = false, features = [ - "png", + "png", "bmp" ] } [target.'cfg(target_os = "macos")'.dependencies] diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 9880e88..bb2bf87 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -35,15 +35,16 @@ use windows_sys::Win32::{ mod image_data { use super::*; use crate::common::ScopeGuard; + use image::codecs::bmp::BmpDecoder; use image::codecs::png::PngEncoder; + use image::DynamicImage; use image::ExtendedColorType; + use image::ImageDecoder; use image::ImageEncoder; use std::{convert::TryInto, mem::size_of, ptr::copy_nonoverlapping}; use windows_sys::Win32::{ Graphics::Gdi::{ - CreateDIBitmap, DeleteObject, GetDC, GetDIBits, BITMAPINFO, BITMAPINFOHEADER, - BITMAPV5HEADER, BI_BITFIELDS, BI_RGB, CBM_INIT, DIB_RGB_COLORS, HBITMAP, HDC, HGDIOBJ, - LCS_GM_IMAGES, RGBQUAD, + DeleteObject, BITMAPV5HEADER, BI_BITFIELDS, BI_RGB, HGDIOBJ, LCS_GM_IMAGES, }, System::Ole::CF_DIBV5, }; @@ -164,138 +165,68 @@ mod image_data { } } - pub(super) fn read_cf_dibv5(dibv5: &[u8]) -> Result, Error> { + // https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapv5header + // According to the docs, when bV5Compression is BI_RGB, "the high byte in each DWORD + // is not used". + // This seems to not be respected in the real world. For example, Chrome, and Chromium + // & Electron-based programs send us BI_RGB headers, but with bitCount=32 - and important + // transparency bytes in the alpha channel. + // + // Apparently, it's our job as the consumer to do the right thing. This method fiddles + // with the header a bit in these cases, then `image` handles the rest. + fn maybe_tweak_header(dibv5: &mut [u8]) { + const BITCOUNT_OFFSET: usize = 14; + + let bitcount = + u16::from_le_bytes(dibv5[BITCOUNT_OFFSET..BITCOUNT_OFFSET + 2].try_into().unwrap()); + + const COMPRESSION_OFFSET: usize = 16; + const MASK_OFFSET: usize = 40; + + let read_u32 = |offset: usize| -> u32 { + let slice = &dibv5[offset..offset + 4]; + u32::from_le_bytes(slice.try_into().unwrap()) + }; + + let compression = read_u32(COMPRESSION_OFFSET); + let red_mask = read_u32(MASK_OFFSET); + let green_mask = read_u32(MASK_OFFSET + 4); + let blue_mask = read_u32(MASK_OFFSET + 8); + let alpha_mask = read_u32(MASK_OFFSET + 12); + + let mut set = |offset: usize, val: u32| { + dibv5[offset..offset + 4].copy_from_slice(val.to_le_bytes().as_slice()); + }; + + if bitcount == 32 && compression == BI_RGB && alpha_mask == 0xff000000 { + set(COMPRESSION_OFFSET, BI_BITFIELDS); + if red_mask == 0 && green_mask == 0 && blue_mask == 0 { + set(MASK_OFFSET, 0xff0000); + set(MASK_OFFSET + 4, 0xff00); + set(MASK_OFFSET + 8, 0xff); + } + } + } + + pub(super) fn read_cf_dibv5(dibv5: &mut [u8]) -> Result, Error> { // The DIBV5 format is a BITMAPV5HEADER followed by the pixel data according to // https://docs.microsoft.com/en-us/windows/win32/dataxchg/standard-clipboard-formats - // These constants are missing in windows-rs - const PROFILE_EMBEDDED: u32 = 0x4D42_4544; - const PROFILE_LINKED: u32 = 0x4C49_4E4B; - - // so first let's get a pointer to the header let header_size = size_of::(); if dibv5.len() < header_size { return Err(Error::unknown("When reading the DIBV5 data, it contained fewer bytes than the BITMAPV5HEADER size. This is invalid.")); } - let header = unsafe { &*(dibv5.as_ptr().cast::()) }; + maybe_tweak_header(dibv5); - let has_profile = - header.bV5CSType == PROFILE_LINKED || header.bV5CSType == PROFILE_EMBEDDED; + let decoder = BmpDecoder::new_without_file_header(std::io::Cursor::new(&*dibv5)) + .map_err(|_| Error::unknown("Failed to read bitmap header"))?; + let (width, height) = decoder.dimensions(); + let bytes = DynamicImage::from_decoder(decoder) + .map_err(|_| Error::unknown("Failed to read bitmap"))? + .into_rgba8() + .into_raw(); - let pixel_data_start = if has_profile { - header.bV5ProfileData as isize + header.bV5ProfileSize as isize - } else { - header_size as isize - }; - - unsafe { - let image_bytes = dibv5.as_ptr().offset(pixel_data_start); - let hdc = get_screen_device_context()?; - let hbitmap = create_bitmap_from_dib(hdc, header, image_bytes)?; - // Now extract the pixels in a desired format - let w = header.bV5Width; - let h = header.bV5Height.abs(); - let result_size = w as usize * h as usize * 4; - - let mut result_bytes = Vec::::with_capacity(result_size); - - let mut output_header = BITMAPINFO { - bmiColors: [RGBQUAD { rgbRed: 0, rgbGreen: 0, rgbBlue: 0, rgbReserved: 0 }], - bmiHeader: BITMAPINFOHEADER { - biSize: size_of::() as u32, - biWidth: w, - biHeight: -h, - biBitCount: 32, - biPlanes: 1, - biCompression: BI_RGB, - biSizeImage: 0, - biXPelsPerMeter: 0, - biYPelsPerMeter: 0, - biClrUsed: 0, - biClrImportant: 0, - }, - }; - - let lines = convert_bitmap_to_rgb( - hdc, - hbitmap, - h, - result_bytes.as_mut_slice(), - &mut output_header, - )?; - let read_len = lines as usize * w as usize * 4; - assert!( - read_len <= result_bytes.capacity(), - "Segmentation fault. Read more bytes than allocated to pixel buffer", - ); - result_bytes.set_len(read_len); - - let result_bytes = win_to_rgba(&mut result_bytes); - - let result = ImageData { - bytes: Cow::Owned(result_bytes), - width: w as usize, - height: h as usize, - }; - Ok(result) - } - } - - fn get_screen_device_context() -> Result { - // SAFETY: Calling `GetDC` with `NULL` is safe. - let hdc = unsafe { GetDC(ResultValue::NULL) }; - if hdc.failure() { - Err(Error::unknown("Failed to get the device context. GetDC returned null")) - } else { - Ok(hdc) - } - } - - unsafe fn create_bitmap_from_dib( - hdc: HDC, - header: *const BITMAPV5HEADER, - image_bytes: *const u8, - ) -> Result { - let hbitmap = CreateDIBitmap( - hdc, - header.cast(), - CBM_INIT as u32, - image_bytes.cast(), - header.cast(), - DIB_RGB_COLORS, - ); - if hbitmap.failure() { - Err(Error::unknown( - "Failed to create the HBITMAP while reading DIBV5. CreateDIBitmap returned null", - )) - } else { - Ok(hbitmap) - } - } - - /// Copies the bitmap image into given buffer with DIB RGB format and - /// returns the number of scan lines copied from the bitmap. - unsafe fn convert_bitmap_to_rgb( - hdc: HDC, - hbitmap: HBITMAP, - lines: i32, - dst: &mut [u8], - header: &mut BITMAPINFO, - ) -> Result { - let lines = GetDIBits( - hdc, - hbitmap, - 0, - lines as u32, - dst.as_mut_ptr().cast(), - header, - DIB_RGB_COLORS, - ); - if lines == 0 { - Err(Error::unknown("Could not get the bitmap bits, GetDIBits returned 0")) - } else { - Ok(lines) - } + Ok(ImageData { width: width as usize, height: height as usize, bytes: bytes.into() }) } /// Converts the RGBA (u8) pixel data into the bitmap-native ARGB (u32) @@ -363,6 +294,7 @@ mod image_data { /// Safety: the `bytes` slice must have a length that's a multiple of 4 #[allow(clippy::identity_op, clippy::erasing_op)] #[must_use] + #[cfg(test)] unsafe fn win_to_rgba(bytes: &mut [u8]) -> Vec { // Check safety invariants to catch obvious bugs. debug_assert_eq!(bytes.len() % 4, 0); @@ -630,7 +562,7 @@ impl<'clipboard> Get<'clipboard> { clipboard_win::raw::get_vec(FORMAT, &mut data) .map_err(|_| Error::unknown("failed to read clipboard image data"))?; - image_data::read_cf_dibv5(&data) + image_data::read_cf_dibv5(&mut data) } pub(crate) fn file_list(self) -> Result, Error> { From a3c64f9a9362d3c2410092031ab571f6ae3efe5c Mon Sep 17 00:00:00 2001 From: wcassels Date: Sat, 9 Aug 2025 16:32:31 +0100 Subject: [PATCH 74/82] Add a couple of end-to-end DIBV5 tests --- src/platform/windows.rs | 74 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/src/platform/windows.rs b/src/platform/windows.rs index bb2bf87..a4cd582 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -369,6 +369,80 @@ mod image_data { let _converted = unsafe { win_to_rgba(&mut data) }; assert_eq!(data, DATA); } + + #[test] + fn firefox_dibv5() { + // A 5x5 sample of https://commons.wikimedia.org/wiki/File:PNG_transparency_demonstration_1.png + let mut raw = vec![ + 124, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 1, 0, 24, 0, 0, 0, 0, 0, 80, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0, 255, 0, 0, 0, 0, 255, 0, 0, 0, 0, + 255, 66, 71, 82, 115, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 36, 47, 144, 42, 68, 110, 48, 74, 66, 52, 74, + 49, 57, 80, 55, 0, 36, 53, 138, 45, 79, 98, 52, 82, 58, 56, 84, 52, 62, 91, 58, 0, 37, + 64, 129, 48, 88, 88, 54, 90, 54, 60, 96, 55, 66, 104, 62, 0, 40, 75, 120, 50, 96, 74, + 55, 99, 51, 62, 106, 57, 68, 113, 62, 0, 42, 89, 107, 50, 104, 60, 57, 108, 49, 64, + 114, 56, 71, 123, 65, 0, + ]; + + let before = raw.clone(); + let image = read_cf_dibv5(&mut raw).unwrap(); + + // Not expecting any header fiddling to happen here. This is a bitmap in 24-bit format, with a header + // that says as much + assert_eq!(raw, before); + + assert_eq!(image.width, 5); + assert_eq!(image.height, 5); + + const EXPECTED: &[u8] = &[ + 107, 89, 42, 255, 60, 104, 50, 255, 49, 108, 57, 255, 56, 114, 64, 255, 65, 123, 71, + 255, 120, 75, 40, 255, 74, 96, 50, 255, 51, 99, 55, 255, 57, 106, 62, 255, 62, 113, 68, + 255, 129, 64, 37, 255, 88, 88, 48, 255, 54, 90, 54, 255, 55, 96, 60, 255, 62, 104, 66, + 255, 138, 53, 36, 255, 98, 79, 45, 255, 58, 82, 52, 255, 52, 84, 56, 255, 58, 91, 62, + 255, 144, 47, 36, 255, 110, 68, 42, 255, 66, 74, 48, 255, 49, 74, 52, 255, 55, 80, 57, + 255, + ]; + assert_eq!(image.bytes, EXPECTED); + } + + #[test] + fn chrome_dibv5() { + // A 5x5 sample of https://commons.wikimedia.org/wiki/File:PNG_transparency_demonstration_1.png + // (interestingly, the same sample as in the Firefox test - despite the pixel data being + // materially different!) + let mut raw = vec![ + 124, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 1, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, + 32, 110, 105, 87, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 38, 145, 192, 38, 65, 111, 158, 46, 73, 68, + 107, 50, 73, 50, 92, 55, 79, 55, 100, 31, 46, 139, 190, 41, 76, 100, 152, 49, 81, 60, + 110, 53, 83, 53, 108, 60, 91, 60, 118, 32, 59, 131, 187, 44, 86, 89, 150, 51, 89, 56, + 121, 57, 95, 57, 127, 63, 103, 63, 139, 35, 71, 122, 186, 46, 95, 76, 150, 52, 99, 54, + 136, 59, 105, 59, 146, 65, 113, 65, 156, 37, 86, 109, 184, 46, 103, 63, 155, 52, 107, + 53, 152, 60, 114, 60, 162, 68, 123, 68, 174, + ]; + + let before = raw.clone(); + let image = read_cf_dibv5(&mut raw).unwrap(); + + // Chrome's header is dodgy. Expect that we fiddled with it. + assert_ne!(raw, before); + + assert_eq!(image.width, 5); + assert_eq!(image.height, 5); + + const EXPECTED: &[u8] = &[ + 109, 86, 37, 184, 63, 103, 46, 155, 53, 107, 52, 152, 60, 114, 60, 162, 68, 123, 68, + 174, 122, 71, 35, 186, 76, 95, 46, 150, 54, 99, 52, 136, 59, 105, 59, 146, 65, 113, 65, + 156, 131, 59, 32, 187, 89, 86, 44, 150, 56, 89, 51, 121, 57, 95, 57, 127, 63, 103, 63, + 139, 139, 46, 31, 190, 100, 76, 41, 152, 60, 81, 49, 110, 53, 83, 53, 108, 60, 91, 60, + 118, 145, 38, 32, 192, 111, 65, 38, 158, 68, 73, 46, 107, 50, 73, 50, 92, 55, 79, 55, + 100, + ]; + assert_eq!(image.bytes, EXPECTED); + } } unsafe fn global_alloc(bytes: usize) -> Result { From 16ef18113f5ab1793e9761df192cc699b8fd2113 Mon Sep 17 00:00:00 2001 From: wcassels Date: Sat, 9 Aug 2025 16:32:53 +0100 Subject: [PATCH 75/82] Implement fetching PNG on Windows and prefer over DIB when available --- src/platform/windows.rs | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/src/platform/windows.rs b/src/platform/windows.rs index a4cd582..7be2cd3 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -36,6 +36,7 @@ mod image_data { use super::*; use crate::common::ScopeGuard; use image::codecs::bmp::BmpDecoder; + use image::codecs::png::PngDecoder; use image::codecs::png::PngEncoder; use image::DynamicImage; use image::ExtendedColorType; @@ -229,6 +230,19 @@ mod image_data { Ok(ImageData { width: width as usize, height: height as usize, bytes: bytes.into() }) } + pub(super) fn read_png(data: &[u8]) -> Result, Error> { + let decoder = PngDecoder::new(std::io::Cursor::new(data)) + .map_err(|_| Error::unknown("Failed to read PNG header"))?; + let (width, height) = decoder.dimensions(); + + let bytes = DynamicImage::from_decoder(decoder) + .map_err(|_| Error::unknown("Failed to decode PNG"))? + .into_rgba8() + .into_raw(); + + Ok(ImageData { width: width as usize, height: height as usize, bytes: bytes.into() }) + } + /// Converts the RGBA (u8) pixel data into the bitmap-native ARGB (u32) /// format in-place. /// @@ -623,19 +637,23 @@ impl<'clipboard> Get<'clipboard> { #[cfg(feature = "image-data")] pub(crate) fn image(self) -> Result, Error> { - const FORMAT: u32 = clipboard_win::formats::CF_DIBV5; - let _clipboard_assertion = self.clipboard?; + let mut data = Vec::new(); - if !clipboard_win::is_format_avail(FORMAT) { + let png_format: Option = clipboard_win::register_format("PNG").map(From::from); + if let Some(id) = png_format.filter(|&id| clipboard_win::is_format_avail(id)) { + // Looks like PNG is available! Let's try it + clipboard_win::raw::get_vec(id, &mut data) + .map_err(|_| Error::unknown("failed to read clipboard PNG data"))?; + return image_data::read_png(&data); + } + + if !clipboard_win::is_format_avail(clipboard_win::formats::CF_DIBV5) { return Err(Error::ContentNotAvailable); } - let mut data = Vec::new(); - - clipboard_win::raw::get_vec(FORMAT, &mut data) + clipboard_win::raw::get_vec(clipboard_win::formats::CF_DIBV5, &mut data) .map_err(|_| Error::unknown("failed to read clipboard image data"))?; - image_data::read_cf_dibv5(&mut data) } From ff15a093d6e9c1c3cbc9b6b56fd6799575f6254e Mon Sep 17 00:00:00 2001 From: Will Cassels Date: Wed, 13 Aug 2025 20:54:41 +0100 Subject: [PATCH 76/82] Return conversionFailure instead of adhoc errors --- src/platform/windows.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 7be2cd3..0afcd48 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -220,10 +220,10 @@ mod image_data { maybe_tweak_header(dibv5); let decoder = BmpDecoder::new_without_file_header(std::io::Cursor::new(&*dibv5)) - .map_err(|_| Error::unknown("Failed to read bitmap header"))?; + .map_err(|_| Error::ConversionFailure)?; let (width, height) = decoder.dimensions(); let bytes = DynamicImage::from_decoder(decoder) - .map_err(|_| Error::unknown("Failed to read bitmap"))? + .map_err(|_| Error::ConversionFailure)? .into_rgba8() .into_raw(); @@ -231,12 +231,12 @@ mod image_data { } pub(super) fn read_png(data: &[u8]) -> Result, Error> { - let decoder = PngDecoder::new(std::io::Cursor::new(data)) - .map_err(|_| Error::unknown("Failed to read PNG header"))?; + let decoder = + PngDecoder::new(std::io::Cursor::new(data)).map_err(|_| Error::ConversionFailure)?; let (width, height) = decoder.dimensions(); let bytes = DynamicImage::from_decoder(decoder) - .map_err(|_| Error::unknown("Failed to decode PNG"))? + .map_err(|_| Error::ConversionFailure)? .into_rgba8() .into_raw(); From 55c0b260c47920e0e880bbf1fbe9d1772162bc8e Mon Sep 17 00:00:00 2001 From: wcassels Date: Wed, 13 Aug 2025 23:39:51 +0100 Subject: [PATCH 77/82] read/write_unaligned rather than using manual field offsets --- src/platform/windows.rs | 42 ++++++++++++++--------------------------- 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 0afcd48..cb11598 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -176,36 +176,22 @@ mod image_data { // Apparently, it's our job as the consumer to do the right thing. This method fiddles // with the header a bit in these cases, then `image` handles the rest. fn maybe_tweak_header(dibv5: &mut [u8]) { - const BITCOUNT_OFFSET: usize = 14; + assert!(dibv5.len() >= size_of::()); + let src = dibv5.as_mut_ptr().cast::(); + let mut header = unsafe { std::ptr::read_unaligned(src) }; - let bitcount = - u16::from_le_bytes(dibv5[BITCOUNT_OFFSET..BITCOUNT_OFFSET + 2].try_into().unwrap()); - - const COMPRESSION_OFFSET: usize = 16; - const MASK_OFFSET: usize = 40; - - let read_u32 = |offset: usize| -> u32 { - let slice = &dibv5[offset..offset + 4]; - u32::from_le_bytes(slice.try_into().unwrap()) - }; - - let compression = read_u32(COMPRESSION_OFFSET); - let red_mask = read_u32(MASK_OFFSET); - let green_mask = read_u32(MASK_OFFSET + 4); - let blue_mask = read_u32(MASK_OFFSET + 8); - let alpha_mask = read_u32(MASK_OFFSET + 12); - - let mut set = |offset: usize, val: u32| { - dibv5[offset..offset + 4].copy_from_slice(val.to_le_bytes().as_slice()); - }; - - if bitcount == 32 && compression == BI_RGB && alpha_mask == 0xff000000 { - set(COMPRESSION_OFFSET, BI_BITFIELDS); - if red_mask == 0 && green_mask == 0 && blue_mask == 0 { - set(MASK_OFFSET, 0xff0000); - set(MASK_OFFSET + 4, 0xff00); - set(MASK_OFFSET + 8, 0xff); + if header.bV5BitCount == 32 + && header.bV5Compression == BI_RGB + && header.bV5AlphaMask == 0xff000000 + { + header.bV5Compression = BI_BITFIELDS; + if header.bV5RedMask == 0 && header.bV5GreenMask == 0 && header.bV5BlueMask == 0 { + header.bV5RedMask = 0xff0000; + header.bV5GreenMask = 0xff00; + header.bV5BlueMask = 0xff; } + + unsafe { std::ptr::write_unaligned(src, header) }; } } From 7bdd1c11754cb3b09b2c8270a7c51386194d0542 Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Thu, 21 Aug 2025 15:16:14 -0500 Subject: [PATCH 78/82] Update errno for windows-sys 0.60 flexibility --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index df18e6f..1cfbe68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -118,9 +118,9 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.10" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", "windows-sys", From 26a96a6199ab7903e709bbe0acb87599b8450749 Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Thu, 21 Aug 2025 15:18:09 -0500 Subject: [PATCH 79/82] Bump windows-sys semver range to support 0.60.x --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 075a52a..56723e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ wl-clipboard-rs = ["dep:wl-clipboard-rs"] env_logger = "0.10.2" [target.'cfg(windows)'.dependencies] -windows-sys = { version = ">=0.52.0, <0.60.0", features = [ +windows-sys = { version = ">=0.52.0, <0.61.0", features = [ "Win32_Foundation", "Win32_Storage_FileSystem", "Win32_System_DataExchange", From edcce2cd6b336c5dda98ef860648a9e4babf272f Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Sat, 23 Aug 2025 14:46:37 -0500 Subject: [PATCH 80/82] Remove CHANGELOG.md in favor of GitHub releases All existing data here already existed or has been copied to a GitHub release --- CHANGELOG.md | 196 --------------------------------------------------- 1 file changed, 196 deletions(-) delete mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 3b1fc35..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,196 +0,0 @@ -# Changelog - -## 3.6.0 on 2025-06-27 - -### Added -- Add support for excluding data from clipboard history on Linux. -- `arboard`, in debug builds, now attempts to call out clipboard lifetime mishandling. - - This is a debugging feature, and as such has no absolute or promised behavior. - -### Changed -- The background thread in the X11 backend no longer exits on every selection request failure. - -### Fixed -- Handled cases where using an unsupported Primary clipboard on Wayland would return the wrong error. -- Clearing the clipboard on Linux now behaves correctly when interacting with other apps on the system. -- Pasting text with an explicit locale ID on Windows now works as intended. - -## 3.5.0 on 2025-04-01 - -### Added -- Add `file_list` to the `Get` interface. -- Implement `Get::html()` for all platforms. - -### Changed -- Updated `wl-clipboard-rs` to `0.9`. -- Improved `windows-sys` version compatibility range to support `0.52` - `0.60`. -- Updated `objc2` to `v0.6`. -- Raised MSRV to 1.71.0. - -## 3.4.1 on 2024-12-09 - -### Added -- Added support for excluding macOS clipboard items from history. - - Note that macOS has no official history, so arboard's implementation uses a community standard instead. - -## 3.4.0 on 2024-29-04 - -### Added -- Added a `wait_until` method for Linux, as a superset of the existing `wait` functionality. - This is a helper for letting an application wait without manual timeout handling. - -### Fixed -- Transparency in copied images now behaves better in certain Windows apps. - -### Changed -- Updated `image` to `0.25`. -- Removed direct `thiserror` dependency. -- Fixed Linux documentation links -- Raised MSRV to 1.67.1 -- Reverted timeout behavior of `Clipboard::new()` on platforms using X11. Applications are - encouraged to wrap constructor calls in their own thread/channel timeout mechanisms instead - to make sure the behavior matches each usecase. -- Migrated away from `objc` to the `objc2` ecosystem for the Apple clipboard implementation. - -## 3.3.2 on 2024-12-02 - -### Fixed -- Fixed compilation on Windows when using the `image-data` feature combined with older Rust compilers. - -## 3.3.1 on 2024-12-02 - -### Changed -- Updated Windows clipboard and migrated from `winapi` to `windows-sys`. -- Internally migrated to Rust 2021 edition. -- Significantly improved the crate's error documentation. -- Updated `core-graphics` to `0.23` -- Updated `x11rb` to `0.13` - -## 3.3.0 on 2023-20-11 - -### Added -- Add support for `ExcludeClipboardContentFromMonitorProcessing` on Windows platforms. - -### Changed -- Improved timeout error messaging. -- Update `wl-clipboard-rs` to `0.8`. -- Update `x11rb` to `0.12`. -- `arboard`'s MSRV is now 1.61. - -## 3.2.1 on 2023-29-08 - -### Fixed -- Removed all leaks from the macOS clipboard code. Previously, both the `get` and `set` methods leaked data. -- Fixed documentation examples so that they compile on Linux. -- Removed extra whitespace macOS's HTML copying template. This caused unexpected behavior in some apps. - -### Changed -- Added a timeout when connecting to the X11 server on UNIX platforms. In situations where the X11 socket is present but unusable, the clipboard - initialization will no longer hang indefinitely. -- Removed macOS-specific dependency on the `once_cell` crate. - -## 3.2.0 on 2022-04-11 - -### Changed -- The Windows clipboard now behaves consistently with the other -platform implementations again. -- Significantly improve cross-platform documentation of `Clipboard`. -- Remove lingering uses of the dbg! macro in the Wayland backend. - -## 3.1.1 on 2022-17-10 - -### Added -- Implemented the ability to set HTML on the clipboard - -### Changed -- Updated minimum `clipboard-win` version to `4.4`. -- Updated `wl-clipboard-rs` to the version `0.7`. - -## 3.1.0 on 2022-20-09 - -### Changed -- Updated `image` to the version `0.24`. -- Lowered Wayland clipboard initialization log level. - -## 3.0.0 on 2022-19-09 - -### Added -- Support for clearing the clipboard. -- Spport for excluding Windows clipboard data from cliboard history and OneDrive. -- Support waiting for another process to read clipboard data before returning -from a `write` call to a X11 and Wayland or clipboard - -### Changed -- Updated `wl-clipboard-rs` to the version `0.6`. -- Updated `x11rb` to the version `0.10`. -- Cleaned up spelling in documentation -- (Breaking) Functions that used to accept `String` now take `Into, str>` instead. -This avoids cloning the string more times then necessary on platforms that can. -- (Breaking) `Error` is now marked as `#[non_exhaustive]`. -- (Breaking) Removed all platform specific modules and clipboard structures from the public API. -If you were using these directly, the recommended replacement is using `arboard::Clipboard` and -the new platform-specific extension traits instead. -- (Breaking) On Windows, the clipboard is now opened once per call to `Clipboard::new()` instead of on -each operation. This means that instances of `Clipboard` should be dropped once you're performed the -needed operations to prevent other applications from working with it afterwards. - -## v2.1.1 on 2022-18-05 - -### Changed - -- Fix compilation on FreeBSD -- Internal cleanup and documentation fixes -- Remove direct dependency on the `once_cell` crate. -- Fixed crates.io repository link - -## v2.1.0 on 2022-09-03 - -### Changed - -- Updated most dependencies -- Removed crate deprecation -- Fixed soundness bug in Windows clipboard - -## v2.0.1 on 2021-11-05 - -### Changed - -- On X11, re-assert clipboard ownership every time the data changes. - -## v2.0.0 on 2021-08-07 - -### Changed - -- Update dependency on yanked crate versions -- Make the image operations an optional feature - -### Added - -- Support selecting which linux clipboard is used - -## v1.2.1 on 2021-05-04 - -### Changed - -- Fixed a bug that caused the `set_image` function on Windows to distort the - image colors. - -## v1.2.0 on 2021-04-06 - -### Added - -- Optional native wayland support through the `wl-clipboard-rs` crate. - -## v1.1.0 on 2020-12-29 - -### Changed - -- The `set_image` function on Windows now also provides the image in - `CF_BITMAP` format. - -## v1.0.2 on 2020-10-29 - -### Changed - -- Fixed the clipboard contents sometimes not being preserved after the program - exited. From a3750c79a5d63f3987317e03c412b7d9dffdc2af Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Sat, 23 Aug 2025 14:47:52 -0500 Subject: [PATCH 81/82] Release 3.6.1 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1cfbe68..c940d24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,7 +19,7 @@ dependencies = [ [[package]] name = "arboard" -version = "3.6.0" +version = "3.6.1" dependencies = [ "clipboard-win", "env_logger", diff --git a/Cargo.toml b/Cargo.toml index 56723e5..fae5b7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "arboard" -version = "3.6.0" +version = "3.6.1" description = "Image and text handling for the OS clipboard." repository = "https://github.com/1Password/arboard" license = "MIT OR Apache-2.0" From 223f4efc7a976ce403d6b74c0333833e092ed7b0 Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Fri, 12 Sep 2025 02:18:09 -0500 Subject: [PATCH 82/82] Return a conversion failure error when CGImageCreate fails --- src/platform/osx.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/platform/osx.rs b/src/platform/osx.rs index a2591eb..31952b6 100644 --- a/src/platform/osx.rs +++ b/src/platform/osx.rs @@ -34,7 +34,7 @@ fn image_from_pixels( pixels: Vec, width: usize, height: usize, -) -> Retained { +) -> Result, Error> { use objc2::AllocAnyThread; use objc2_app_kit::NSImage; use objc2_core_foundation::CGFloat; @@ -70,6 +70,13 @@ fn image_from_pixels( let colorspace = unsafe { CGColorSpaceCreateDeviceRGB() }.unwrap(); + // XXX: If this returns an error, try running your application from the command line or + // use `Console.app`. For the later, make sure that before you start streaming log messages + // that Action -> `Include Info Messages` and Action -> `Include Debug Messages` are both + // enabled in the menubar. CoreGraphics will write debugging/error information to these places. + // + // - https://redsweater.com/blog/129/coregraphics-log-jam + // - https://github.com/1Password/arboard/issues/204 let cg_image = unsafe { CGImageCreate( width, @@ -85,10 +92,10 @@ fn image_from_pixels( CGColorRenderingIntent::RenderingIntentDefault, ) } - .unwrap(); + .ok_or(Error::ConversionFailure)?; let size = NSSize { width: width as CGFloat, height: height as CGFloat }; - unsafe { NSImage::initWithCGImage_size(NSImage::alloc(), &cg_image, size) } + Ok(unsafe { NSImage::initWithCGImage_size(NSImage::alloc(), &cg_image, size) }) } pub(crate) struct Clipboard { @@ -335,7 +342,7 @@ impl<'clipboard> Set<'clipboard> { #[cfg(feature = "image-data")] pub(crate) fn image(self, data: ImageData) -> Result<(), Error> { let pixels = data.bytes.into(); - let image = image_from_pixels(pixels, data.width, data.height); + let image = image_from_pixels(pixels, data.width, data.height)?; self.clipboard.clear();