arboard-3.5.0/.cargo_vcs_info.json0000644000000001360000000000100124530ustar { "git": { "sha1": "91c33159b019b636f0a0419557d5d736196f9681" }, "path_in_vcs": "" }arboard-3.5.0/.github/workflows/test.yml000064400000000000000000000066301046102023000163470ustar 00000000000000name: Test on: push: branches: [ master ] pull_request: branches: [ master ] jobs: rustfmt: runs-on: ubuntu-22.04 steps: - uses: actions-rust-lang/setup-rust-toolchain@v1 with: toolchain: stable components: rustfmt - uses: actions/checkout@v4 - name: Check formatting run: cargo fmt --all -- --check clippy: needs: rustfmt runs-on: ${{ matrix.os }} 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.71.0"] steps: - uses: actions-rust-lang/setup-rust-toolchain@v1 with: toolchain: ${{ matrix.rust_version }} components: clippy - uses: actions/checkout@v4 - name: Run `cargo clippy` with no features if: ${{ matrix.rust_version == 'stable' }} 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' }} 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' }} run: cargo clippy --verbose --no-default-features --features wayland-data-control -- -D warnings -D clippy::dbg_macro - 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 }} strategy: matrix: # No Linux test for now as it just fails due to not having a desktop environment. os: [macos-latest, windows-latest] steps: - uses: actions-rust-lang/setup-rust-toolchain@v1 with: toolchain: stable - name: Checkout uses: actions/checkout@v4 - name: Run tests with no features run: cargo test --no-default-features - name: Run tests with `image-data` feature run: cargo test --no-default-features --features image-data - name: Run tests with `wayland-data-control` feature run: cargo test --no-default-features --features wayland-data-control - name: Run tests with all features run: cargo test --all-features miri: needs: clippy env: MIRIFLAGS: -Zmiri-symbolic-alignment-check runs-on: ${{ matrix.os }} strategy: matrix: # Currently, only Windows has soundness tests. os: [windows-latest] steps: - uses: actions-rust-lang/setup-rust-toolchain@v1 with: toolchain: nightly-2023-10-08 components: miri - name: Checkout uses: actions/checkout@v4 - name: Check soundness run: cargo miri test windows --features image-data semver: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Check semver uses: obi1kenobi/cargo-semver-checks-action@v2 arboard-3.5.0/.gitignore000064400000000000000000000000251046102023000132300ustar 00000000000000target .vscode *.png arboard-3.5.0/CHANGELOG.md000064400000000000000000000125651046102023000130650ustar 00000000000000# 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. ## 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. arboard-3.5.0/Cargo.lock0000644000000544440000000000100104410ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "adler" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aho-corasick" version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" dependencies = [ "memchr", ] [[package]] name = "arboard" version = "3.5.0" dependencies = [ "clipboard-win", "env_logger", "image", "log", "objc2", "objc2-app-kit", "objc2-core-foundation", "objc2-core-graphics", "objc2-foundation", "parking_lot", "percent-encoding", "windows-sys", "wl-clipboard-rs", "x11rb", ] [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" [[package]] name = "bytemuck" version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f5715e491b5a1598fc2bef5a606847b5dc1d48ea625bd3c02c00de8285591da" [[package]] name = "byteorder" 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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clipboard-win" version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79f4473f5144e20d9aceaf2972478f06ddf687831eafeeb434fbaf0acc4144ad" dependencies = [ "error-code", ] [[package]] name = "crc32fast" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" dependencies = [ "cfg-if", ] [[package]] name = "downcast-rs" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" [[package]] name = "env_logger" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" dependencies = [ "humantime", "is-terminal", "log", "regex", "termcolor", ] [[package]] name = "errno" version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", "windows-sys", ] [[package]] name = "error-code" version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "281e452d3bad4005426416cdba5ccfd4f5c1280e10099e21db27f7c1c28347fc" [[package]] name = "fastrand" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "fixedbitset" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" dependencies = [ "crc32fast", "miniz_oxide", ] [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "gethostname" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" dependencies = [ "libc", "windows-targets 0.48.0", ] [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hermit-abi" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" [[package]] name = "humantime" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "image" version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11" dependencies = [ "bytemuck", "byteorder", "num-traits", "png", "tiff", ] [[package]] name = "indexmap" version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" dependencies = [ "autocfg", "hashbrown", ] [[package]] name = "is-terminal" version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37" dependencies = [ "hermit-abi", "libc", "windows-sys", ] [[package]] name = "jpeg-decoder" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" [[package]] name = "libc" version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "linux-raw-sys" version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "lock_api" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "memchr" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" dependencies = [ "adler", ] [[package]] name = "nom" version = "7.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" dependencies = [ "memchr", "minimal-lexical", ] [[package]] name = "num-traits" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg", ] [[package]] name = "objc2" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3531f65190d9cff863b77a99857e74c314dd16bf56c538c4b57c7cbc3f3a6e59" dependencies = [ "objc2-encode", ] [[package]] name = "objc2-app-kit" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5906f93257178e2f7ae069efb89fbd6ee94f0592740b5f8a1512ca498814d0fb" dependencies = [ "bitflags 2.8.0", "objc2", "objc2-core-graphics", "objc2-foundation", ] [[package]] name = "objc2-core-foundation" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daeaf60f25471d26948a1c2f840e3f7d86f4109e3af4e8e4b5cd70c39690d925" dependencies = [ "bitflags 2.8.0", "objc2", ] [[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.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" [[package]] name = "objc2-foundation" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a21c6c9014b82c39515db5b396f91645182611c97d24637cf56ac01e5f8d998" dependencies = [ "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]] name = "once_cell" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" [[package]] name = "os_pipe" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29d73ba8daf8fac13b0501d1abeddcfe21ba7401ada61a819144b6c2a4f32209" dependencies = [ "libc", "windows-sys", ] [[package]] name = "parking_lot" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5014253a1331579ce62aa67443b4a658c5e7dd03d4bc6d302b94474888143" dependencies = [ "fixedbitset", "indexmap", ] [[package]] name = "pkg-config" version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" [[package]] name = "png" version = "0.17.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f0e7f4c94ec26ff209cee506314212639d6c91b80afb82984819fafce9df01c" dependencies = [ "bitflags 1.3.2", "crc32fast", "flate2", "miniz_oxide", ] [[package]] name = "proc-macro2" version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] [[package]] name = "quick-xml" version = "0.37.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003" dependencies = [ "memchr", ] [[package]] name = "quote" version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] [[package]] name = "redox_syscall" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ "bitflags 2.8.0", ] [[package]] name = "regex" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.6.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" [[package]] name = "rustix" version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ "bitflags 2.8.0", "errno", "libc", "linux-raw-sys", "windows-sys", ] [[package]] name = "scopeguard" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "smallvec" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" [[package]] name = "syn" version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52205623b1b0f064a4e71182c3b18ae902267282930c6d5462c91b859668426e" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tempfile" version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand", "rustix", "windows-sys", ] [[package]] name = "termcolor" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" dependencies = [ "winapi-util", ] [[package]] name = "thiserror" version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c53f98874615aea268107765aa1ed8f6116782501d18e53d08b471733bea6c85" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8b463991b4eab2d801e724172285ec4195c650e8ec79b149e6c2a8e6dd3f783" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tiff" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" dependencies = [ "flate2", "jpeg-decoder", "weezl", ] [[package]] name = "tree_magic_mini" version = "3.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aac5e8971f245c3389a5a76e648bfc80803ae066a1243a75db0064d7c1129d63" dependencies = [ "fnv", "memchr", "nom", "once_cell", "petgraph", ] [[package]] name = "unicode-ident" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" [[package]] name = "wayland-backend" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7208998eaa3870dad37ec8836979581506e0c5c64c20c9e79e9d2a10d6f47bf" dependencies = [ "cc", "downcast-rs", "rustix", "smallvec", "wayland-sys", ] [[package]] name = "wayland-client" version = "0.31.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2120de3d33638aaef5b9f4472bff75f07c56379cf76ea320bd3a3d65ecaf73f" dependencies = [ "bitflags 2.8.0", "rustix", "wayland-backend", "wayland-scanner", ] [[package]] name = "wayland-protocols" version = "0.32.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0781cf46869b37e36928f7b432273c0995aa8aed9552c556fb18754420541efc" dependencies = [ "bitflags 2.8.0", "wayland-backend", "wayland-client", "wayland-scanner", ] [[package]] name = "wayland-protocols-wlr" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "248a02e6f595aad796561fa82d25601bd2c8c3b145b1c7453fc8f94c1a58f8b2" dependencies = [ "bitflags 2.8.0", "wayland-backend", "wayland-client", "wayland-protocols", "wayland-scanner", ] [[package]] name = "wayland-scanner" version = "0.31.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484" dependencies = [ "proc-macro2", "quick-xml", "quote", ] [[package]] name = "wayland-sys" version = "0.31.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615" dependencies = [ "pkg-config", ] [[package]] name = "weezl" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" [[package]] name = "winapi-util" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ "windows-sys", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-targets" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" dependencies = [ "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]] name = "windows_aarch64_gnullvm" 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.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4de22eebb1d1e2bad2d970086e96da0e12cde0b411321e5b0f7b2a1f876aa26f" dependencies = [ "libc", "log", "os_pipe", "rustix", "tempfile", "thiserror", "tree_magic_mini", "wayland-backend", "wayland-client", "wayland-protocols", "wayland-protocols-wlr", ] [[package]] name = "x11rb" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8f25ead8c7e4cba123243a6367da5d3990e0d3affa708ea19dce96356bd9f1a" dependencies = [ "gethostname", "rustix", "x11rb-protocol", ] [[package]] name = "x11rb-protocol" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e63e71c4b8bd9ffec2c963173a4dc4cbde9ee96961d4fcb4429db9929b606c34" arboard-3.5.0/Cargo.toml0000644000000103470000000000100104560ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.71.0" name = "arboard" version = "3.5.0" authors = [ "Artur Kovacs ", "Avi Weinstock ", "Arboard contributors", ] build = false autobins = false autoexamples = false autotests = false autobenches = false description = "Image and text handling for the OS clipboard." readme = "README.md" keywords = [ "clipboard", "image", ] license = "MIT OR Apache-2.0" repository = "https://github.com/1Password/arboard" [lib] name = "arboard" path = "src/lib.rs" [[example]] name = "daemonize" path = "examples/daemonize.rs" [[example]] name = "get_image" path = "examples/get_image.rs" required-features = ["image-data"] [[example]] name = "hello_world" path = "examples/hello_world.rs" [[example]] name = "set_get_html" path = "examples/set_get_html.rs" [[example]] name = "set_image" path = "examples/set_image.rs" required-features = ["image-data"] [dependencies] [dev-dependencies.env_logger] version = "0.10.2" [features] core-graphics = ["dep:objc2-core-graphics"] default = ["image-data"] image = ["dep:image"] image-data = [ "dep:objc2-core-graphics", "dep:objc2-core-foundation", "image", "windows-sys", "core-graphics", ] wayland-data-control = ["wl-clipboard-rs"] windows-sys = ["dep:windows-sys"] wl-clipboard-rs = ["dep:wl-clipboard-rs"] [target.'cfg(all(unix, not(any(target_os="macos", target_os="android", target_os="emscripten"))))'.dependencies.image] version = "0.25" features = ["png"] optional = true default-features = false [target.'cfg(all(unix, not(any(target_os="macos", target_os="android", target_os="emscripten"))))'.dependencies.log] version = "0.4" [target.'cfg(all(unix, not(any(target_os="macos", target_os="android", target_os="emscripten"))))'.dependencies.parking_lot] version = "0.12" [target.'cfg(all(unix, not(any(target_os="macos", target_os="android", target_os="emscripten"))))'.dependencies.percent-encoding] version = "2.3.1" [target.'cfg(all(unix, not(any(target_os="macos", target_os="android", target_os="emscripten"))))'.dependencies.wl-clipboard-rs] version = "0.9.0" optional = true [target.'cfg(all(unix, not(any(target_os="macos", target_os="android", target_os="emscripten"))))'.dependencies.x11rb] version = "0.13" [target.'cfg(target_os = "macos")'.dependencies.image] version = "0.25" features = ["tiff"] optional = true default-features = false [target.'cfg(target_os = "macos")'.dependencies.objc2] version = "0.6.0" [target.'cfg(target_os = "macos")'.dependencies.objc2-app-kit] version = "0.3.0" features = [ "std", "objc2-core-graphics", "NSPasteboard", "NSPasteboardItem", "NSImage", ] default-features = false [target.'cfg(target_os = "macos")'.dependencies.objc2-core-foundation] version = "0.3.0" features = [ "std", "CFCGTypes", ] optional = true default-features = false [target.'cfg(target_os = "macos")'.dependencies.objc2-core-graphics] version = "0.3.0" features = [ "std", "CGImage", "CGColorSpace", "CGDataProvider", ] optional = true default-features = false [target.'cfg(target_os = "macos")'.dependencies.objc2-foundation] version = "0.3.0" features = [ "std", "NSArray", "NSString", "NSEnumerator", "NSGeometry", "NSValue", ] default-features = false [target."cfg(windows)".dependencies.clipboard-win] version = "5.3.1" features = ["std"] [target."cfg(windows)".dependencies.image] version = "0.25" features = ["png"] optional = true default-features = false [target."cfg(windows)".dependencies.log] version = "0.4" [target."cfg(windows)".dependencies.windows-sys] version = ">=0.52.0, <0.60.0" features = [ "Win32_Foundation", "Win32_Graphics_Gdi", "Win32_System_DataExchange", "Win32_System_Memory", "Win32_System_Ole", ] optional = true arboard-3.5.0/Cargo.toml.orig000064400000000000000000000050561046102023000141400ustar 00000000000000[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" readme = "README.md" keywords = ["clipboard", "image"] edition = "2021" rust-version = "1.71.0" [features] default = ["image-data"] 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] env_logger = "0.10.2" [target.'cfg(windows)'.dependencies] windows-sys = { version = ">=0.52.0, <0.60.0", optional = true, features = [ "Win32_Foundation", "Win32_Graphics_Gdi", "Win32_System_DataExchange", "Win32_System_Memory", "Win32_System_Ole", ] } clipboard-win = { version = "5.3.1", features = ["std"] } log = "0.4" image = { version = "0.25", optional = true, default-features = false, features = [ "png", ] } [target.'cfg(target_os = "macos")'.dependencies] objc2 = "0.6.0" objc2-foundation = { version = "0.3.0", default-features = false, features = [ "std", "NSArray", "NSString", "NSEnumerator", "NSGeometry", "NSValue", ] } 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.9.0", optional = true } image = { version = "0.25", optional = true, default-features = false, features = [ "png", ] } parking_lot = "0.12" percent-encoding = "2.3.1" [[example]] name = "get_image" required-features = ["image-data"] [[example]] name = "set_image" required-features = ["image-data"] arboard-3.5.0/LICENSE-APACHE.txt000064400000000000000000000236761046102023000140230ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS arboard-3.5.0/LICENSE-MIT.txt000064400000000000000000000020711046102023000135150ustar 00000000000000MIT License Copyright (c) 2022 The Arboard contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. arboard-3.5.0/README.md000064400000000000000000000043731046102023000125310ustar 00000000000000# Arboard (Arthur's Clipboard) [![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.71.0+-blue.svg) ## General 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. ## 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. 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). ## Example ```rust use arboard::Clipboard; fn main() { let mut clipboard = Clipboard::new().unwrap(); println!("Clipboard text was: {}", clipboard.get_text().unwrap()); let the_string = "Hello, world!"; clipboard.set_text(the_string).unwrap(); println!("But now the clipboard text should be: \"{}\"", the_string); } ``` ## Yet another clipboard crate 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. arboard-3.5.0/examples/daemonize.rs000064400000000000000000000020471046102023000154050ustar 00000000000000//! Example showcasing the use of `set_text_wait` and spawning a daemon to allow the clipboard's //! contents to live longer than the process on Linux. use arboard::Clipboard; #[cfg(target_os = "linux")] use arboard::SetExtLinux; use std::{env, error::Error, process}; // An argument that can be passed into the program to signal that it should daemonize itself. This // can be anything as long as it is unlikely to be passed in by the user by mistake. const DAEMONIZE_ARG: &str = "__internal_daemonize"; fn main() -> Result<(), Box> { #[cfg(target_os = "linux")] if env::args().nth(1).as_deref() == Some(DAEMONIZE_ARG) { Clipboard::new()?.set().wait().text("Hello, world!")?; return Ok(()); } env_logger::init(); if cfg!(target_os = "linux") { process::Command::new(env::current_exe()?) .arg(DAEMONIZE_ARG) .stdin(process::Stdio::null()) .stdout(process::Stdio::null()) .stderr(process::Stdio::null()) .current_dir("/") .spawn()?; } else { Clipboard::new()?.set_text("Hello, world!")?; } Ok(()) } arboard-3.5.0/examples/get_image.rs000064400000000000000000000002461046102023000153520ustar 00000000000000use arboard::Clipboard; fn main() { let mut ctx = Clipboard::new().unwrap(); let img = ctx.get_image().unwrap(); println!("Image data is:\n{:?}", img.bytes); } arboard-3.5.0/examples/hello_world.rs000064400000000000000000000004741046102023000157460ustar 00000000000000use arboard::Clipboard; fn main() { env_logger::init(); let mut clipboard = Clipboard::new().unwrap(); println!("Clipboard text was: {:?}", clipboard.get_text()); let the_string = "Hello, world!"; clipboard.set_text(the_string).unwrap(); println!("But now the clipboard text should be: \"{the_string}\""); } arboard-3.5.0/examples/set_get_html.rs000064400000000000000000000010531046102023000161040ustar 00000000000000use arboard::Clipboard; use std::{thread, time::Duration}; fn main() { env_logger::init(); let mut ctx = Clipboard::new().unwrap(); let html = r#"

Hello, World!

Lorem ipsum dolor sit amet,
consectetur adipiscing elit."#; let alt_text = r#"Hello, World! Lorem ipsum dolor sit amet, 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}"); } arboard-3.5.0/examples/set_image.rs000064400000000000000000000005121046102023000153620ustar 00000000000000use arboard::{Clipboard, ImageData}; fn main() { let mut ctx = Clipboard::new().unwrap(); #[rustfmt::skip] let bytes = [ 255, 100, 100, 255, 100, 255, 100, 100, 100, 100, 255, 100, 0, 0, 0, 255, ]; let img_data = ImageData { width: 2, height: 2, bytes: bytes.as_ref().into() }; ctx.set_image(img_data).unwrap(); } arboard-3.5.0/rustfmt.toml000064400000000000000000000001371046102023000136450ustar 00000000000000hard_tabs=true use_field_init_shorthand=true use_small_heuristics="Max" use_try_shorthand=true arboard-3.5.0/src/common.rs000064400000000000000000000137301046102023000136740ustar 00000000000000/* SPDX-License-Identifier: Apache-2.0 OR MIT Copyright 2022 The Arboard contributors 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. */ #[cfg(feature = "image-data")] use std::borrow::Cow; /// 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. #[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) ContentNotAvailable, /// The selected clipboard is not supported by the current configuration (system and/or environment). /// /// 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 ClipboardNotSupported, /// The native clipboard is not accessible due to being held by an other 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 /// to interact with the clipboard from multiple threads at once. /// /// 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. ClipboardOccupied, /// 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. 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::*; macro_rules! kind_to_str { ($( $e: pat ),*) => { match self { $( $e => stringify!($e), )* } } } let name = kind_to_str!( ContentNotAvailable, ClipboardNotSupported, ClipboardOccupied, ConversionFailure, Unknown { .. } ); f.write_fmt(format_args!("{name} - \"{self}\"")) } } impl Error { 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. /// This struct stores four channels (red, green, blue, alpha) so /// a `3*3` image is going to be stored on `3*3*4 = 36` bytes of data. /// /// The pixels are in row-major order meaning that the second pixel /// in `bytes` (starting at the fifth byte) corresponds to the pixel that's /// sitting to the right side of the top-left pixel (x=1, y=0) /// /// Assigning a `2*1` image would for example look like this /// ``` /// use arboard::ImageData; /// use std::borrow::Cow; /// let bytes = [ /// // A red pixel /// 255, 0, 0, 255, /// /// // A green pixel /// 0, 255, 0, 255, /// ]; /// let img = ImageData { /// width: 2, /// height: 1, /// bytes: Cow::from(bytes.as_ref()) /// }; /// ``` #[cfg(feature = "image-data")] #[derive(Debug, Clone)] pub struct ImageData<'a> { pub width: usize, pub height: usize, pub bytes: Cow<'a, [u8]>, } #[cfg(feature = "image-data")] 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]> { self.bytes.into_owned().into() } /// Returns an image data that is guaranteed to own its bytes. /// It moves the bytes if they are already owned and clones them if they are borrowed. pub fn to_owned_img(&self) -> ImageData<'static> { ImageData { width: self.width, height: self.height, bytes: self.bytes.clone().into_owned().into(), } } } #[cfg(any(windows, all(unix, not(target_os = "macos"))))] pub(crate) struct ScopeGuard { callback: Option, } #[cfg(any(windows, all(unix, not(target_os = "macos"))))] impl ScopeGuard { #[cfg_attr(all(windows, not(feature = "image-data")), allow(dead_code))] pub(crate) fn new(callback: F) -> Self { ScopeGuard { callback: Some(callback) } } } #[cfg(any(windows, all(unix, not(target_os = "macos"))))] impl Drop for ScopeGuard { fn drop(&mut self) { if let Some(callback) = self.callback.take() { (callback)(); } } } /// Common trait for sealing platform extension traits. pub(crate) mod private { pub trait Sealed {} impl Sealed for crate::Get<'_> {} impl Sealed for crate::Set<'_> {} impl Sealed for crate::Clear<'_> {} } arboard-3.5.0/src/lib.rs000064400000000000000000000362121046102023000131520ustar 00000000000000/* SPDX-License-Identifier: Apache-2.0 OR MIT Copyright 2022 The Arboard contributors 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, path::PathBuf}; pub use common::Error; #[cfg(feature = "image-data")] pub use common::ImageData; mod platform; #[cfg(all( unix, not(any(target_os = "macos", target_os = "android", target_os = "emscripten")), ))] 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 /// that all `Clipboard`s must be 'dropped' before the program exits. In most scenarios this happens /// automatically but there are frameworks (for example, `winit`) that take over the execution /// and where the objects don't get dropped when the application exits. In these cases you have to /// make sure the object is dropped by taking ownership of it in a confined scope when detecting /// that your application is about to quit. /// /// It is also valid to have these multiple `Clipboards` on separate threads at once but note that /// executing multiple clipboard operations in parallel might fail with a `ClipboardOccupied` error. /// /// # Platform-specific behavior /// /// `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 /// or keeping one open in some application/global state). /// /// ## Linux /// /// Using either Wayland and X11, the clipboard and its content is "hosted" inside of the application /// that last put data onto it. This means that when the last `Clipboard` instance is dropped, the contents /// may become unavailable to other apps. See [SetExtLinux] for more details. /// /// ## 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 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. /// /// # 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) } /// 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, alt_text: Option, ) -> Result<(), Error> { self.set().html(html, alt_text) } /// Fetches image data from the clipboard, and returns the decoded pixels. /// /// 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() } /// Places an image to the clipboard. /// /// The chosen output format, depending on the platform is the following: /// /// - 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) } /// 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() } /// Begins a "clear" option to remove data from the clipboard. pub fn clear_with(&mut self) -> Clear<'_> { Clear { platform: platform::Clear::new(&mut self.platform) } } /// Begins a "get" operation to retrieve data from the clipboard. pub fn get(&mut self) -> Get<'_> { Get { platform: platform::Get::new(&mut self.platform) } } /// Begins a "set" operation to set the clipboard's contents. pub fn set(&mut self) -> Set<'_> { Set { platform: platform::Set::new(&mut self.platform) } } } /// A builder for an operation that gets a value from the clipboard. #[must_use] pub struct Get<'clipboard> { pub(crate) platform: platform::Get<'clipboard>, } impl Get<'_> { /// Completes the "get" operation by fetching UTF-8 text from the clipboard. pub fn text(self) -> Result { self.platform.text() } /// Completes the "get" operation by fetching image data from the clipboard and returning the /// decoded pixels. /// /// 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. #[cfg(feature = "image-data")] 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() } /// 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. #[must_use] pub struct Set<'clipboard> { pub(crate) platform: platform::Set<'clipboard>, } impl Set<'_> { /// Completes the "set" operation by placing text onto the clipboard. Any valid UTF-8 string /// is accepted. pub fn text<'a, T: Into>>(self, text: T) -> Result<(), Error> { let text = text.into(); self.platform.text(text) } /// Completes the "set" operation by placing HTML as well as a plain-text alternative onto the /// clipboard. /// /// Any valid UTF-8 string is accepted. pub fn html<'a, T: Into>>( self, html: T, alt_text: Option, ) -> Result<(), Error> { let html = html.into(); let alt_text = alt_text.map(|e| e.into()); self.platform.html(html, alt_text) } /// Completes the "set" operation by placing an image onto the clipboard. /// /// The chosen output format, depending on the platform is the following: /// /// - On macOS: `NSImage` object /// - On Linux: PNG, under the atom `image/png` /// - On Windows: In order of priority `CF_DIB` and `CF_BITMAP` #[cfg(feature = "image-data")] pub fn image(self, image: ImageData) -> Result<(), Error> { self.platform.image(image) } } /// A builder for an operation that clears the data from the clipboard. #[must_use] pub struct Clear<'clipboard> { pub(crate) platform: platform::Clear<'clipboard>, } impl Clear<'_> { /// Completes the "clear" operation by deleting any existing clipboard data, /// regardless of the format. pub fn default(self) -> Result<(), Error> { self.platform.clear() } } /// All tests grouped in one because the windows clipboard cannot be open on /// multiple threads at once. #[cfg(test)] mod tests { use super::*; use std::{sync::Arc, thread, time::Duration}; #[test] fn all_tests() { let _ = env_logger::builder().is_test(true).try_init(); { let mut ctx = Clipboard::new().unwrap(); let text = "some string"; ctx.set_text(text).unwrap(); assert_eq!(ctx.get_text().unwrap(), text); // We also need to check that the content persists after the drop; this is // especially important on X11 drop(ctx); // Give any external mechanism a generous amount of time to take over // responsibility for the clipboard, in case that happens asynchronously // (it appears that this is the case on X11 plus Mutter 3.34+, see #4) thread::sleep(Duration::from_millis(300)); let mut ctx = Clipboard::new().unwrap(); assert_eq!(ctx.get_text().unwrap(), text); } { let mut ctx = Clipboard::new().unwrap(); let text = "Some utf8: 🤓 ∑φ(n)<ε 🐔"; ctx.set_text(text).unwrap(); assert_eq!(ctx.get_text().unwrap(), text); } { let mut ctx = Clipboard::new().unwrap(); let text = "hello world"; ctx.set_text(text).unwrap(); assert_eq!(ctx.get_text().unwrap(), text); ctx.clear().unwrap(); match ctx.get_text() { Ok(text) => assert!(text.is_empty()), Err(Error::ContentNotAvailable) => {} Err(e) => panic!("unexpected error: {e}"), }; // confirm it is OK to clear when already empty. ctx.clear().unwrap(); } { let mut ctx = Clipboard::new().unwrap(); let html = "hello world!"; ctx.set_html(html, None).unwrap(); match ctx.get_text() { Ok(text) => assert!(text.is_empty()), Err(Error::ContentNotAvailable) => {} Err(e) => panic!("unexpected error: {e}"), }; } { let mut ctx = Clipboard::new().unwrap(); let html = "hello world!"; let alt_text = "hello world!"; 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(); #[rustfmt::skip] let bytes = [ 255, 100, 100, 255, 100, 255, 100, 100, 100, 100, 255, 100, 0, 0, 0, 255, ]; let img_data = ImageData { width: 2, height: 2, bytes: bytes.as_ref().into() }; // Make sure that setting one format overwrites the other. ctx.set_image(img_data.clone()).unwrap(); assert!(matches!(ctx.get_text(), Err(Error::ContentNotAvailable))); 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 clipboard ctx.set_image(img_data.clone()).unwrap(); let got = ctx.get_image().unwrap(); assert_eq!(img_data.bytes, got.bytes); #[rustfmt::skip] let big_bytes = vec![ 255, 100, 100, 255, 100, 255, 100, 100, 100, 100, 255, 100, 0, 1, 2, 255, 0, 1, 2, 255, 0, 1, 2, 255, ]; let bytes_cloned = big_bytes.clone(); let big_img_data = ImageData { width: 3, height: 2, bytes: big_bytes.into() }; ctx.set_image(big_img_data).unwrap(); let got = ctx.get_image().unwrap(); assert_eq!(bytes_cloned.as_slice(), got.bytes.as_ref()); } #[cfg(all( unix, not(any(target_os = "macos", target_os = "android", target_os = "emscripten")), ))] { use crate::{LinuxClipboardKind, SetExtLinux}; use std::sync::atomic::{self, AtomicBool}; let mut ctx = Clipboard::new().unwrap(); const TEXT1: &str = "I'm a little teapot,"; const TEXT2: &str = "short and stout,"; const TEXT3: &str = "here is my handle"; ctx.set().clipboard(LinuxClipboardKind::Clipboard).text(TEXT1.to_string()).unwrap(); ctx.set().clipboard(LinuxClipboardKind::Primary).text(TEXT2.to_string()).unwrap(); // The secondary clipboard is not available under wayland if !cfg!(feature = "wayland-data-control") || std::env::var_os("WAYLAND_DISPLAY").is_none() { ctx.set().clipboard(LinuxClipboardKind::Secondary).text(TEXT3.to_string()).unwrap(); } assert_eq!(TEXT1, &ctx.get().clipboard(LinuxClipboardKind::Clipboard).text().unwrap()); assert_eq!(TEXT2, &ctx.get().clipboard(LinuxClipboardKind::Primary).text().unwrap()); // The secondary clipboard is not available under wayland if !cfg!(feature = "wayland-data-control") || std::env::var_os("WAYLAND_DISPLAY").is_none() { assert_eq!( TEXT3, &ctx.get().clipboard(LinuxClipboardKind::Secondary).text().unwrap() ); } let was_replaced = Arc::new(AtomicBool::new(false)); let setter = thread::spawn({ let was_replaced = was_replaced.clone(); move || { thread::sleep(Duration::from_millis(100)); let mut ctx = Clipboard::new().unwrap(); ctx.set_text("replacement text".to_owned()).unwrap(); was_replaced.store(true, atomic::Ordering::Release); } }); ctx.set().wait().text("initial text".to_owned()).unwrap(); assert!(was_replaced.load(atomic::Ordering::Acquire)); setter.join().unwrap(); } } // The cross-platform abstraction should allow any number of clipboards // to be open at once without issue, as documented under [Clipboard]. #[test] fn multiple_clipboards_at_once() { const THREAD_COUNT: usize = 100; let mut handles = Vec::with_capacity(THREAD_COUNT); let barrier = Arc::new(std::sync::Barrier::new(THREAD_COUNT)); for _ in 0..THREAD_COUNT { let barrier = barrier.clone(); handles.push(thread::spawn(move || { // As long as the clipboard isn't used multiple times at once, multiple instances // are perfectly fine. let _ctx = Clipboard::new().unwrap(); thread::sleep(Duration::from_millis(10)); barrier.wait(); })); } for thread_handle in handles { thread_handle.join().unwrap(); } } #[test] fn clipboard_trait_consistently() { fn assert_send_sync() {} assert_send_sync::(); assert!(std::mem::needs_drop::()); } } arboard-3.5.0/src/platform/linux/mod.rs000064400000000000000000000277731046102023000161620ustar 00000000000000use 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; use crate::{common::private, Error}; mod x11; #[cfg(feature = "wayland-data-control")] mod wayland; fn into_unknown(error: E) -> Error { Error::Unknown { description: error.to_string() } } #[cfg(feature = "image-data")] fn encode_as_png(image: &ImageData) -> Result, Error> { use image::ImageEncoder as _; if image.bytes.is_empty() || image.width == 0 || image.height == 0 { return Err(Error::ConversionFailure); } let mut png_bytes = Vec::new(); let encoder = image::codecs::png::PngEncoder::new(&mut png_bytes); encoder .write_image( image.bytes.as_ref(), image.width as u32, image.height as u32, image::ExtendedColorType::Rgba8, ) .map_err(|_| Error::ConversionFailure)?; 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 /// enum provides a way to get/set to a specific clipboard (the default /// [`Clipboard`](Self::Clipboard) being used for the common platform API). You can choose which /// clipboard to use with [`GetExtLinux::clipboard`] and [`SetExtLinux::clipboard`]. /// /// See for a better /// description of the different clipboards. #[derive(Copy, Clone, Debug)] pub enum LinuxClipboardKind { /// Typically used selection for explicit cut/copy/paste actions (ie. windows/macos like /// clipboard behavior) Clipboard, /// Typically used for mouse selections and/or currently selected text. Accessible via middle /// mouse click. /// /// *On Wayland, this may not be available for all systems (requires a compositor supporting /// version 2 or above) and operations using this will return an error if unsupported.* Primary, /// The secondary clipboard is rarely used but theoretically available on X11. /// /// *On Wayland, this is not be available and operations using this variant will return an /// error.* Secondary, } pub(crate) enum Clipboard { X11(x11::Clipboard), #[cfg(feature = "wayland-data-control")] WlDataControl(wayland::Clipboard), } impl Clipboard { pub(crate) fn new() -> Result { #[cfg(feature = "wayland-data-control")] { if std::env::var_os("WAYLAND_DISPLAY").is_some() { // Wayland is available match wayland::Clipboard::new() { Ok(clipboard) => { trace!("Successfully initialized the Wayland data control clipboard."); return Ok(Self::WlDataControl(clipboard)); } Err(e) => warn!( "Tried to initialize the wayland data control protocol clipboard, but failed. Falling back to the X11 clipboard protocol. The error was: {}", e ), } } } Ok(Self::X11(x11::Clipboard::new()?)) } } pub(crate) struct Get<'clipboard> { clipboard: &'clipboard mut Clipboard, selection: LinuxClipboardKind, } impl<'clipboard> Get<'clipboard> { pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self { Self { clipboard, selection: LinuxClipboardKind::Clipboard } } pub(crate) fn text(self) -> Result { match self.clipboard { Clipboard::X11(clipboard) => clipboard.get_text(self.selection), #[cfg(feature = "wayland-data-control")] Clipboard::WlDataControl(clipboard) => clipboard.get_text(self.selection), } } #[cfg(feature = "image-data")] pub(crate) fn image(self) -> Result, Error> { match self.clipboard { Clipboard::X11(clipboard) => clipboard.get_image(self.selection), #[cfg(feature = "wayland-data-control")] 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), } } 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. pub trait GetExtLinux: private::Sealed { /// Sets the clipboard the operation will retrieve data from. /// /// If wayland support is enabled and available, attempting to use the Secondary clipboard will /// return an error. fn clipboard(self, selection: LinuxClipboardKind) -> Self; } impl GetExtLinux for crate::Get<'_> { fn clipboard(mut self, selection: LinuxClipboardKind) -> Self { self.platform.selection = selection; self } } /// 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: WaitConfig, selection: LinuxClipboardKind, } impl<'clipboard> Set<'clipboard> { pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self { 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, self.wait), #[cfg(feature = "wayland-data-control")] Clipboard::WlDataControl(clipboard) => clipboard.set_text(text, self.selection, self.wait), } } 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), #[cfg(feature = "wayland-data-control")] Clipboard::WlDataControl(clipboard) => clipboard.set_html(html, alt, self.selection, self.wait), } } #[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), #[cfg(feature = "wayland-data-control")] Clipboard::WlDataControl(clipboard) => clipboard.set_image(image, self.selection, self.wait), } } } /// Linux specific extensions to the [`Set`](super::Set) builder. pub trait SetExtLinux: private::Sealed { /// Whether to wait for the clipboard's contents to be replaced after setting it. /// /// The Wayland and X11 clipboards work by having the clipboard content being, at any given /// time, "owned" by a single process, and that process is expected to reply to all the requests /// from any other system process that wishes to access the clipboard's contents. As a /// consequence, when that process exits the contents of the clipboard will effectively be /// cleared since there is no longer anyone around to serve requests for it. /// /// This poses a problem for short-lived programs that just want to copy to the clipboard and /// then exit, since they don't want to wait until the user happens to copy something else just /// to finish. To resolve that, whenever the user copies something you can offload the actual /// work to a newly-spawned daemon process which will run in the background (potentially /// outliving the current process) and serve all the requests. That process will then /// automatically and silently exit once the user copies something else to their clipboard so it /// doesn't take up too many resources. /// /// To support that pattern, this method will not only have the contents of the clipboard be /// set, but will also wait and continue to serve requests until the clipboard is overwritten. /// As long as you don't exit the current process until that method has returned, you can avoid /// all surprising situations where the clipboard's contents seemingly disappear from under your /// feet. /// /// See the [daemonize example] for a demo of how you could implement this. /// /// [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 /// return an error. /// /// # Examples /// /// ``` /// use arboard::{Clipboard, SetExtLinux, LinuxClipboardKind}; /// # fn main() -> Result<(), arboard::Error> { /// let mut ctx = Clipboard::new()?; /// /// let clipboard = "This goes in the traditional (ex. Copy & Paste) clipboard."; /// ctx.set().clipboard(LinuxClipboardKind::Clipboard).text(clipboard.to_owned())?; /// /// let primary = "This goes in the primary keyboard. It's typically used via middle mouse click."; /// ctx.set().clipboard(LinuxClipboardKind::Primary).text(primary.to_owned())?; /// # Ok(()) /// # } /// ``` fn clipboard(self, selection: LinuxClipboardKind) -> Self; } impl SetExtLinux for crate::Set<'_> { fn wait(mut self) -> Self { self.platform.wait = WaitConfig::Forever; self } fn clipboard(mut self, selection: LinuxClipboardKind) -> Self { self.platform.selection = selection; self } fn wait_until(mut self, deadline: Instant) -> Self { self.platform.wait = WaitConfig::Until(deadline); self } } pub(crate) struct Clear<'clipboard> { clipboard: &'clipboard mut Clipboard, } impl<'clipboard> Clear<'clipboard> { pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self { Self { clipboard } } pub(crate) fn clear(self) -> Result<(), Error> { self.clear_inner(LinuxClipboardKind::Clipboard) } fn clear_inner(self, selection: LinuxClipboardKind) -> Result<(), Error> { let mut set = Set::new(self.clipboard); set.selection = selection; set.text(Cow::Borrowed("")) } } /// Linux specific extensions to the [Clear] builder. pub trait ClearExtLinux: private::Sealed { /// Performs the "clear" operation on the selected clipboard. /// /// ### Example /// /// ```no_run /// # use arboard::{Clipboard, LinuxClipboardKind, ClearExtLinux, Error}; /// # fn main() -> Result<(), Error> { /// let mut clipboard = Clipboard::new()?; /// /// clipboard /// .clear_with() /// .clipboard(LinuxClipboardKind::Secondary)?; /// # Ok(()) /// # } /// ``` /// /// If wayland support is enabled and available, attempting to use the Secondary clipboard will /// return an error. fn clipboard(self, selection: LinuxClipboardKind) -> Result<(), Error>; } impl ClearExtLinux for crate::Clear<'_> { fn clipboard(self, selection: LinuxClipboardKind) -> Result<(), Error> { 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); } } arboard-3.5.0/src/platform/linux/wayland.rs000064400000000000000000000132021046102023000170200ustar 00000000000000use std::{borrow::Cow, io::Read, path::PathBuf}; use wl_clipboard_rs::{ copy::{self, Error as CopyError, MimeSource, MimeType, Options, Source}, paste::{self, get_contents, Error as PasteError, Seat}, utils::is_primary_selection_supported, }; #[cfg(feature = "image-data")] use super::encode_as_png; use super::{into_unknown, paths_from_uri_list, LinuxClipboardKind, WaitConfig}; use crate::common::Error; #[cfg(feature = "image-data")] use crate::common::ImageData; #[cfg(feature = "image-data")] const MIME_PNG: &str = "image/png"; pub(crate) struct Clipboard {} impl TryInto for LinuxClipboardKind { type Error = Error; fn try_into(self) -> Result { match self { LinuxClipboardKind::Clipboard => Ok(copy::ClipboardType::Regular), LinuxClipboardKind::Primary => Ok(copy::ClipboardType::Primary), LinuxClipboardKind::Secondary => Err(Error::ClipboardNotSupported), } } } impl TryInto for LinuxClipboardKind { type Error = Error; fn try_into(self) -> Result { match self { LinuxClipboardKind::Clipboard => Ok(paste::ClipboardType::Regular), LinuxClipboardKind::Primary => Ok(paste::ClipboardType::Primary), LinuxClipboardKind::Secondary => Err(Error::ClipboardNotSupported), } } } 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)); } Ok(Self {}) } 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(PasteError::PrimarySelectionUnsupported) => Err(Error::ClipboardNotSupported), Err(err) => Err(into_unknown(err)), } } 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>, selection: LinuxClipboardKind, wait: WaitConfig, ) -> 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 { CopyError::PrimarySelectionUnsupported => Error::ClipboardNotSupported, other => into_unknown(other), })?; 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>, alt: Option>, selection: LinuxClipboardKind, wait: WaitConfig, ) -> 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), } .map_err(|e| match e { CopyError::PrimarySelectionUnsupported => Error::ClipboardNotSupported, other => into_unknown(other), })?; Ok(()) } #[cfg(feature = "image-data")] pub(crate) fn get_image( &mut self, 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)); 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(into_unknown(err)), } } #[cfg(feature = "image-data")] pub(crate) fn set_image( &mut self, image: ImageData, selection: LinuxClipboardKind, wait: WaitConfig, ) -> 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(()) } 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) } } arboard-3.5.0/src/platform/linux/x11.rs000064400000000000000000000725671046102023000160150ustar 00000000000000/* SPDX-License-Identifier: Apache-2.0 OR MIT Copyright 2022 The Arboard contributors 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. */ // More info about using the clipboard on X11: // https://tronche.com/gui/x/icccm/sec-2.html#s-2.6 // https://freedesktop.org/wiki/ClipboardManager/ use std::{ borrow::Cow, cell::RefCell, collections::{hash_map::Entry, HashMap}, path::PathBuf, sync::{ atomic::{AtomicBool, Ordering}, Arc, }, thread::JoinHandle, thread_local, time::{Duration, Instant}, }; use log::{error, trace, warn}; use parking_lot::{Condvar, Mutex, MutexGuard, RwLock}; use x11rb::{ connection::Connection, protocol::{ xproto::{ Atom, AtomEnum, ConnectionExt as _, CreateWindowAux, EventMask, PropMode, Property, PropertyNotifyEvent, SelectionNotifyEvent, SelectionRequestEvent, Time, WindowClass, SELECTION_NOTIFY_EVENT, }, Event, }, rust_connection::RustConnection, wrapper::ConnectionExt as _, COPY_DEPTH_FROM_PARENT, COPY_FROM_PARENT, NONE, }; #[cfg(feature = "image-data")] use super::encode_as_png; use super::{into_unknown, paths_from_uri_list, LinuxClipboardKind, WaitConfig}; #[cfg(feature = "image-data")] use crate::ImageData; use crate::{common::ScopeGuard, Error}; type Result = std::result::Result; static CLIPBOARD: Mutex> = parking_lot::const_mutex(None); x11rb::atom_manager! { pub Atoms: AtomCookies { CLIPBOARD, PRIMARY, SECONDARY, CLIPBOARD_MANAGER, SAVE_TARGETS, TARGETS, ATOM, INCR, UTF8_STRING, UTF8_MIME_0: b"text/plain;charset=utf-8", UTF8_MIME_1: b"text/plain;charset=UTF-8", // Text in ISO Latin-1 encoding // See: https://tronche.com/gui/x/icccm/sec-2.html#s-2.6.2 STRING, // Text in unknown encoding // See: https://tronche.com/gui/x/icccm/sec-2.html#s-2.6.2 TEXT, TEXT_MIME_UNKNOWN: b"text/plain", HTML: b"text/html", URI_LIST: b"text/uri-list", PNG_MIME: b"image/png", // This is just some random name for the property on our window, into which // the clipboard owner writes the data we requested. ARBOARD_CLIPBOARD, } } thread_local! { static ATOM_NAME_CACHE: RefCell> = Default::default(); } // Some clipboard items, like images, may take a very long time to produce a // `SelectionNotify`. Multiple seconds long. const LONG_TIMEOUT_DUR: Duration = Duration::from_millis(4000); const SHORT_TIMEOUT_DUR: Duration = Duration::from_millis(10); #[derive(Debug, PartialEq, Eq)] enum ManagerHandoverState { Idle, InProgress, Finished, } struct GlobalClipboard { inner: Arc, /// Join handle to the thread which serves selection requests. server_handle: JoinHandle<()>, } struct XContext { conn: RustConnection, win_id: u32, } struct Inner { /// The context for the thread which serves clipboard read /// requests coming to us. server: XContext, atoms: Atoms, clipboard: Selection, primary: Selection, secondary: Selection, handover_state: Mutex, handover_cv: Condvar, serve_stopped: AtomicBool, } 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("X11 server connection timed out because it was unreachable") })?; 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 = // Just in case that some program reports SelectionNotify events // with XCB_EVENT_MASK_PROPERTY_CHANGE mask. EventMask::PROPERTY_CHANGE | // To receive DestroyNotify event and stop the message loop. EventMask::STRUCTURE_NOTIFY; // create the window conn.create_window( // copy as much as possible from the parent, because no other specific input is needed COPY_DEPTH_FROM_PARENT, win_id, screen.root, 0, 0, 1, 1, 0, WindowClass::COPY_FROM_PARENT, COPY_FROM_PARENT, // don't subscribe to any special events because we are requesting everything we need ourselves &CreateWindowAux::new().event_mask(event_mask), ) .map_err(into_unknown)?; conn.flush().map_err(into_unknown)?; Ok(Self { conn, win_id }) } } #[derive(Default)] struct Selection { data: RwLock>>, /// Mutex around nothing to use with the below condvar. mutex: Mutex<()>, /// A condvar that is notified when the contents of this clipboard are changed. /// /// This is associated with `Self::mutex`. data_changed: Condvar, } #[derive(Debug, Clone)] struct ClipboardData { bytes: Vec, /// The atom representing the format in which the data is encoded. format: Atom, } enum ReadSelNotifyResult { GotData(Vec), IncrStarted, EventNotRecognized, } impl Inner { fn new() -> Result { let server = XContext::new()?; let atoms = Atoms::new(&server.conn).map_err(into_unknown)?.reply().map_err(into_unknown)?; Ok(Self { server, atoms, clipboard: Selection::default(), primary: Selection::default(), secondary: Selection::default(), handover_state: Mutex::new(ManagerHandoverState::Idle), handover_cv: Condvar::new(), serve_stopped: AtomicBool::new(false), }) } fn write( &self, data: Vec, selection: LinuxClipboardKind, wait: WaitConfig, ) -> Result<()> { if self.serve_stopped.load(Ordering::Relaxed) { 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; // 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) .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. let mut guard = selection.mutex.lock(); // 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. selection.data_changed.notify_all(); match wait { WaitConfig::None => {} WaitConfig::Forever => { drop(data_guard); selection.data_changed.wait(&mut guard); } WaitConfig::Until(deadline) => { drop(data_guard); selection.data_changed.wait_until(&mut guard, deadline); } } Ok(()) } /// `formats` must be a slice of atoms, where each atom represents a target format. /// The first format from `formats`, which the clipboard owner supports will be the /// format of the return value. fn read(&self, formats: &[Atom], selection: LinuxClipboardKind) -> Result { // if we are the current owner, we can get the current clipboard ourselves if self.is_owner(selection)? { let data = self.selection_of(selection).data.read(); if let Some(data_list) = &*data { for data in data_list { for format in formats { if *format == data.format { return Ok(data.clone()); } } } } return Err(Error::ContentNotAvailable); } // if let Some(data) = self.data.read().clone() { // return Ok(data) // } let reader = XContext::new()?; trace!("Trying to get the clipboard data."); for format in formats { match self.read_single(&reader, selection, *format) { Ok(bytes) => { return Ok(ClipboardData { bytes, format: *format }); } Err(Error::ContentNotAvailable) => { continue; } Err(e) => return Err(e), } } Err(Error::ContentNotAvailable) } fn read_single( &self, reader: &XContext, selection: LinuxClipboardKind, target_format: Atom, ) -> Result> { // Delete the property so that we can detect (using property notify) // when the selection owner receives our request. reader .conn .delete_property(reader.win_id, self.atoms.ARBOARD_CLIPBOARD) .map_err(into_unknown)?; // request to convert the clipboard selection to our data type(s) reader .conn .convert_selection( reader.win_id, self.atom_of(selection), target_format, self.atoms.ARBOARD_CLIPBOARD, Time::CURRENT_TIME, ) .map_err(into_unknown)?; reader.conn.sync().map_err(into_unknown)?; trace!("Finished `convert_selection`"); let mut incr_data: Vec = Vec::new(); let mut using_incr = false; let mut timeout_end = Instant::now() + LONG_TIMEOUT_DUR; while Instant::now() < timeout_end { let event = reader.conn.poll_for_event().map_err(into_unknown)?; let event = match event { Some(e) => e, None => { std::thread::sleep(Duration::from_millis(1)); continue; } }; match event { // The first response after requesting a selection. Event::SelectionNotify(event) => { trace!("Read SelectionNotify"); let result = self.handle_read_selection_notify( reader, target_format, &mut using_incr, &mut incr_data, event, )?; match result { ReadSelNotifyResult::GotData(data) => return Ok(data), ReadSelNotifyResult::IncrStarted => { // This means we received an indication that an the // data is going to be sent INCRementally. Let's // reset our timeout. timeout_end += SHORT_TIMEOUT_DUR; } ReadSelNotifyResult::EventNotRecognized => (), } } // If the previous SelectionNotify event specified that the data // will be sent in INCR segments, each segment is transferred in // a PropertyNotify event. Event::PropertyNotify(event) => { let result = self.handle_read_property_notify( reader, target_format, using_incr, &mut incr_data, &mut timeout_end, event, )?; if result { return Ok(incr_data); } } _ => log::trace!("An unexpected event arrived while reading the clipboard."), } } log::info!("Time-out hit while reading the clipboard."); Err(Error::ContentNotAvailable) } fn atom_of(&self, selection: LinuxClipboardKind) -> Atom { match selection { LinuxClipboardKind::Clipboard => self.atoms.CLIPBOARD, LinuxClipboardKind::Primary => self.atoms.PRIMARY, LinuxClipboardKind::Secondary => self.atoms.SECONDARY, } } fn selection_of(&self, selection: LinuxClipboardKind) -> &Selection { match selection { LinuxClipboardKind::Clipboard => &self.clipboard, LinuxClipboardKind::Primary => &self.primary, LinuxClipboardKind::Secondary => &self.secondary, } } fn kind_of(&self, atom: Atom) -> Option { match atom { a if a == self.atoms.CLIPBOARD => Some(LinuxClipboardKind::Clipboard), a if a == self.atoms.PRIMARY => Some(LinuxClipboardKind::Primary), a if a == self.atoms.SECONDARY => Some(LinuxClipboardKind::Secondary), _ => None, } } fn is_owner(&self, selection: LinuxClipboardKind) -> Result { let current = self .server .conn .get_selection_owner(self.atom_of(selection)) .map_err(into_unknown)? .reply() .map_err(into_unknown)? .owner; Ok(current == self.server.win_id) } fn atom_name(&self, atom: x11rb::protocol::xproto::Atom) -> Result { String::from_utf8( self.server .conn .get_atom_name(atom) .map_err(into_unknown)? .reply() .map_err(into_unknown)? .name, ) .map_err(into_unknown) } fn atom_name_dbg(&self, atom: x11rb::protocol::xproto::Atom) -> &'static str { ATOM_NAME_CACHE.with(|cache| { let mut cache = cache.borrow_mut(); match cache.entry(atom) { Entry::Occupied(entry) => *entry.get(), Entry::Vacant(entry) => { let s = self .atom_name(atom) .map(|s| Box::leak(s.into_boxed_str()) as &str) .unwrap_or("FAILED-TO-GET-THE-ATOM-NAME"); entry.insert(s); s } } }) } fn handle_read_selection_notify( &self, reader: &XContext, target_format: u32, using_incr: &mut bool, incr_data: &mut Vec, event: SelectionNotifyEvent, ) -> Result { // The property being set to NONE means that the `convert_selection` // failed. // According to: https://tronche.com/gui/x/icccm/sec-2.html#s-2.4 // the target must be set to the same as what we requested. if event.property == NONE || event.target != target_format { return Err(Error::ContentNotAvailable); } if self.kind_of(event.selection).is_none() { log::info!("Received a SelectionNotify for a selection other than CLIPBOARD, PRIMARY or SECONDARY. This is unexpected."); return Ok(ReadSelNotifyResult::EventNotRecognized); } if *using_incr { log::warn!("Received a SelectionNotify while already expecting INCR segments."); return Ok(ReadSelNotifyResult::EventNotRecognized); } // request the selection let mut reply = reader .conn .get_property(true, event.requestor, event.property, event.target, 0, u32::MAX / 4) .map_err(into_unknown)? .reply() .map_err(into_unknown)?; // trace!("Property.type: {:?}", self.atom_name(reply.type_)); // we found something if reply.type_ == target_format { Ok(ReadSelNotifyResult::GotData(reply.value)) } else if reply.type_ == self.atoms.INCR { // Note that we call the get_property again because we are // indicating that we are ready to receive the data by deleting the // property, however deleting only works if the type matches the // property type. But the type didn't match in the previous call. reply = reader .conn .get_property( true, event.requestor, event.property, self.atoms.INCR, 0, u32::MAX / 4, ) .map_err(into_unknown)? .reply() .map_err(into_unknown)?; log::trace!("Receiving INCR segments"); *using_incr = true; if reply.value_len == 4 { let min_data_len = reply.value32().and_then(|mut vals| vals.next()).unwrap_or(0); incr_data.reserve(min_data_len as usize); } Ok(ReadSelNotifyResult::IncrStarted) } else { // this should never happen, we have sent a request only for supported types Err(Error::unknown("incorrect type received from clipboard")) } } /// Returns Ok(true) when the incr_data is ready fn handle_read_property_notify( &self, reader: &XContext, target_format: u32, using_incr: bool, incr_data: &mut Vec, timeout_end: &mut Instant, event: PropertyNotifyEvent, ) -> Result { if event.atom != self.atoms.ARBOARD_CLIPBOARD || event.state != Property::NEW_VALUE { return Ok(false); } if !using_incr { // This must mean the selection owner received our request, and is // now preparing the data return Ok(false); } let reply = reader .conn .get_property(true, event.window, event.atom, target_format, 0, u32::MAX / 4) .map_err(into_unknown)? .reply() .map_err(into_unknown)?; // log::trace!("Received segment. value_len {}", reply.value_len,); if reply.value_len == 0 { // This indicates that all the data has been sent. return Ok(true); } incr_data.extend(reply.value); // Let's reset our timeout, since we received a valid chunk. *timeout_end = Instant::now() + SHORT_TIMEOUT_DUR; // Not yet complete Ok(false) } fn handle_selection_request(&self, event: SelectionRequestEvent) -> Result<()> { let selection = match self.kind_of(event.selection) { Some(kind) => kind, None => { warn!("Received a selection request to a selection other than the CLIPBOARD, PRIMARY or SECONDARY. This is unexpected."); return Ok(()); } }; let success; // 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 { for data in data_list { targets.push(data.format); if data.format == self.atoms.UTF8_STRING { // When we are storing a UTF8 string, // add all equivalent formats to the supported targets targets.push(self.atoms.UTF8_MIME_0); targets.push(self.atoms.UTF8_MIME_1); } } } self.server .conn .change_property32( PropMode::REPLACE, event.requestor, event.property, // TODO: change to `AtomEnum::ATOM` self.atoms.ATOM, &targets, ) .map_err(into_unknown)?; self.server.conn.flush().map_err(into_unknown)?; success = true; } else { trace!("Handling request for (probably) the clipboard contents."); let data = self.selection_of(selection).data.read(); if let Some(data_list) = &*data { success = match data_list.iter().find(|d| d.format == event.target) { Some(data) => { self.server .conn .change_property8( PropMode::REPLACE, event.requestor, event.property, event.target, &data.bytes, ) .map_err(into_unknown)?; self.server.conn.flush().map_err(into_unknown)?; true } None => false, }; } else { // This must mean that we lost ownership of the data // since the other side requested the selection. // Let's respond with the property set to none. success = false; } } // on failure we notify the requester of it let property = if success { event.property } else { AtomEnum::NONE.into() }; // tell the requestor that we finished sending data self.server .conn .send_event( false, event.requestor, EventMask::NO_EVENT, SelectionNotifyEvent { response_type: SELECTION_NOTIFY_EVENT, sequence: event.sequence, time: event.time, requestor: event.requestor, selection: event.selection, target: event.target, property, }, ) .map_err(into_unknown)?; self.server.conn.flush().map_err(into_unknown) } fn ask_clipboard_manager_to_request_our_data(&self) -> Result<()> { if self.server.win_id == 0 { // This shouldn't really ever happen but let's just check. error!("The server's window id was 0. This is unexpected"); return Ok(()); } if !self.is_owner(LinuxClipboardKind::Clipboard)? { // 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(()); } // It's important that we lock the state before sending the request // because we don't want the request server thread to lock the state // after the request but before we can lock it here. let mut handover_state = self.handover_state.lock(); trace!("Sending the data to the clipboard manager"); self.server .conn .convert_selection( self.server.win_id, self.atoms.CLIPBOARD_MANAGER, self.atoms.SAVE_TARGETS, self.atoms.ARBOARD_CLIPBOARD, Time::CURRENT_TIME, ) .map_err(into_unknown)?; self.server.conn.flush().map_err(into_unknown)?; *handover_state = ManagerHandoverState::InProgress; let max_handover_duration = Duration::from_millis(100); // Note that we are using a parking_lot condvar here, which doesn't wake up // spuriously let result = self.handover_cv.wait_for(&mut handover_state, max_handover_duration); if *handover_state == ManagerHandoverState::Finished { return Ok(()); } if result.timed_out() { warn!("Could not hand the clipboard contents over to the clipboard manager. The request timed out."); 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.")) } } fn serve_requests(context: Arc) -> Result<(), Box> { fn handover_finished(clip: &Arc, mut handover_state: MutexGuard) { log::trace!("Finishing clipboard manager handover."); *handover_state = ManagerHandoverState::Finished; // Not sure if unlocking the mutex is necessary here but better safe than sorry. drop(handover_state); clip.handover_cv.notify_all(); } trace!("Started serve requests thread."); let _guard = ScopeGuard::new(|| { context.serve_stopped.store(true, Ordering::Relaxed); }); let mut written = false; let mut notified = false; loop { match context.server.conn.wait_for_event().map_err(into_unknown)? { Event::DestroyNotify(_) => { // This window is being destroyed. trace!("Clipboard server window is being destroyed x_x"); return Ok(()); } Event::SelectionClear(event) => { // TODO: check if this works // Someone else has new content in the clipboard, so it is // notifying us that we should delete our data now. trace!("Somebody else owns the clipboard now"); if let Some(selection) = context.kind_of(event.selection) { let selection = context.selection_of(selection); let mut data_guard = selection.data.write(); *data_guard = None; // It is important that this mutex is locked at the time of calling // `notify_all` to prevent notifications getting lost in case the sleeping // thread has unlocked its `data_guard` and is just about to sleep. // It is also important that the RwLock is kept write-locked for the same // reason. let _guard = selection.mutex.lock(); selection.data_changed.notify_all(); } } Event::SelectionRequest(event) => { trace!( "SelectionRequest - selection is: {}, target is {}", context.atom_name_dbg(event.selection), context.atom_name_dbg(event.target), ); // Someone is requesting the clipboard content from us. context.handle_selection_request(event).map_err(into_unknown)?; // if we are in the progress of saving to the clipboard manager // make sure we save that we have finished writing let handover_state = context.handover_state.lock(); if *handover_state == ManagerHandoverState::InProgress { // Only set written, when the actual contents were written, // not just a response to what TARGETS we have. if event.target != context.atoms.TARGETS { trace!("The contents were written to the clipboard manager."); written = true; // if we have written and notified, make sure to notify that we are done if notified { handover_finished(&context, handover_state); } } } } Event::SelectionNotify(event) => { // We've requested the clipboard content and this is the answer. // Considering that this thread is not responsible for reading // clipboard contents, this must come from the clipboard manager // signaling that the data was handed over successfully. if event.selection != context.atoms.CLIPBOARD_MANAGER { error!("Received a `SelectionNotify` from a selection other than the CLIPBOARD_MANAGER. This is unexpected in this thread."); continue; } let handover_state = context.handover_state.lock(); if *handover_state == ManagerHandoverState::InProgress { // Note that some clipboard managers send a selection notify // before even sending a request for the actual contents. // (That's why we use the "notified" & "written" flags) trace!("The clipboard manager indicated that it's done requesting the contents from us."); notified = true; // One would think that we could also finish if the property // here is set 0, because that indicates failure. However // this is not the case; for example on KDE plasma 5.18, we // immediately get a SelectionNotify with property set to 0, // but following that, we also get a valid SelectionRequest // from the clipboard manager. if written { handover_finished(&context, handover_state); } } } _event => { // May be useful for debugging but nothing else really. // trace!("Received unwanted event: {:?}", event); } } } } pub(crate) struct Clipboard { inner: Arc, } impl Clipboard { pub(crate) fn new() -> Result { let mut global_cb = CLIPBOARD.lock(); if let Some(global_cb) = &*global_cb { return Ok(Self { inner: Arc::clone(&global_cb.inner) }); } // At this point we know that the clipboard does not exist. let ctx = Arc::new(Inner::new()?); let join_handle; { let ctx = Arc::clone(&ctx); join_handle = std::thread::spawn(move || { if let Err(error) = serve_requests(ctx) { error!("Worker thread errored with: {}", error); } }); } *global_cb = Some(GlobalClipboard { inner: Arc::clone(&ctx), server_handle: join_handle }); Ok(Self { inner: ctx }) } pub(crate) fn get_text(&self, selection: LinuxClipboardKind) -> Result { let formats = [ self.inner.atoms.UTF8_STRING, self.inner.atoms.UTF8_MIME_0, self.inner.atoms.UTF8_MIME_1, self.inner.atoms.STRING, self.inner.atoms.TEXT, self.inner.atoms.TEXT_MIME_UNKNOWN, ]; let result = self.inner.read(&formats, selection)?; if result.format == self.inner.atoms.STRING { // ISO Latin-1 // See: https://stackoverflow.com/questions/28169745/what-are-the-options-to-convert-iso-8859-1-latin-1-to-a-string-utf-8 Ok(result.bytes.into_iter().map(|c| c as char).collect()) } else { String::from_utf8(result.bytes).map_err(|_| Error::ConversionFailure) } } pub(crate) fn set_text( &self, message: Cow<'_, str>, selection: LinuxClipboardKind, 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) } 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>, alt: Option>, selection: LinuxClipboardKind, wait: WaitConfig, ) -> Result<()> { let mut data = vec![]; if let Some(alt_text) = alt { data.push(ClipboardData { bytes: alt_text.into_owned().into_bytes(), format: self.inner.atoms.UTF8_STRING, }); } data.push(ClipboardData { bytes: html.into_owned().into_bytes(), format: self.inner.atoms.HTML, }); self.inner.write(data, selection, wait) } #[cfg(feature = "image-data")] pub(crate) fn get_image(&self, selection: LinuxClipboardKind) -> Result> { let formats = [self.inner.atoms.PNG_MIME]; let bytes = self.inner.read(&formats, selection)?.bytes; let cursor = std::io::Cursor::new(&bytes); let mut reader = image::io::Reader::new(cursor); reader.set_format(image::ImageFormat::Png); let image = match reader.decode() { Ok(img) => img.into_rgba8(), Err(_e) => return Err(Error::ConversionFailure), }; let (w, h) = image.dimensions(); let image_data = ImageData { width: w as usize, height: h as usize, bytes: image.into_raw().into() }; Ok(image_data) } #[cfg(feature = "image-data")] pub(crate) fn set_image( &self, image: ImageData, selection: LinuxClipboardKind, 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) } 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 { fn drop(&mut self) { // There are always at least 3 owners: // the global, the server thread, and one `Clipboard::inner` const MIN_OWNERS: usize = 3; // We start with locking the global guard to prevent race // conditions below. let mut global_cb = CLIPBOARD.lock(); if Arc::strong_count(&self.inner) == MIN_OWNERS { // If the are the only owners of the clipboard are ourselves and // the global object, then we should destroy the global object, // and send the data to the clipboard manager if let Err(e) = self.inner.ask_clipboard_manager_to_request_our_data() { error!("Could not hand the clipboard data over to the clipboard manager: {}", e); } let global_cb = global_cb.take(); if let Err(e) = self.inner.server.conn.destroy_window(self.inner.server.win_id) { error!("Failed to destroy the clipboard window. Error: {}", e); return; } if let Err(e) = self.inner.server.conn.flush() { error!("Failed to flush the clipboard window. Error: {}", e); return; } if let Some(global_cb) = global_cb { if let Err(e) = global_cb.server_handle.join() { // Let's try extracting the error message let message; if let Some(msg) = e.downcast_ref::<&'static str>() { message = Some((*msg).to_string()); } else if let Some(msg) = e.downcast_ref::() { message = Some(msg.clone()); } else { message = None; } if let Some(message) = message { error!( "The clipboard server thread panicked. Panic message: '{}'", message, ); } else { error!("The clipboard server thread panicked."); } } } } } } arboard-3.5.0/src/platform/mod.rs000064400000000000000000000005711046102023000150060ustar 00000000000000#[cfg(all(unix, not(any(target_os = "macos", target_os = "android", target_os = "emscripten"))))] mod linux; #[cfg(all( unix, not(any(target_os = "macos", target_os = "android", target_os = "emscripten")) ))] pub use linux::*; #[cfg(windows)] mod windows; #[cfg(windows)] pub use windows::*; #[cfg(target_os = "macos")] mod osx; #[cfg(target_os = "macos")] pub use osx::*; arboard-3.5.0/src/platform/osx.rs000064400000000000000000000310221046102023000150330ustar 00000000000000/* SPDX-License-Identifier: Apache-2.0 OR MIT Copyright 2022 The Arboard contributors 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. */ #[cfg(feature = "image-data")] use crate::common::ImageData; use crate::common::{private, Error}; use objc2::{ msg_send, rc::{autoreleasepool, Retained}, runtime::ProtocolObject, ClassType, }; 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. #[cfg(feature = "image-data")] fn image_from_pixels( pixels: Vec, width: usize, height: usize, ) -> Retained { 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, ptr::{self, NonNull}, }; 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()) }) } 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 = unsafe { CGColorSpaceCreateDeviceRGB() }.unwrap(); 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 }; unsafe { NSImage::initWithCGImage_size(NSImage::alloc(), &cg_image, size) } } pub(crate) struct Clipboard { pasteboard: Retained, } 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 { // 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![NSPasteboard::class(), generalPasteboard] }; if let Some(pasteboard) = pasteboard { Ok(Clipboard { pasteboard }) } else { Err(Error::ClipboardNotSupported) } } fn clear(&mut self) { 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("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")) }; // unsafe { transmute(cls) } // }; // let image_class: Id = { // let cls: Id = unsafe { Id::from_ptr(class("NSImage")) }; // unsafe { transmute(cls) } // }; // let url_class: Id = { // let cls: Id = unsafe { Id::from_ptr(class("NSURL")) }; // unsafe { transmute(cls) } // }; // let classes = vec![url_class, image_class, string_class]; // let classes: Id> = NSArray::from_vec(classes); // let options: Id> = NSDictionary::new(); // let contents: Id> = unsafe { // let obj: *mut NSArray = // msg_send![self.pasteboard, readObjectsForClasses:&*classes options:&*options]; // if obj.is_null() { // return Err(err("pasteboard#readObjectsForClasses:options: returned null")); // } // Id::from_ptr(obj) // }; // if contents.count() == 0 { // Ok(None) // } else { // let obj = &contents[0]; // if obj.is_kind_of(Class::get("NSString").unwrap()) { // let s: &NSString = unsafe { transmute(obj) }; // Ok(Some(ClipboardContent::Utf8(s.as_str().to_owned()))) // } else if obj.is_kind_of(Class::get("NSImage").unwrap()) { // let tiff: &NSArray = unsafe { msg_send![obj, TIFFRepresentation] }; // let len: usize = unsafe { msg_send![tiff, length] }; // let bytes: *const u8 = unsafe { msg_send![tiff, bytes] }; // let vec = unsafe { std::slice::from_raw_parts(bytes, len) }; // // Here we copy the entire &[u8] into a new owned `Vec` // // Is there another way that doesn't copy multiple megabytes? // Ok(Some(ClipboardContent::Tiff(vec.into()))) // } else if obj.is_kind_of(Class::get("NSURL").unwrap()) { // let s: &NSString = unsafe { msg_send![obj, absoluteString] }; // Ok(Some(ClipboardContent::Utf8(s.as_str().to_owned()))) // } else { // // let cls: &Class = unsafe { msg_send![obj, class] }; // // println!("{}", cls.name()); // Err(err("pasteboard#readObjectsForClasses:options: returned unknown class")) // } // } // } } pub(crate) struct Get<'clipboard> { clipboard: &'clipboard Clipboard, } impl<'clipboard> Get<'clipboard> { pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self { Self { clipboard } } pub(crate) fn text(self) -> Result { unsafe { self.clipboard.string_from_type(NSPasteboardTypeString) } } pub(crate) fn html(self) -> Result { unsafe { self.clipboard.string_from_type(NSPasteboardTypeHTML) } } #[cfg(feature = "image-data")] pub(crate) fn image(self) -> Result, Error> { 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 image_data = unsafe { self.clipboard.pasteboard.dataForType(NSPasteboardTypeTIFF) } .ok_or(Error::ContentNotAvailable)?; // 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) })?; let rgba = image.into_rgba8(); let (width, height) = rgba.dimensions(); Ok(ImageData { width: width as usize, height: height as usize, 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> { clipboard: &'clipboard mut Clipboard, exclude_from_history: bool, } impl<'clipboard> Set<'clipboard> { pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self { Self { clipboard, exclude_from_history: false } } pub(crate) fn text(self, data: Cow<'_, str>) -> Result<(), Error> { self.clipboard.clear(); 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); if success { Ok(()) } else { Err(Error::unknown("NSPasteboard#writeObjects: returned false")) } } pub(crate) fn html(self, html: Cow<'_, str>, alt: Option>) -> Result<(), Error> { self.clipboard.clear(); // Text goes to the clipboard as UTF-8 but may be interpreted as Windows Latin 1. // This wrapping forces it to be interpreted as UTF-8. // // See: // https://bugzilla.mozilla.org/show_bug.cgi?id=466599 // https://bugs.chromium.org/p/chromium/issues/detail?id=11957 let html = format!( r#"{html}"#, ); 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 = 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 { self.clipboard.pasteboard.setString_forType(&alt_nss, NSPasteboardTypeString) }; } } add_clipboard_exclusions(self.clipboard, self.exclude_from_history); if success { Ok(()) } else { Err(Error::unknown("NSPasteboard#writeObjects: returned false")) } } #[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); self.clipboard.clear(); 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); if success { Ok(()) } else { Err(Error::unknown( "Failed to write the image to the pasteboard (`writeObjects` returned NO).", )) } } } pub(crate) struct Clear<'clipboard> { clipboard: &'clipboard mut Clipboard, } impl<'clipboard> Clear<'clipboard> { pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self { Self { clipboard } } pub(crate) fn clear(self) -> Result<(), Error> { self.clipboard.clear(); 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 } } arboard-3.5.0/src/platform/windows.rs000064400000000000000000000657721046102023000157370ustar 00000000000000/* SPDX-License-Identifier: Apache-2.0 OR MIT Copyright 2022 The Arboard contributors 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. */ #[cfg(feature = "image-data")] use crate::common::ImageData; use crate::common::{private, Error}; use std::{borrow::Cow, marker::PhantomData, path::PathBuf, thread, time::Duration}; #[cfg(feature = "image-data")] mod image_data { use super::*; use crate::common::ScopeGuard; use image::codecs::png::PngEncoder; use image::ExtendedColorType; use image::ImageEncoder; use std::{convert::TryInto, io, 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, }, }; 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, ) -> 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; 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, }; // 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(|| global_unlock_checked(hdata)); 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.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()); // 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 HANDLE) }.failure() { unsafe { DeleteObject(hdata as HGDIOBJ) }; Err(last_error("SetClipboardData failed with error")) } else { Ok(()) } } 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 HANDLE) }.failure() { unsafe { DeleteObject(hdata as HGDIOBJ) }; Err(last_error("SetClipboardData failed with error")) } else { Ok(()) } } 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 // 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::()) }; 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); 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) } } /// 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. /// /// 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); 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()) } } } /// 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() } } /// 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); 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 { 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); } } /// A shim clipboard type that can have operations performed with it, but /// does not represent an open clipboard itself. /// /// Windows only allows one thread on the entire system to have the clipboard /// open at once, so we have to open it very sparingly or risk causing the rest /// of the system to be unresponsive. Instead, the clipboard is opened for /// every operation and then closed afterwards. pub(crate) struct Clipboard(()); // The other platforms have `Drop` implementation on their // clipboard, so Windows should too for consistently. impl Drop for Clipboard { fn drop(&mut self) {} } struct OpenClipboard<'clipboard> { _inner: clipboard_win::Clipboard, // The Windows clipboard can not be sent between threads once // open. _marker: PhantomData<*const ()>, _for_shim: &'clipboard mut Clipboard, } impl Clipboard { const DEFAULT_OPEN_ATTEMPTS: usize = 5; pub(crate) fn new() -> Result { Ok(Self(())) } fn open(&mut self) -> Result { // Attempt to open the clipboard multiple times. On Windows, its common for something else to temporarily // be using it during attempts. // // For past work/evidence, see Firefox(https://searchfox.org/mozilla-central/source/widget/windows/nsClipboard.cpp#421) and // Chromium(https://source.chromium.org/chromium/chromium/src/+/main:ui/base/clipboard/clipboard_win.cc;l=86). // // Note: This does not use `Clipboard::new_attempts` because its implementation sleeps for `0ms`, which can // cause race conditions between closing/opening the clipboard in single-threaded apps. let mut attempts = Self::DEFAULT_OPEN_ATTEMPTS; let clipboard = loop { match clipboard_win::Clipboard::new() { Ok(this) => break Ok(this), Err(err) => match attempts { 0 => break Err(err), _ => attempts -= 1, }, } // The default value matches Chromium's implementation, but could be tweaked later. thread::sleep(Duration::from_millis(5)); } .map_err(|_| Error::ClipboardOccupied)?; Ok(OpenClipboard { _inner: clipboard, _marker: PhantomData, _for_shim: self }) } } // Note: In all of the builders, a clipboard opening result is stored. // This is done for a few reasons: // 1. consistently with the other platforms which can have an occupied clipboard. // It is better if the operation fails at the most similar place on all platforms. // 2. `{Get, Set, Clear}::new()` don't return a `Result`. Windows is the only case that // needs this kind of handling, so it doesn't need to affect the other APIs. // 3. Due to how the clipboard works on Windows, we need to open it for every operation // and keep it open until its finished. This approach allows RAII to still be applicable. pub(crate) struct Get<'clipboard> { clipboard: Result, Error>, } impl<'clipboard> Get<'clipboard> { pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self { Self { clipboard: clipboard.open() } } pub(crate) fn text(self) -> Result { const FORMAT: u32 = clipboard_win::formats::CF_UNICODETEXT; let _clipboard_assertion = self.clipboard?; // XXX: ToC/ToU race conditions are not possible because we are the sole owners of the clipboard currently. if !clipboard_win::is_format_avail(FORMAT) { 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) } 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; let _clipboard_assertion = self.clipboard?; if !clipboard_win::is_format_avail(FORMAT) { return Err(Error::ContentNotAvailable); } let mut data = Vec::new(); clipboard_win::raw::get_vec(FORMAT, &mut data) .map_err(|_| Error::unknown("failed to read clipboard image data"))?; 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> { 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_monitoring: false, exclude_from_cloud: false, exclude_from_history: false, } } 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("Could not place the specified text to the clipboard"))?; 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> { let open_clipboard = self.clipboard?; let alt = match alt { Some(s) => s.into(), None => String::new(), }; 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(e.to_string()))?; } add_clipboard_exclusions( open_clipboard, self.exclude_from_monitoring, self.exclude_from_cloud, self.exclude_from_history, ) } #[cfg(feature = "image-data")] pub(crate) fn image(self, image: ImageData) -> Result<(), Error> { let open_clipboard = self.clipboard?; if let Err(e) = clipboard_win::raw::empty() { return Err(Error::unknown(format!( "Failed to empty the clipboard. Got error code: {e}" ))); }; // 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(()) } } fn add_clipboard_exclusions( _open_clipboard: OpenClipboard<'_>, exclude_from_monitoring: bool, exclude_from_cloud: bool, exclude_from_history: bool, ) -> Result<(), Error> { /// `set` should be called with the registered format and a DWORD value of 0. /// /// 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 (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("Failed to exclude data from clipboard monitoring"))?; } } 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, // 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("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("Failed to exclude data from clipboard history"))?; } } Ok(()) } /// 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]. /// /// [cloud clipboard]: https://support.microsoft.com/en-us/windows/clipboard-in-windows-c436501e-985d-1c8d-97ea-fe46ddf338c6 fn exclude_from_cloud(self) -> Self; /// Excludes the data which will be set on the clipboard from being added to /// the system's [clipboard history] list. /// /// [clipboard history]: https://support.microsoft.com/en-us/windows/get-help-with-clipboard-30375039-ce71-9fe4-5b30-21b7aab6b13f fn exclude_from_history(self) -> Self; } 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 } fn exclude_from_history(mut self) -> Self { self.platform.exclude_from_history = true; self } } pub(crate) struct Clear<'clipboard> { clipboard: Result, Error>, } impl<'clipboard> Clear<'clipboard> { pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self { Self { clipboard: clipboard.open() } } pub(crate) fn clear(self) -> Result<(), Error> { let _clipboard_assertion = self.clipboard?; clipboard_win::empty().map_err(|_| Error::unknown("failed to clear clipboard")) } } fn wrap_html(ctn: &str) -> String { let h_version = "Version:0.9"; let h_start_html = "\r\nStartHTML:"; let h_end_html = "\r\nEndHTML:"; let h_start_frag = "\r\nStartFragment:"; let h_end_frag = "\r\nEndFragment:"; let c_start_frag = "\r\n\r\n\r\n\r\n"; let c_end_frag = "\r\n\r\n\r\n"; let h_len = h_version.len() + h_start_html.len() + 10 + h_end_html.len() + 10 + h_start_frag.len() + 10 + h_end_frag.len() + 10; let n_start_html = h_len + 2; let n_start_frag = h_len + c_start_frag.len(); 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, ) } arboard-3.5.0/tools/debugger.entitlements000064400000000000000000000003431046102023000166240ustar 00000000000000 com.apple.security.get-task-allow arboard-3.5.0/tools/run_with_leaks.sh000075500000000000000000000012301046102023000157540ustar 00000000000000#!/bin/bash set -euo pipefail # This script is a utility on Apple platforms to run one # of arboard's example binaries under the `leaks` CLI tool, # which can help to diagnose memory leakage in any kind of # native or runtime-managed code. example_name="$@" script_dir=$(dirname $BASH_SOURCE[0]) # Build the example cargo build --example "$example_name" # Sign it with the required entitlements for process debugging. codesign -s - -v -f --entitlements "$script_dir/debugger.entitlements" "./target/debug/examples/$example_name" # Run the example binary under `leaks` to look for any leaked objects. leaks --atExit -- "./target/debug/examples/$example_name"